<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Coding Challenges]]></title><description><![CDATA[A weekly Coding Challenge to help software engineers level up.]]></description><link>https://codingchallenges.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!tr2O!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F14c3fd5e-953d-4509-ab4c-cbbb37d99969_1080x1080.png</url><title>Coding Challenges</title><link>https://codingchallenges.substack.com</link></image><generator>Substack</generator><lastBuildDate>Sun, 07 Jun 2026 08:08:25 GMT</lastBuildDate><atom:link href="https://codingchallenges.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[John Crickett]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[codingchallenges@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[codingchallenges@substack.com]]></itunes:email><itunes:name><![CDATA[John Crickett]]></itunes:name></itunes:owner><itunes:author><![CDATA[John Crickett]]></itunes:author><googleplay:owner><![CDATA[codingchallenges@substack.com]]></googleplay:owner><googleplay:email><![CDATA[codingchallenges@substack.com]]></googleplay:email><googleplay:author><![CDATA[John Crickett]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Coding Challenge #123 - Database Driven LLM Wiki]]></title><description><![CDATA[This challenge is to build your own database powered LLM Wiki.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-122-database-driven</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-122-database-driven</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 06 Jun 2026 08:01:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ece01156-5758-4fd2-97a7-fa895710a3e9_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #123 - Database Driven LLM Wiki</h2><p>This challenge is to build your own personal knowledge base tool - a system where an LLM agent reads your curated sources, extracts the key information, and builds you a living, interlinked wiki that grows smarter with everything you add.</p><p>Most people&#8217;s experience with LLMs and documents is RAG: upload files, ask questions, get answers stitched together from retrieved chunks. It works, but the LLM rediscovers knowledge from scratch every time. Ask a subtle question that spans five documents, and the system has to find and piece together fragments it&#8217;s seen before. Nothing accumulates.</p><p>LLM Wiki takes a different approach. Instead of just retrieving from raw documents at query time, an LLM agent incrementally builds and maintains a persistent wiki - a structured collection of markdown files that sits between you and your sources. When you add a new source, the agent reads it, extracts key information, and integrates it into the existing wiki: updating entity pages, revising topic summaries, noting where new data contradicts old claims, strengthening or challenging the evolving synthesis. The knowledge is compiled once and then kept current, not re-derived on every query.</p><p>You&#8217;re in charge of sourcing, exploration, and asking good questions. The LLM does all the grunt work: summarising, cross-referencing, filing, and the bookkeeping that makes a knowledge base actually useful over time.</p><p>This challenge is inspired by Andrej Karpathy&#8217;s <a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f">LLM Wiki</a> concept - a pattern for building personal knowledge bases using LLMs. Karpathy describes the idea in the abstract; our job is to build a working implementation.</p><p>Under the hood, the system stores vector embeddings and full-text indexes for every wiki page in <a href="https://fandf.co/4x9nXJd">Oracle AI Database</a>. When you ask a question, it finds relevant pages using hybrid search, and an LLM synthesises an answer with citations. The agentic behaviour - ingestion workflows, multi-step querying, lint passes - is orchestrated using LangGraph&#8217;s state machine model, while LangChain handles the LLM integration. It&#8217;s a practical introduction to agent-based knowledge management, vector search, full-text search, and building tools that genuinely compound in value over time.</p><h2>The Challenge - Building LLM Wiki</h2><p>You&#8217;re going to build a personal knowledge base that an LLM agent writes and maintains for you. It starts by ingesting source documents into a wiki of markdown files, then lets you query it through a web interface or CLI chat. Step by step you&#8217;ll add wiki scaffolding, source ingestion, index and log management, vector storage, semantic retrieval, project management, linting, and a user interface. By the end, you&#8217;ll have a tool that genuinely helps you build and navigate a growing body of knowledge.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>You&#8217;ll need to make a few decisions and get some infrastructure running:</p><ol><li><p><strong>Set up your vector and full-text database.</strong> You&#8217;ll need <a href="https://fandf.co/4x9nXJd">Oracle Database 26ai</a> running in a local Docker container. Pull the <code>container-registry.oracle.com/database/free:latest</code> image, start the container, and set a password for the admin account. You can find full setup instructions in the <a href="https://fandf.co/4nSWzL1">Oracle Database Free Get Started guide</a>. Once the container is running, connect using a SQL client and verify you can create a table. Store all credentials in an environment file, not hardcoded anywhere.</p></li></ol><pre><code><code>docker pull container-registry.oracle.com/database/free:latest
docker run -d -p 1521:1521 -e ORACLE_PWD=&lt;your-password&gt; container-registry.oracle.com/database/free:latest
</code></code></pre><ol><li><p><strong>Choose your embedding model.</strong> You need a text embedding model that captures semantic meaning. Nomic&#8217;s <code>nomic-embed-text</code> is open source (Apache 2.0) and runs locally on CPU. It produces 768-dimensional vectors. Install it via Hugging Face: <code>pip install sentence-transformers</code> and load it as <code>nomic-ai/nomic-embed-text-v2-moe</code>. Any general-purpose embedding model with reasonable semantic quality will work - the key requirement is that it can capture the meaning of wiki pages well enough to find relevant ones for a given query.</p></li><li><p><strong>Set up your LLM provider.</strong> You&#8217;ll need a language model for writing wiki pages, summarising sources, and answering questions. Any provider with a chat API will work - Anthropic, OpenAI, Google, Mistral, or a local model via Ollama. The model needs to be capable enough to write coherent markdown and extract structured information from source documents.</p></li><li><p><strong>Set up LangChain and LangGraph.</strong> You&#8217;ll use LangChain for LLM integration and LangGraph for orchestrating the agent&#8217;s workflows. Install both: <code>pip install langchain langgraph</code>. LangChain handles the plumbing of prompting and response parsing. LangGraph handles the agentic flows - the multi-step processes of ingesting a source (read, extract, write summaries, update pages, update index, log), answering queries (search, read pages, synthesise), and running lint passes.</p></li><li><p><strong>Pick a topic for your first wiki.</strong> Choose a domain you&#8217;re genuinely curious about and gather 50-100 source documents - articles, papers, blog posts - about it. Save them as markdown or plain text files. This will be your test corpus. Pick something where the articles naturally reference the same entities and concepts, so there are connections for your wiki to surface. A technical topic works well (e.g. database internals, a specific ML technique, a programming language), but any domain with depth will do - history, cooking, fitness, whatever interests you.</p></li></ol><p><strong>Testing:</strong> Verify your Oracle Database container is running and you can connect to it. Load your embedding model and generate a test embedding to confirm it returns a vector of 768 dimensions. Make a test call to your LLM API to confirm it returns a valid response. Verify your environment file is being read correctly and no credentials are in your source code.</p><h3>Step 1</h3><p>In this step your goal is to build the wiki scaffolding - the system that creates and manages the directory of markdown files that will become your knowledge base.</p><p>The wiki lives on disk as a directory of markdown files. Before you can ingest sources or answer questions, you need the infrastructure to create pages, write content to them, and link them together. Think of this as the file system layer of your knowledge base.</p><p>Start by defining what a new wiki looks like on disk. When you create a wiki, the system should scaffold a directory structure with subdirectories for different page types: summaries for source summaries, entities for pages about people/companies/concepts, topics for overview pages, and a raw directory for the original source files. Alongside the directories, create a schema file (call it <a href="http://schema.md/">SCHEMA.md</a>) that defines the conventions for this wiki - what the directories are for, what naming conventions to use, what frontmatter fields pages should have, and how cross-references should be formatted.</p><p>Pages should carry YAML frontmatter at the top with metadata: title, type (entity, concept, summary, overview), date created, date updated, tags, and a list of sources the page draws from. The frontmatter makes pages queryable later and lets tools like Obsidian&#8217;s Dataview plugin generate dynamic views.</p><p>Cross-references between pages should use standard markdown links or wikilinks (<code>[[Page Name]]</code>). Which style you use is up to you, but the schema file should record the convention so the agent can be consistent. When one page references another, the agent should be able to follow that link, read the target page, and update both sides of the relationship.</p><p>Build a simple CLI that lets you create a new wiki, list existing wikis, and inspect a wiki&#8217;s structure - how many pages it has, what directories exist, and what the schema says. This CLI is just for development and testing; you&#8217;ll replace it with a proper interface later.</p><p><strong>Testing:</strong></p><ul><li><p>Create a wiki. Verify the directory structure and <a href="http://schema.md/">SCHEMA.md</a> file were created on disk.</p></li><li><p>Manually add a few test pages with frontmatter and cross-references. Verify the frontmatter parses correctly and links resolve to the expected paths.</p></li><li><p>Create a second wiki with a different schema configuration (different frontmatter fields, different link style). Verify both wikis coexist and respect their own conventions.</p></li><li><p>List your wikis and verify both appear.</p></li><li><p>Delete a wiki directory manually and verify the listing correctly reflects its absence.</p></li></ul><h3>Step 2</h3><p>In this step your goal is to build the source ingestion pipeline - the agent workflow that reads a source document and integrates its knowledge into the wiki.</p><p>This is the heart of the system. When you drop a new source into the raw directory and tell the agent to ingest it, a multi-step workflow begins. The agent reads the source, extracts key information, discusses the takeaways with you (in interactive mode), and then updates the wiki across multiple pages.</p><p>Model the ingestion workflow as a graph in LangGraph. Each node handles one concern: read the source, extract entities and concepts, identify claims and key information, write a summary page in the summaries directory, update or create entity pages for each entity found, update or create concept pages for each concept, revise topic overview pages, flag contradictions with existing content, update the index, and append an entry to the log. A single source might touch 10-15 wiki pages.</p><p>The agent should be able to detect contradictions. When a new source makes a claim that conflicts with something already in the wiki, the agent should note the discrepancy on the relevant page rather than silently overwriting or ignoring it. The user should be able to see where sources disagree and make their own judgement.</p><p>The agent should also identify gaps - entities or concepts referenced in the source that don&#8217;t yet have pages - and create stub pages or flag them for later attention.</p><p>Think carefully about how you prompt the LLM for each of these tasks. The quality of the wiki depends entirely on the quality of the extraction and synthesis. You&#8217;ll likely need different prompts for different page types: a summary page prompt, an entity page prompt, a concept page prompt, and so on. The schema file you built in Step 1 should guide these prompts.</p><p>The original source file should go into the raw directory and never be modified. The agent reads from it but never writes to it. This is your source of truth.</p><p><strong>Testing:</strong></p><ul><li><p>Ingest a single source document (a short article, 500-1000 words) into a test wiki. Verify the agent creates a summary page that captures the key points without hallucinating facts not in the source.</p></li><li><p>Verify the agent creates or updates entity pages for the key people, companies, or concepts mentioned in the source.</p></li><li><p>Verify the agent creates or updates topic overview pages that connect this source to existing knowledge (if the wiki already has content).</p></li><li><p>Ingest a second source on the same topic that contradicts something in the first source. Verify the agent flags the contradiction on the relevant page.</p></li><li><p>Check the wiki directory after ingestion. It should contain new or updated files in the summaries, entities, and topics directories. The raw directory should contain the original source unchanged.</p></li><li><p>Ingest a source that mentions an entity not yet in the wiki. Verify the agent creates a stub page or flags the gap.</p></li></ul><h3>Step 3</h3><p>In this step your goal is to build the index and log - two special files that help the agent (and you) navigate the wiki as it grows.</p><p>The index (<code>index.md</code>) is content-oriented. It&#8217;s a catalogue of every page in the wiki, organised by category: entities, concepts, sources, overviews. Each entry includes a link to the page, a one-line summary, and optionally metadata like creation date and the number of sources that feed into it. When the agent needs to answer a query, it reads the index first to find candidate pages, then drills into the most relevant ones. This approach works well at moderate scale (hundreds of pages) and avoids the need for embedding-based RAG infrastructure at the browsing level.</p><p>The log (<code>log.md</code>) is chronological. It&#8217;s an append-only record of everything that happened: ingests, queries, lint passes, schema changes. Each entry starts with a consistent prefix format: <code>## [YYYY-MM-DD] type | Description</code>. This makes the log parseable with simple command-line tools - <code>grep "^## \\\\[" log.md | tail -5</code> gives you the last five entries.</p><p>The key design decision is that the agent owns both files. Every ingestion should update the index with new pages and revised summaries. Every operation should append to the log. The agent should read the index at the start of every query to know what&#8217;s available. The agent should read the log at the start of every session to know what&#8217;s been done recently.</p><p>Build the index and log maintenance into your LangGraph workflows from Step 2. After the agent finishes writing wiki pages for an ingestion, it should update the index and append to the log as the final nodes in the graph. If an ingestion fails partway through, the log should record the failure.</p><p><strong>Testing:</strong></p><ul><li><p>Ingest a source and verify the index is updated with entries for the new summary page, entity pages, and concept pages. Each entry should have a link and a one-line description.</p></li><li><p>Ingest a second source and verify the index reflects both sources, with shared entity pages showing updated descriptions.</p></li><li><p>Check the log after several ingestions. Verify each entry has the correct format (<code>## [YYYY-MM-DD] ingest | Title</code>) and appears in chronological order.</p></li><li><p>Run <code>grep "^## \\\\[" log.md</code> and verify you get a clean chronological listing.</p></li><li><p>Manually delete a wiki page. Ingest a new source and verify the index accounts for the missing page (removes the stale entry rather than leaving dead links).</p></li></ul><h3>Step 4</h3><p>In this step your goal is to add semantic search over your wiki pages using vector embeddings and full-text search stored in <a href="https://fandf.co/4x9nXJd">Oracle AI Database</a>.</p><p>So far the agent navigates the wiki by reading the index and following links. That works at moderate scale, but as your wiki grows to hundreds of pages, you&#8217;ll want semantic search - finding pages by meaning, not just by browsing the catalogue.</p><p>Take every wiki page (excluding the index and log themselves, and excluding raw source files), generate a vector embedding for it using your embedding model, and store the embedding alongside the page&#8217;s path, title, type, tags, and a snippet or summary in Oracle Database. The metadata fields should all be stored and indexed so you can filter by type (&#8221;only entity pages&#8221;) or by tag.</p><p>Create a vector index on the embedding column for fast cosine similarity search. Also create an Oracle Text full-text index on the page content (or at minimum on the title and summary fields). Vector search finds semantically related pages even when the words don&#8217;t match. Full-text search catches exact names, technical terms, and phrases that vector search might rank lower. Together they give you robust retrieval.</p><p>Think about when embeddings should be generated. Every time the agent creates or updates a page during ingestion, the new or revised page needs to be re-embedded and stored. Pages that weren&#8217;t touched by an ingestion should keep their existing embeddings. You&#8217;ll need to track which pages changed so you only re-embed those.</p><p>Also think about what you embed. You could embed the full page text, but long pages might dilute the semantic signal. You could embed a summary or the first N paragraphs. You could embed both the title and the body separately and combine scores. Experiment and find what works for your test corpus - different approaches suit different types of content.</p><p><strong>Testing:</strong></p><ul><li><p>Run the full pipeline against your test wiki: parse all pages, generate embeddings, store in Oracle Database. Verify that the number of stored embeddings matches the number of wiki pages.</p></li><li><p>Query Oracle Database directly to inspect a few stored entries. Verify each contains the embedding vector, page path, title, type, and tags.</p></li><li><p>Verify that both the vector index and the full-text index have been created.</p></li><li><p>Update a page (by ingesting a new source that modifies an existing entity page). Verify only the changed page is re-embedded; unchanged pages keep their existing embeddings.</p></li><li><p>Search for a page by a concept it discusses (not by its exact title). Verify vector search returns it even though the words don&#8217;t match.</p></li><li><p>Search for an exact technical term or person&#8217;s name. Verify full-text search catches it with high confidence.</p></li></ul><h3>Step 5</h3><p>In this step your goal is to build the query system - the agent workflow that answers your questions by searching the wiki and synthesising a response.</p><p>Now that you have a searchable wiki, you need the agent to put it to use. When you ask a question, the agent should follow a multi-step process: read the index to identify candidate pages, search for semantically relevant pages using the hybrid search you built in Step 4, read the most relevant pages in full, and synthesise an answer with citations to specific pages and sections.</p><p>Model the query workflow as a LangGraph graph. The nodes might include: read index, hybrid search, read candidate pages, and synthesise answer. If the agent finds gaps - the question touches on something the wiki doesn&#8217;t cover well - it should say so honestly rather than speculating.</p><p>The system should support different answer formats depending on the question. A comparison between two concepts might be best as a table. A timeline of events might be best as a chronological list. A straightforward explanation might be best as prose. Give the LLM the flexibility to choose the format, and provide guidance in your prompts.</p><p>An important capability: the system should offer to file good answers back into the wiki as new pages. When you ask a question that generates a useful analysis, comparison, or synthesis, the agent should ask if you want to save it. If you do, it writes the answer as a new wiki page, adds it to the index, logs it, and embeds it. This way your explorations compound in the knowledge base just like ingested sources do.</p><p>Support follow-up questions within a session. If you ask &#8220;tell me about X&#8221; and then &#8220;what about Y?&#8221;, the agent should understand from context that Y relates to the broader topic X is part of. LangGraph&#8217;s state carries conversation context forward between queries.</p><p><strong>Testing:</strong></p><ul><li><p>Ask a question about something well-covered in your wiki. The answer should be accurate, cite specific pages, and not hallucinate facts not in the wiki.</p></li><li><p>Ask the same question with different phrasing. Verify you get a similar answer - semantic search should match by meaning, not exact wording.</p></li><li><p>Ask a question that spans two or more wiki pages. Verify the agent reads multiple pages and synthesises an answer that connects them.</p></li><li><p>Ask a follow-up question without re-stating the topic. Verify the agent maintains context from the previous exchange.</p></li><li><p>Ask a question about something not covered in your wiki. Verify the agent honestly reports the gap rather than making things up.</p></li><li><p>Ask the agent to save an answer as a wiki page. Verify the page is created on disk, added to the index, logged, and embedded in Oracle Database.</p></li><li><p>Ask a comparison question (&#8221;what&#8217;s the difference between X and Y?&#8221;). Verify the answer uses an appropriate format (table, side-by-side, etc.).</p></li></ul><h3>Step 6</h3><p>In this step your goal is to add wiki project management so you can maintain separate knowledge bases for different topics.</p><p>A single wiki is useful, but you&#8217;ll likely want separate knowledge bases for different areas of your life - one for your research topic, one for book notes, one for health and fitness, one for career learning. Each should be isolated, with its own set of pages, its own embeddings, and its own agent memory.</p><p>Add support for named wiki projects. Store project metadata - name, creation date, last ingestion timestamp, page count, source count - in <a href="https://fandf.co/4x9nXJd">Oracle AI Database</a>. Each project&#8217;s embeddings and metadata should be isolated so that searches against one wiki never return pages from another.</p><p>The user should be able to create a new wiki project, list existing projects, and select which project to work with. When the user starts the system, they should be able to specify a project name and immediately pick up where they left off.</p><p>All wiki data should persist between runs. The markdown files live on disk in their project directories. The embeddings and metadata live in <a href="https://fandf.co/4x9nXJd">Oracle AI Database</a>. When the user comes back and selects a project, everything should be exactly as they left it - the same pages, the same index, the same log, the same search capability.</p><p><strong>Testing:</strong></p><ul><li><p>Create two wikis on different topics, each with a small set of source documents. Ingest sources into both.</p></li><li><p>Query one wiki and verify results come only from that wiki, not the other.</p></li><li><p>List your wikis and verify both appear with correct names and metadata (page count, last ingestion time).</p></li><li><p>Query the project metadata directly in Oracle Database and verify it matches what the system reports.</p></li><li><p>Stop and restart your system. Verify all wiki data is intact and queryable for both projects.</p></li><li><p>Add a new source to an existing wiki and re-run ingestion. Verify only new or changed pages are re-embedded; unchanged pages preserve their existing embeddings.</p></li></ul><h3>Step 7</h3><p>In this step your goal is to build a linting system that health-checks your wiki and helps it stay consistent as it grows.</p><p>As your wiki accumulates pages and sources, inconsistencies creep in. A page makes a claim that a newer source contradicts, but the older page was never updated. A concept is discussed across five pages but never got its own dedicated page. A page references another that was renamed or deleted. Links only go one way. Gaps appear where you have half the story. Humans abandon wikis because this maintenance burden grows faster than the value. The agent can handle it.</p><p>Build a lint operation as a LangGraph workflow. The agent should walk the wiki systematically and check for: contradictions between pages (two pages make incompatible claims), stale claims (a page asserts something that a newer source has revised or disproven), orphan pages (pages with no inbound links from other wiki pages), missing pages (entities or concepts referenced but lacking their own page), broken cross-references (links that point to non-existent pages), and data gaps (areas where the wiki is thin and could benefit from additional sources).</p><p>The agent should report its findings as a prioritised list: critical issues first (contradictions, stale claims), then warnings (orphans, missing pages), then suggestions (gaps, possible new sources to look for). Each issue should include the specific pages involved and a suggested action.</p><p>The lint pass should also suggest new questions to investigate and new sources to look for - what&#8217;s missing from the wiki that would fill important gaps? This turns the lint operation from a bug-finding exercise into a research planning tool.</p><p>Make the lint operation interactive by default. The agent presents its findings, and you accept, reject, or modify each suggestion before any changes are made. The agent should not modify pages without confirmation unless you explicitly run in auto-fix mode.</p><p><strong>Testing:</strong></p><ul><li><p>Create a deliberate contradiction: write two entity pages that make incompatible claims about the same thing. Run lint and verify the agent detects and reports the contradiction, citing both pages.</p></li><li><p>Create an orphan page: a page with no other pages linking to it. Run lint and verify it&#8217;s flagged.</p></li><li><p>Reference a page that doesn&#8217;t exist (a broken wikilink). Run lint and verify the broken link is reported.</p></li><li><p>Have a concept discussed across multiple pages but without its own dedicated page. Run lint and verify the agent suggests creating one.</p></li><li><p>Accept a lint suggestion and verify the agent applies the fix correctly.</p></li><li><p>Reject a lint suggestion and verify no changes are made.</p></li></ul><h3>Step 8</h3><p>In this step your goal is to build a user interface for your LLM Wiki, providing a chat-based interface for querying and managing your knowledge base.</p><p>Your wiki agent currently works through a development CLI. Now give it a proper interface. You have a choice: build a web interface (similar to the Code Sherpa challenge), a CLI chat interface, or both. The core requirements are the same regardless.</p><p>The interface should provide a chat panel for querying the wiki. The user types questions in natural language, and the agent responds with synthesised answers that cite specific wiki pages. Responses should render markdown so that tables, lists, and formatted text display clearly. Citations should be clickable links that open the referenced page.</p><p>The interface should support the full query workflow you built in Step 5: semantic search, multi-page synthesis, follow-up questions with context, different answer formats, and saving answers as new wiki pages.</p><p>Provide a way to browse the wiki structure: a page tree or list showing categories (entities, concepts, summaries, overviews) and the pages within each. This helps the user understand the shape of their knowledge base at a glance.</p><p>When the agent is processing a query, show a loading state so the user knows something is happening. LangGraph workflows can take several seconds as the agent reads the index, searches for pages, reads them, and synthesises an answer.</p><p>Build a separate CLI tool for source ingestion that doesn&#8217;t require launching the full interface. A user should be able to run something like <code>llm-wiki ingest my-research ./new-article.md</code> and have the agent process the source in the background, updating the wiki, index, log, and embeddings. This makes adding sources a quick, low-friction operation.</p><p><strong>Testing:</strong></p><ul><li><p>Launch the interface and verify you can access it.</p></li><li><p>Select a wiki project and ask a question. Verify the response appears with markdown rendering and citations to wiki pages.</p></li><li><p>Click a citation link. Verify it opens the referenced page.</p></li><li><p>Ask a follow-up question and verify the system maintains context from the previous exchange.</p></li><li><p>Browse the wiki structure through the interface. Verify it accurately reflects the pages on disk.</p></li><li><p>Ask the agent to save an answer as a wiki page. Verify the page appears in the wiki structure immediately.</p></li><li><p>Use the CLI ingestion tool to add a new source. Verify the wiki updates without needing to launch the full interface.</p></li><li><p>Submit a query and verify a loading indicator appears while the agent processes it.</p></li></ul><h3>Going Further</h3><p>You&#8217;ve built a working personal knowledge base with an LLM agent that ingests sources, writes wiki pages, answers questions, and keeps everything consistent. Here are some ways to take it further:</p><ul><li><p><strong>Cloud database support:</strong> Add an option to connect to a cloud-hosted <a href="https://fandf.co/4x9nXJd">Oracle AI Database</a> instance instead of the local Docker container. Read the connection string from configuration.</p></li><li><p><strong>Batch ingestion:</strong> Add a batch mode that ingests multiple sources at once with less supervision. The agent processes each source, generates updates, and presents a summary of changes for your review rather than discussing each source individually.</p></li><li><p><strong>Marp slide generation:</strong> Add the ability to generate slide decks (using Marp format) from wiki content. This turns a collection of pages on a topic into a presentation with a single request.</p></li><li><p><strong>Obsidian integration:</strong> Build tighter integration with Obsidian. Watch the wiki directory for changes made through Obsidian and update embeddings automatically. Use Obsidian&#8217;s Dataview plugin with the frontmatter your agent already writes.</p></li><li><p><strong>Multi-format sources:</strong> Extend source ingestion to handle PDFs, web URLs (with scraping), and audio transcripts. The more formats you support, the more knowledge you can capture.</p></li></ul><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #122 - AI-Powered Contract Review Agent]]></title><description><![CDATA[This challenge is to build your own AI powered agent to review documents.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-122-ai-powered-contract</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-122-ai-powered-contract</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 30 May 2026 08:01:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f0b1db65-d6bc-4f77-8a68-080398afe4dc_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #122 - AI-Powered Contract Review Agent</h2><p>This challenge is to build your own AI-powered contract review agent using <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> - an application that takes a PDF contract, breaks it into clauses, analyses each one for risk in parallel using LLMs, pauses for human review, and streams back a final summary. Whilst the example of contract review, the workflow is applicable to many other domains.</p><p><em>This challenge was created in collaboration with <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>, whose platform provides durable background tasks with no timeouts, built-in retries, concurrency controls, human-in-the-loop pause points, real-time streaming, and full observability - all in TypeScript.</em></p><p>Contract review is one of the most time-consuming parts of legal work. Lawyers spend hours poring over dense documents looking for risky clauses, ambiguous language, and missing terms. An AI agent that can do the first pass, flag issues, and then incorporate human feedback before producing a final report would save enormous amounts of time.</p><p>Building this from scratch means you&#8217;d need to solve several hard infrastructure problems: job queuing with retries, parallel execution with concurrency control, durable pause-and-resume for human review, real-time streaming to the frontend, and execution tracing. That&#8217;s exactly what <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> handles for you. You define your workflow as a set of tasks - functions that can run for as long as needed - and <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> takes care of the rest. Your focus stays on the application logic: extracting clauses, analysing risk, and generating summaries.</p><p>By the end you&#8217;ll have a deep understanding of durable workflow orchestration and how a platform like <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> makes building AI-powered applications dramatically simpler.</p><h2>The Challenge - Building Your Own AI-Powered Contract Review Agent</h2><p>You&#8217;re going to build a web application that lets users upload PDF contracts and then kicks off a series of durable <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> tasks. Those tasks will extract text, split it into clauses, analyse each clause in parallel with automatic retries, pause for human approval, and stream a final summary back to the frontend in real time. The system will handle contracts with 50+ clauses reliably, support multiple LLM providers, and give you complete visibility into every step through Trigger.dev&#8217;s built-in dashboard.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>You&#8217;ll need to make a few decisions:</p><ol><li><p><strong>Set up your Trigger.dev project.</strong> <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> is a TypeScript-first platform, so you&#8217;ll be working in that ecosystem. Scaffold a new project using <code>npx create-trigger@latest</code> and follow the quickstart to get a task running. Choose a framework for your web app - Next.js is a natural fit since it pairs well with <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>, but you can also use Express, Remix, or any Node.js framework. Get your <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> API key from the <a href="https://fandf.co/4fjMkNR">dashboard</a> and configure your environment.</p></li><li><p><strong>Choose your LLM providers.</strong> Sign up for API keys. You&#8217;ll build an abstraction layer so you can swap providers without changing the rest of your code.</p></li><li><p><strong>Choose your database.</strong> You&#8217;ll need to persist users, contracts, clause analyses, reviewer decisions, and final summaries. PostgreSQL works well and integrates naturally with Prisma. Pick what you&#8217;re most comfortable with.</p></li><li><p><strong>Understand the Trigger.dev project structure.</strong> Your tasks live in the <code>trigger/</code> folder. Each file defines one or more tasks using the <code>task()</code> function. These are functions that can run indefinitely with no timeouts, automatic retries on failure, and built-in logging. You trigger tasks from your web app, and you can chain tasks together using <code>triggerAndWait()</code> or <code>batchTriggerAndWait()</code>. Take a few minutes to read through the <a href="https://fandf.co/4um7NKr">tasks documentation</a> and get comfortable with the concepts.</p></li></ol><p><strong>Testing:</strong> Run the example task that <code>create-trigger</code> generates (usually called <code>hello-world</code>). Trigger it from your web app and verify it appears in the <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> dashboard. Make a simple API call to each of your chosen LLM providers with a basic prompt and verify you get a coherent response. Set up your database, create a test table, and verify you can read and write data. Once all three are working independently, you&#8217;re ready to start building.</p><h3>Step 1</h3><p>In this step your goal is to build user authentication and a lightweight homepage.</p><p>Create a sign-up and login system using email and password. You&#8217;ll need a user model in your database, registration and login forms, and session management. Keep the auth simple - you don&#8217;t need OAuth or social login, just email/password with hashed passwords.</p><p>Build a lightweight homepage that explains what the product does. It doesn&#8217;t need to be a full marketing site - just a clear explanation that this is an AI-powered contract review tool, what it does, and a call-to-action to sign up or log in.</p><p><strong>Testing:</strong></p><ul><li><p>Visit the homepage and verify it renders correctly with the product explanation.</p></li><li><p>Register a new account with an email and password. Verify you&#8217;re redirected and logged in.</p></li><li><p>Log out and log back in with the same credentials.</p></li><li><p>Try registering with an email that already exists. You should get an appropriate error.</p></li><li><p>Try logging in with the wrong password. You should get an appropriate error, not a crash.</p></li></ul><h3>Step 2</h3><p>In this step your goal is to build the PDF upload and text extraction pipeline.</p><p>Create a web UI where logged-in users can drag and drop or select a PDF contract file for upload. Once uploaded, store the file and extract the raw text from the PDF. Use a PDF parsing library - pdf-parse for simpler PDFs or pdf.js for more complex documents.</p><p>This is a great place to introduce your first custom task. Create a <code>processContractUpload</code> task in your <code>trigger/</code> folder. When a user uploads a PDF, your web app should store the file, create a contract record in your database, and then trigger the task with the contract ID. The task should:</p><ul><li><p>Extract raw text from the PDF</p></li><li><p>Store the extracted text back to the database, linked to the contract</p></li><li><p>Update the contract status</p></li></ul><p>You don&#8217;t need to worry about timeouts - <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> tasks can run as long as needed, which is important for large PDFs. You also don&#8217;t need to worry about what happens if the server restarts mid-processing. The task will resume where it left off.</p><p><strong>Testing:</strong></p><ul><li><p>Upload a multi-page PDF document. Verify the text is extracted and stored in the database.</p></li><li><p>Upload a PDF with unusual formatting (headers, footers, columns). Check how well your extraction library handles it. Some garbled text is expected.</p></li><li><p>Try uploading a non-PDF file. Your application should reject it with a clear error message.</p></li><li><p>Try uploading without being logged in. The application should redirect to the login page.</p></li><li><p>Open the <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> dashboard. You should see your <code>processContractUpload</code> task run with a status, duration, and any logs you emitted.</p></li></ul><h3>Step 3</h3><p>In this step your goal is to split the extracted text into individual clauses using an LLM.</p><p>Raw extracted PDF text is rarely clean. You&#8217;ll have page numbers, headers, footers, and sometimes text in the wrong order. Your job now is to take that extracted text and use an LLM to identify and split it into individual, well-formed clauses.</p><p>Extend your <code>processContractUpload</code> task or create a new child task that takes the extracted text, sends it to an LLM with a prompt instructing the LLM to return the text split into clauses, and stores the results. Each clause should be a distinct logical unit - a paragraph, a condition, a definition, a warranty.</p><p>Store the identified clauses in your database, linked to the contract. Each clause should have a reference number (1, 2, 3...) so you can refer to it later.</p><p><strong>Testing:</strong></p><ul><li><p>Upload a contract and verify it gets split into multiple clauses. For a typical multi-page contract, you should get at least 10-15 clauses.</p></li><li><p>Inspect the clauses in your database. Each one should be a coherent, self-contained piece of text, not a fragment mid-sentence.</p></li><li><p>Upload a very short document (a single paragraph). It should still work, returning one clause.</p></li><li><p>Check the <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> dashboard and verify the clause-splitting step appears in the run timeline.</p></li></ul><h3>Step 4</h3><p>In this step your goal is to analyse each clause in parallel using LLMs to flag risk levels and ambiguous language.</p><p>This is the core of the application - and where <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s features really shine. For each clause, you need to send it to an LLM for analysis. The analysis should identify:</p><ul><li><p><strong>Risk level</strong> - high, medium, or low</p></li><li><p><strong>Risk explanation</strong> - a short explanation of why the clause is risky</p></li><li><p><strong>Ambiguous language</strong> - any vague terms like &#8220;reasonable efforts&#8221;, &#8220;as soon as practical&#8221;, &#8220;material adverse change&#8221; that could be interpreted differently</p></li><li><p><strong>Recommendations</strong> - suggested changes to reduce risk or clarify ambiguity</p></li></ul><p>Create an <code>analyseClause</code> task that takes a clause ID and text, calls your LLM, and stores the analysis result in the database. Configure it with retry settings so transient LLM API failures are handled automatically - <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> will retry with exponential backoff by default.</p><p>Now for the parallelism. From your parent task, use <code>analyseClause.batchTriggerAndWait()</code> to trigger all clause analyses in a single batch call. <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> will execute them in parallel (up to your environment&#8217;s concurrency limit), collect all the results, and return them to your parent task. A 50-clause contract is no problem - you get fan-out parallelism without writing any queue infrastructure.</p><p>Set a <code>concurrencyLimit</code> on the <code>analyseClause</code> task&#8217;s queue if you need to respect LLM API rate limits. For example, if your OpenAI tier allows 10 concurrent requests, set <code>queue: { concurrencyLimit: 10 }</code>.</p><p><strong>Testing:</strong></p><ul><li><p>Upload a contract and verify all clauses are analysed, each with a risk level, explanation, ambiguity flags, and recommendations.</p></li><li><p>Check the <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> dashboard run view. You should see the parent task with all the child <code>analyseClause</code> runs, their individual statuses, durations, and any retries.</p></li><li><p>Temporarily use a rate-limited API key and verify that failed analyses are automatically retried and eventually succeed. Watch the retries in the dashboard.</p></li><li><p>Upload a 50+ clause contract. Verify it completes reliably. All 50+ analyses should be in the database.</p></li><li><p>Inspect a few analyses. A clause that says &#8220;The Provider shall not be liable for any damages&#8221; should be flagged as high risk. A clause that mentions &#8220;reasonable efforts&#8221; should be flagged for ambiguous language.</p></li></ul><h3>Step 5</h3><p>In this step your goal is to aggregate the clause analyses into a structured review report and pause for human review.</p><p>Once all clause analyses are complete, your parent task should aggregate them into a structured review report. The report should show each clause number, the clause text, the risk level, the analysis explanation, any ambiguous language found, and recommendations. Group clauses by risk level (high first) so the reviewer can tackle the most important issues first.</p><p>Now for the human-in-the-loop part - this is where <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s waitpoint system comes in. After aggregating the results, use <code>wait.createToken()</code> to create a pause point. Store the token ID alongside the contract in your database so your review dashboard can reference it later. Then call <code>wait.forToken()</code> - your task will suspend at this point. <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> checkpoints the task state and releases compute resources. You&#8217;re not paying for idle time, and there&#8217;s no timeout.</p><p>Send an email notification to the user with a link to the review dashboard. <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> has hooks for this - you can use the <code>onSuccess</code> hook of the analysis task, or send the email before the waitpoint. Use a transactional email service like Resend, SendGrid, or Mailgun.</p><p><strong>Testing:</strong></p><ul><li><p>Complete an analysis on a contract. Verify the aggregated report is stored in the database, ordered by risk level.</p></li><li><p>Verify the email notification is sent containing the correct summary statistics and a working link to the dashboard.</p></li><li><p>Check the <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> dashboard. The run should show as <code>WAITING</code> - it&#8217;s suspended at the waitpoint, waiting for the review token to be completed.</p></li><li><p>Verify that the task does not proceed until someone completes the token.</p></li></ul><h3>Step 6</h3><p>In this step your goal is to build the review dashboard where a reviewer can approve, reject, or annotate each flagged clause.</p><p>Build a web dashboard that displays the aggregated review report. For each clause, the reviewer should be able to:</p><ul><li><p><strong>Approve</strong> the clause as is (no changes needed)</p></li><li><p><strong>Reject</strong> the clause (it needs revision)</p></li><li><p><strong>Annotate</strong> it with a free-text note explaining their reasoning or providing instructions</p></li></ul><p>The dashboard should show the original clause text alongside the AI&#8217;s analysis so the reviewer has full context to make a decision. Make it easy to navigate between clauses and see at a glance which ones have been reviewed and which still need attention.</p><p>When the reviewer is done and submits their review, your application should save all decisions and annotations to the database, then complete the waitpoint token to resume the suspended task. Use <code>wait.completeToken()</code> from your web app backend (or send a POST to the token&#8217;s URL from the frontend using the public access token). The task will resume exactly where it left off, with all the reviewer&#8217;s decisions available from the token&#8217;s output.</p><p>You can also use <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s <a href="https://fandf.co/4dqxJi4">Realtime</a> hooks in your dashboard. <code>useRealtimeRun()</code> lets you subscribe to run status changes without polling - so your dashboard can show the live status of the contract review workflow.</p><p><strong>Testing:</strong></p><ul><li><p>Navigate to the review dashboard for a contract. Verify all clauses are displayed with their AI analysis.</p></li><li><p>Approve a few clauses, reject a few, and add annotations to some. Verify the decisions are saved to the database.</p></li><li><p>Submit the review. Verify the token is completed and the <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> task resumes.</p></li><li><p>Watch the <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> dashboard during review submission. The run should transition from <code>WAITING</code> to running again.</p></li><li><p>Before submitting, check that you can see visually which clauses have been reviewed and which haven&#8217;t.</p></li></ul><h3>Step 7</h3><p>In this step your goal is to make the system LLM provider-agnostic with a configurable abstraction layer.</p><p>Your clause analysis and summary generation tasks currently call one or two specific LLM providers. Build an abstraction layer so you can swap providers without changing your task code.</p><p>Define a common interface for LLM interactions: a function that takes a prompt (or messages), configuration (temperature, max tokens, etc.), and returns a standardised response with the generated text and metadata (tokens used, finish reason, etc.).</p><p>Configuration should be externalised. Provider selection, model choice, and API keys should come from environment variables. Your <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> tasks should interact with the abstraction layer, not with any specific provider&#8217;s SDK directly.</p><p><strong>Testing:</strong></p><ul><li><p>Run a full contract analysis using one LLM provider. Verify it works end to end.</p></li><li><p>Switch the configuration to use another provider instead. Rerun the same contract. The analysis should complete with comparable results.</p></li><li><p>Swap providers without changing a single line of task code (only environment variables).</p></li><li><p>Verify your abstraction layer captures provider-agnostic metadata regardless of which provider is underneath.</p></li></ul><h3>Step 8</h3><p>In this step your goal is to generate the final summary report using an LLM, incorporating the clause analyses and reviewer feedback.</p><p>Now that the human review is complete, the waitpoint token has been completed, and your task has resumed, it&#8217;s time to generate the final summary.</p><p>Create a <code>generateSummary</code> task that loads all clause analyses, reviewer decisions, and annotations from the database, sends everything to an LLM, and asks it to synthesise a final report. The final summary should include:</p><ul><li><p><strong>Executive summary</strong> - a high-level overview of the contract&#8217;s risk profile</p></li><li><p><strong>Key findings</strong> - the most important issues identified, incorporating reviewer feedback</p></li><li><p><strong>Risk breakdown</strong> - a summary of risk levels across the contract</p></li><li><p><strong>Clause-by-clause detail</strong> - for each clause, the original risk assessment and the reviewer&#8217;s decision, combined into a final recommendation</p></li></ul><p>This should be a well-written, professional document that could be shared with a client or colleague. Your parent task should trigger the summary generation using <code>triggerAndWait()</code> so it gets the result back.</p><p><strong>Testing:</strong></p><ul><li><p>Generate a final summary for a contract that has been fully reviewed. Verify it includes all sections and incorporates both the AI analysis and the reviewer feedback.</p></li><li><p>Check that clauses the reviewer approved show as &#8220;accepted&#8221; in the final report, while rejected clauses include the reviewer&#8217;s annotations and reasoning.</p></li><li><p>Verify the summary is stored in the database and linked to the contract.</p></li><li><p>Read the summary from start to finish. It should read as a coherent, professional document, not a jumble of disconnected analyses.</p></li></ul><h3>Step 9</h3><p>In this step your goal is to stream the final summary to the frontend in real time as it is being generated.</p><p>Final summaries can be long, and waiting for a complete document before showing anything is a poor user experience. <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> has first-class support for streaming data from tasks to your frontend. Use <a href="https://trigger.dev/docs/realtime">Realtime streams</a> to pipe LLM tokens directly to the browser as they&#8217;re generated.</p><p>Define a stream using <code>streams.define()</code> - give it a clear ID like <code>"summary-output"</code> and a type for the stream chunks. In your <code>generateSummary</code> task, configure your LLM call to stream tokens, and pipe the stream to your defined stream using <code>.pipe()</code>.</p><p>In your frontend, use the <code>useRealtimeStream()</code> React hook to subscribe to the stream. As tokens arrive, your component renders them incrementally. No polling, no WebSocket management, no SSE wiring - the hook handles the connection automatically.</p><p>The user should also be able to receive the final summary via email as an alternative. Once the stream is complete, send the full summary as an email.</p><p><strong>Testing:</strong></p><ul><li><p>Generate a final summary and watch the frontend. Tokens should appear incrementally, not all at once at the end.</p></li><li><p>Verify that the stream works across page refreshes - existing chunks should be replayed.</p></li><li><p>Check that the streaming handles slow generation gracefully. Partial content should render without freezing.</p></li><li><p>Verify the email delivery option works. Trigger an email with the completed summary and check your inbox.</p></li><li><p>Generate summaries from different providers. Streaming should work regardless of which provider is configured.</p></li></ul><h3>Step 10</h3><p>In this step your goal is to explore <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s built-in observability - run tracing, logging, and monitoring.</p><p>You&#8217;ve already been using <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s dashboard throughout this challenge to see your tasks run. Now let&#8217;s make the most of its observability features. Unlike building your own tracing system from scratch, <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> gives you this out of the box.</p><p>Add <strong>tags</strong> to your tasks and runs so you can filter them in the dashboard. For example, tag runs with the contract ID, the user ID, the LLM provider used, and the workflow stage.</p><p>Use <code>runs.metadata</code> to attach structured data to your runs that updates as the workflow progresses. For example, set metadata for the number of clauses found, the count of high/medium/low risk clauses, the review status, and any error counts. This metadata appears in the dashboard and is available via the SDK.</p><p>Use <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s built-in <code>logger</code> throughout your tasks. It automatically captures log entries with timestamps and attaches them to the run - no log aggregation infrastructure needed.</p><p>Finally, explore the dashboard&#8217;s run view. You can see the full timeline of your contract review workflow: when each <code>analyseClause</code> child task started and completed, which clauses triggered which retries, how long each LLM call took, and any errors that occurred. The batch trigger view shows all parallel clause analyses at a glance, with individual run statuses and durations.</p><p><strong>Testing:</strong></p><ul><li><p>Run a complete contract review. Open the <a href="https://fandf.co/4f6c6oF">Trigger.dev</a> dashboard and find your run.</p></li><li><p>Verify you can see every step in the timeline: PDF extraction, clause splitting, batch clause analysis (with all child runs), waitpoint pause, waitpoint completion, and summary generation.</p></li><li><p>Click into individual <code>analyseClause</code> runs. You should see logs, duration, and whether any retries occurred.</p></li><li><p>Apply filters in the dashboard using your tags. Filter by status (failed runs only), by user, or by date range.</p></li><li><p>Add metadata to your runs and verify it appears in the dashboard.</p></li></ul><h3>Going Further</h3><p>Want to take this further? Here are some ideas:</p><ul><li><p><strong>Use </strong><code>wait.for()</code><strong> to schedule follow-ups.</strong> Trigger a task that waits 7 days, then sends a reminder to review a contract that hasn&#8217;t been actioned.</p></li><li><p><strong>Use input streams for cancellation.</strong> Add a cancel button to the frontend that uses <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s input streams to abort a running summary generation mid-stream.</p></li><li><p><strong>Add support for more file formats.</strong> DOCX is even more common than PDF for contracts. Add support for Word documents and other formats.</p></li><li><p><strong>Add role-based access control.</strong> Different users might need different permissions - uploaders, reviewers, and administrators.</p></li><li><p><strong>Add comparison mode.</strong> Upload two versions of the same contract and have the LLM identify what changed and whether the changes alter the risk profile.</p></li><li><p><strong>Add custom risk categories.</strong> Let users define their own risk categories and rules, then use <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s <code>wait.forToken()</code> to collect approval for each category.</p></li><li><p><strong>Add a clause library.</strong> Build a library of standard, low-risk clause templates that the LLM can suggest as replacements for high-risk clauses.</p></li><li><p><strong>Use concurrency keys for multi-tenancy.</strong> Leverage <a href="https://fandf.co/4f6c6oF">Trigger.dev</a>&#8217;s <code>concurrencyKey</code> to give each organisation or user their own isolated queue.</p></li></ul><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #121 - Dd]]></title><description><![CDATA[This challenge is to build your own dd unix tool.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-121-dd</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-121-dd</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 23 May 2026 08:00:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0069aa6b-ee35-4dff-bd08-02e01882b66e_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #121 - Dd</h2><p>This challenge is to build your own version of <code>dd</code>, the low-level data copying and conversion utility found on every Unix-like system.</p><p><code>dd</code> has been around since the early days of Unix - it first appeared in Version 5 Unix in the mid-1970s. The name is a nod to IBM&#8217;s JCL (Job Control Language) <code>DD</code> statement, which was used to describe data sets on mainframes. Unlike most Unix tools, <code>dd</code> uses a <code>key=value</code> syntax for its arguments rather than the usual flags, another inheritance from its mainframe roots.</p><p>At its heart, <code>dd</code> reads data in fixed-size blocks, optionally transforms it, and writes it out. That simplicity makes it surprisingly powerful: it&#8217;s used to copy disk images, create files of a specific size, benchmark storage throughput, convert character encodings, and recover data from failing drives. If you&#8217;ve ever created a bootable USB stick with <code>dd if=image.iso of=/dev/sdb</code>, you&#8217;ve used it.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Dd</h2><p>In this challenge you&#8217;re going to build your own version of <code>dd</code>. You&#8217;ll start with the core block-copy loop and progressively add the operands and conversion options that make <code>dd</code> so versatile.</p><h3>Step Zero</h3><p>In this introductory step your goal is to set your environment up ready to begin developing and testing your solution.</p><p>Choose your target platform and programming language. <code>dd</code> is a low-level tool that benefits from a language with good support for binary I/O and byte-level manipulation all work well.</p><p>Before you start coding, spend a few minutes playing with the system <code>dd</code> to get a feel for how it behaves:</p><pre><code><code># Copy a file
dd if=/etc/hosts of=/tmp/hosts-copy

# Copy from stdin to stdout
echo "Hello, dd!" | dd

# Create a 1 MB file of zeroes
dd if=/dev/zero of=/tmp/zeros bs=1M count=1</code></code></pre><p>Notice the summary that <code>dd</code> prints to stderr when it finishes - something like:</p><pre><code><code>2+0 records in
2+0 records out
1024 bytes (1.0 kB, 1.0 KiB) copied, 0.000123 s, 8.3 MB/s</code></code></pre><p>That <code>2+0</code> notation means &#8220;2 full records and 0 partial records&#8221;. You&#8217;ll be implementing that too.</p><p>Also note that <code>dd</code> uses <code>key=value</code> operands rather than the usual <code>-flag</code> style. <code>if=</code>, <code>of=</code>, <code>bs=</code>, and so on are all positional arguments, not flags.</p><h3>Step 1</h3><p>In this step your goal is to implement the core block-copy loop.</p><p>Your <code>ccdd</code> should read data from stdin and write it to stdout in fixed-size blocks, defaulting to 512 bytes. It should support the <code>if=FILE</code> operand to read from a file instead of stdin, and the <code>of=FILE</code> operand to write to a file instead of stdout.</p><p>When it finishes, it should print a summary to stderr in the same format as the real <code>dd</code>:</p><pre><code><code>&lt;n&gt;+&lt;m&gt; records in
&lt;n&gt;+&lt;m&gt; records out
&lt;bytes&gt; bytes copied, &lt;time&gt; s, &lt;rate&gt; MB/s</code></code></pre><p>Where <code>n</code> is the number of full blocks read and <code>m</code> is the number of partial blocks (blocks where fewer bytes were available than the block size).</p><p><strong>Testing:</strong> Copy a file and verify the output is identical:</p><pre><code><code>ccdd if=/etc/hosts of=/tmp/hosts-copy
diff /etc/hosts /tmp/hosts-copy</code></code></pre><p>The <code>diff</code> should produce no output. Check the summary printed to stderr matches what the real <code>dd</code> reports. Also test reading from stdin and writing to stdout:</p><pre><code><code>echo "Hello, dd!" | ccdd | cat</code></code></pre><h3>Step 2</h3><p>In this step your goal is to add block size control with the <code>bs=</code>, <code>ibs=</code>, and <code>obs=</code> operands, along with size suffixes.</p><p>The <code>bs=BYTES</code> operand sets both the input and output block size simultaneously. The <code>ibs=BYTES</code> and <code>obs=BYTES</code> operands set them independently - useful when you want to read in small chunks but write in large ones, or vice versa.</p><p>You should support the following size suffixes on any byte count:</p><p>Suffix Multiplier <code>c</code> 1 <code>w</code> 2 <code>b</code> 512 <code>k</code> or <code>K</code> 1024 <code>M</code> 1,048,576 <code>G</code> 1,073,741,824</p><p>So <code>bs=4k</code> means a 4096-byte block size, and <code>bs=2M</code> means 2,097,152 bytes.</p><p><strong>Testing:</strong> Verify that different block sizes produce the same output:</p><pre><code><code>ccdd if=/etc/hosts of=/tmp/out1 bs=1
ccdd if=/etc/hosts of=/tmp/out2 bs=512
ccdd if=/etc/hosts of=/tmp/out3 bs=4k
diff /tmp/out1 /tmp/out2
diff /tmp/out1 /tmp/out3</code></code></pre><p>All three should be identical. Check the records in/out summary changes appropriately - a 1-byte block size will show many more records than a 4k block size for the same file.</p><h3>Step 3</h3><p>In this step your goal is to add the <code>count=N</code>, <code>skip=N</code>, and <code>seek=N</code> operands.</p><p><code>count=N</code> limits the copy to N input blocks. <code>skip=N</code> skips N input blocks before starting to copy (seeking forward in the input). <code>seek=N</code> skips N output blocks before starting to write (seeking forward in the output, leaving the beginning of the output file untouched).</p><p>These three operands are what make <code>dd</code> useful for working with disk images and binary file formats where you need to operate on a specific region of a file.</p><p><strong>Testing:</strong> Extract the middle portion of a file:</p><pre><code><code># Create a test file with known content
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' &gt; /tmp/test.bin  # 64 bytes of A
printf 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB' &gt;&gt; /tmp/test.bin # 64 bytes of B
printf 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC' &gt;&gt; /tmp/test.bin # 64 bytes of C

# Extract just the B section (skip 1 block of 64 bytes, copy 1 block)
ccdd if=/tmp/test.bin of=/tmp/out.bin bs=64 skip=1 count=1
xxd /tmp/out.bin</code></code></pre><p>The output should be 64 bytes of <code>B</code>. Test <code>seek=N</code> by writing into the middle of an existing file:</p><pre><code><code>dd if=/dev/zero of=/tmp/sparse.bin bs=64 count=3
ccdd if=/tmp/test.bin of=/tmp/sparse.bin bs=64 skip=1 count=1 seek=1
xxd /tmp/sparse.bin</code></code></pre><p>The middle 64 bytes should now be <code>B</code>, with zeroes before and after.</p><h3>Step 4</h3><p>In this step your goal is to implement the <code>conv=</code> operand with the text conversion options: <code>ucase</code>, <code>lcase</code>, and <code>swab</code>.</p><p><code>conv=ucase</code> converts all lowercase ASCII letters to uppercase as the data passes through. <code>conv=lcase</code> does the reverse. <code>conv=swab</code> swaps adjacent bytes - byte 0 with byte 1, byte 2 with byte 3, and so on. If an odd number of bytes is read, the last byte is held over and swapped with the first byte of the next block.</p><p>Multiple conversions can be combined with commas: <code>conv=ucase,swab</code>.</p><p><strong>Testing:</strong></p><pre><code><code>echo "Hello, World!" | ccdd conv=ucase</code></code></pre><p>Should output <code>HELLO, WORLD!</code>. Test <code>lcase</code>:</p><pre><code><code>echo "Hello, World!" | ccdd conv=lcase</code></code></pre><p>Should output <code>hello, world!</code>. Test <code>swab</code> with a known byte sequence:</p><pre><code><code>printf '\\x01\\x02\\x03\\x04' | ccdd conv=swab | xxd</code></code></pre><p>Should show <code>02 01 04 03</code> - each pair of bytes swapped.</p><h3>Step 5</h3><p>In this step your goal is to implement the remaining <code>conv=</code> options: <code>notrunc</code>, <code>noerror</code>, <code>sync</code>, and <code>sparse</code>.</p><p>By default, <code>dd</code> truncates the output file before writing. <code>conv=notrunc</code> disables this, leaving any existing content beyond what <code>dd</code> writes intact. This is essential when patching a specific region of a binary file.</p><p><code>conv=noerror</code> tells <code>dd</code> to continue after a read error rather than stopping. It&#8217;s used when recovering data from a failing drive - you&#8217;d rather get most of the data than none of it.</p><p><code>conv=sync</code> pads each input block with null bytes (<code>\\x00</code>) to the full input block size when a short read occurs. Combined with <code>noerror</code>, this is the standard recipe for imaging a failing drive: <code>dd if=/dev/sda of=image.img conv=noerror,sync</code>.</p><p><code>conv=sparse</code> is an optimisation: instead of writing blocks that are entirely null bytes, <code>dd</code> seeks past them in the output file. The filesystem records these as &#8220;holes&#8221;, creating a sparse file that takes up less actual disk space than its apparent size.</p><p><strong>Testing:</strong> Test <code>notrunc</code> by writing a short string into the middle of a longer file:</p><pre><code><code>echo "Hello, World!" &gt; /tmp/original.txt
echo "Hi" | ccdd of=/tmp/original.txt conv=notrunc
cat /tmp/original.txt</code></code></pre><p>The output should start with <code>Hi</code> but retain the rest of the original content (<code>, World!</code> and the newline). Without <code>notrunc</code>, the file would be truncated to just <code>Hi\\n</code>.</p><p>Test <code>sparse</code> by creating a file with large null regions and checking its actual disk usage:</p><pre><code><code>ccdd if=/dev/zero of=/tmp/sparse.img bs=1M count=100 conv=sparse
ls -lh /tmp/sparse.img    # apparent size: 100 MB
du -sh /tmp/sparse.img    # actual disk usage: near 0</code></code></pre><h3>Step 6</h3><p>In this step your goal is to implement the <code>status=</code> operand and <code>SIGUSR1</code> signal handling.</p><p><code>status=none</code> suppresses all output, including the final summary. <code>status=noxfer</code> suppresses the transfer statistics (bytes, time, rate) but still prints the records in/out counts. <code>status=progress</code> prints periodic transfer statistics to stderr while the copy is running, so you can see progress on long operations.</p><p>You should also handle the <code>SIGUSR1</code> signal: when your process receives it, print the current transfer statistics to stderr without interrupting the copy. This is how you check on a long-running <code>dd</code> without stopping it.</p><p><strong>Testing:</strong> Verify <code>status=none</code> produces no output at all:</p><pre><code><code>echo "test" | ccdd status=none 2&gt;/tmp/stderr.txt
cat /tmp/stderr.txt  # should be empty</code></code></pre><p>Test <code>status=progress</code> with a slow copy:</p><pre><code><code>ccdd if=/dev/zero of=/tmp/progress-test bs=1M count=500 status=progress</code></code></pre><p>You should see the statistics updating as the copy runs. Test <code>SIGUSR1</code>:</p><pre><code><code>ccdd if=/dev/zero of=/tmp/signal-test bs=1M count=1000 &amp;
PID=$!
sleep 1
kill -USR1 $PID
wait $PID</code></code></pre><p>Sending <code>SIGUSR1</code> should print the current statistics without stopping the copy.</p><h3>Step 7</h3><p>In this step your goal is to add <code>iflag=</code> and <code>oflag=</code> for I/O flags.</p><p><code>iflag=direct</code> opens the input file with <code>O_DIRECT</code> (or the platform equivalent), bypassing the OS page cache. This is useful for benchmarking raw storage throughput without cache effects. <code>oflag=direct</code> does the same for the output.</p><p><code>oflag=dsync</code> opens the output with <code>O_DSYNC</code>, which causes each write to block until the data is physically written to storage. This is slower but guarantees durability.</p><p><code>iflag=fullblock</code> changes how <code>dd</code> handles short reads on the input. Normally, a short read (fewer bytes than the block size) counts as a partial record. With <code>fullblock</code>, <code>dd</code> keeps reading until it has accumulated a full block or reaches end-of-file. This is important when reading from pipes or network sockets, where a single <code>read()</code> call may return less than the requested amount even when more data is coming.</p><p>Multiple flags can be combined with commas: <code>iflag=direct,fullblock</code>.</p><p><strong>Testing:</strong> Test <code>iflag=fullblock</code> with a pipe that delivers data in small chunks:</p><pre><code><code># Without fullblock, each small write becomes a partial record
yes | head -c 4096 | ccdd bs=512 &gt; /dev/null

# With fullblock, partial reads are accumulated into full blocks
yes | head -c 4096 | ccdd bs=512 iflag=fullblock &gt; /dev/null</code></code></pre><p>Compare the records in/out summary - <code>fullblock</code> should show fewer partial records. For <code>oflag=dsync</code>, copy a file and verify it completes successfully (the main observable effect is that it&#8217;s slower, as each write is synchronised to disk).</p><h3>Going Further</h3><ul><li><p>Add <code>count_bytes=N</code> and <code>skip_bytes=N</code> (GNU extensions) to operate in bytes rather than blocks - useful when the data doesn&#8217;t divide evenly into your block size</p></li><li><p>Implement <code>conv=ascii</code> and <code>conv=ebcdic</code> to convert between ASCII and EBCDIC character encodings, the original purpose of <code>dd</code>&#8216;s conversion mode</p></li><li><p>Add <code>iflag=count_bytes</code> so that <code>count=N</code> counts bytes rather than blocks</p></li><li><p>Build a progress bar using ANSI escape codes for <code>status=progress</code>, showing a visual indicator alongside the transfer rate</p></li><li><p>Benchmark your implementation against the system <code>dd</code> on a large file and see how close you can get - try different block sizes and see how throughput changes</p></li><li><p>Use your <code>ccdd</code> to create and restore a disk image of a USB drive (carefully!) and verify the image is byte-for-byte identical using <code>md5sum</code></p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #120 - md5sum]]></title><description><![CDATA[This challenge is to build your own md5sum.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-120-md5sum</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-120-md5sum</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 16 May 2026 08:16:11 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5b176582-1e6d-47a0-b27c-3d8b4130e544_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #120 - md5sum</h2><p>This challenge is to build your own version of <code>md5sum</code>, the command-line utility that computes and verifies MD5 message digests.</p><p>MD5 (Message-Digest Algorithm 5) was designed by Ronald Rivest in 1991 and published as RFC 1321. For decades it was the go-to hash function for verifying file integrity - you&#8217;d download a file, run <code>md5sum</code> on it, and compare the output to the hash published alongside it to make sure nothing got corrupted or tampered with.</p><p>These days MD5 is considered cryptographically broken (collisions can be generated in seconds on a modern laptop), but it&#8217;s still widely used for checksums, cache keys, and non-security purposes. More importantly for Coding Challenges, MD5 is a simple hash function to implement from scratch. Building it yourself is a wonderful way to demystify how hash functions work.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Your Own MD5sum</h2><p>In this challenge you&#8217;re going to build your own version of <code>md5sum</code>. There are two tracks through this challenge. Pick the one that suits you, or do both.</p><p><strong>Track 1 (Steps 1 - 3)</strong> gets you to a fully working <code>md5sum</code> clone using your language&#8217;s standard library or a third-party library for the hash computation. You&#8217;ll focus on the command-line interface, file handling, and the check mode.</p><p><strong>Track 2 (Steps 4 - 6)</strong> takes you deeper. You&#8217;ll implement the MD5 algorithm itself from scratch by following RFC 1321, replacing the library you used in Track 1 with your own code. This is where you&#8217;ll learn how hash functions actually work - message padding, Merkle-Damg&#229;rd construction, and the compression function that sits at the heart of MD5 (and SHA-1, and SHA-2).</p><p>Both tracks produce the same tool. The only difference is whether the hashing happens inside a library or inside your own code.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>Choose your target platform and programming language. Any language will work for Track 1. For Track 2, pick a language that gives you easy access to 32-bit unsigned integer arithmetic and bitwise operations.</p><p>Before you start coding, have a play with the system <code>md5sum</code> so you get a feel for how it behaves:</p><pre><code><code>echo -n "" | md5sum
echo -n "Hello, World!" | md5sum</code></code></pre><p>The output format is the 32-character hex digest, two spaces, and the filename (or <code>-</code> for stdin). Note the <code>-n</code> flag on <code>echo</code>, without it, <code>echo</code> appends a newline, which changes the hash. This is a common gotcha when testing.</p><p>If you&#8217;re planning to do Track 2, open <a href="https://www.ietf.org/rfc/rfc1321.txt">RFC 1321</a>. It&#8217;s short, readable, and includes reference C code and a full test suite in the appendix. You won&#8217;t need it until Step 4, but it&#8217;s worth a skim now.</p><h3>Step 1</h3><p>In this step your goal is to hash the contents of a file and print the result in the standard <code>md5sum</code> output format.</p><p>Your tool should accept one or more filenames as command-line arguments, compute the MD5 hash of each file&#8217;s contents, and print one line per file in the format:</p><pre><code><code>&lt;32-char hex digest&gt;  &lt;filename&gt;</code></code></pre><p>Note the two spaces between the digest and the filename, this is the <code>md5sum</code> convention for text mode. You can use your language&#8217;s built-in MD5 library or any third-party package for the hash computation. The focus in this step is on reading files, formatting the output correctly, and handling errors (for example, printing a message to stderr and continuing if a file doesn&#8217;t exist).</p><p><strong>Testing:</strong> Create a test file and compare your output to the system <code>md5sum</code>:</p><pre><code><code>echo -n "Coding Challenges" &gt; test.txt
md5sum test.txt
ccmd5 test.txt</code></code></pre><p>Both should produce the same hash. Test with multiple files:</p><pre><code><code>echo -n "File one" &gt; a.txt
echo -n "File two" &gt; b.txt
ccmd5 a.txt b.txt
md5sum a.txt b.txt</code></code></pre><p>The output should match line for line. Also test what happens when you pass a file that doesn&#8217;t exist - your tool should print an error message for that file and still process the remaining files.</p><h3>Step 2</h3><p>In this step your goal is to support reading from standard input and to handle binary files correctly.</p><p>When no filenames are given, your tool should read from stdin, compute the hash, and print the result with <code>-</code> as the filename. When <code>-</code> is given explicitly as a filename argument, it should also read from stdin.</p><p>Your tool should also support the <code>-b</code> flag for binary mode. In binary mode the output uses <code>*</code> (space-asterisk) between the digest and the filename instead of two spaces. On modern systems the hash is computed the same way regardless of mode, the flag only affects the output format and is a holdover from systems where text and binary file reads differed. But <code>md5sum</code> supports it, so yours should too.</p><p><strong>Testing:</strong></p><pre><code><code>echo -n "Hello" | ccmd5
echo -n "Hello" | md5sum</code></code></pre><p>Both should output the same hash followed by <code>-</code>. Test binary mode:</p><pre><code><code>ccmd5 -b test.txt
md5sum -b test.txt</code></code></pre><p>The output should show <code>*</code> before the filename instead of two spaces. Test with a binary file too -- an image, a compiled executable, or <code>/bin/ls</code> -- and verify your hash matches the system <code>md5sum</code>.</p><h3>Step 3</h3><p>In this step your goal is to implement check mode with the <code>-c</code> flag.</p><p>When called with <code>-c</code>, your tool should read a file containing previously generated checksums (one per line, in the same format your tool produces) and verify each one. For each line, it should read the named file, compute its hash, and compare it to the stored hash. It should print the filename followed by <code>OK</code> or <code>FAILED</code> for each entry.</p><p>At the end, if any checksums failed, your tool should print a summary line to stderr saying how many didn&#8217;t match, and exit with a non-zero status code. If all checksums match, it should exit with status 0.</p><p>Implement the <code>--quiet</code> flag, which suppresses the <code>OK</code> lines and only shows failures. And implement the <code>--status</code> flag, which suppresses all output and only sets the exit code -- useful in scripts.</p><p><strong>Testing:</strong> Generate a checksum file, then verify it:</p><pre><code><code>ccmd5 a.txt b.txt &gt; checksums.md5
ccmd5 -c checksums.md5</code></code></pre><p>You should see:</p><pre><code><code>a.txt: OK
b.txt: OK</code></code></pre><p>Now tamper with one of the files and re-check:</p><pre><code><code>echo -n "Changed" &gt; a.txt
ccmd5 -c checksums.md5</code></code></pre><p>You should see:</p><pre><code><code>a.txt: FAILED
b.txt: OK
ccmd5: WARNING: 1 computed checksum did NOT match</code></code></pre><p>Test <code>--quiet</code> (only the <code>FAILED</code> line should appear) and <code>--status</code> (no output, but <code>echo $?</code> should show a non-zero exit code).</p><p>If you&#8217;re happy with a working <code>md5sum</code> clone and aren&#8217;t interested in implementing the hash algorithm itself, skip ahead to Going Further. Otherwise, read on.</p><h3>Step 4</h3><p>In this step your goal is to implement MD5 message padding and preprocessing, the first stage of the algorithm.</p><p>From here on you&#8217;re replacing the library hash with your own implementation. By the end of Step 6, your tool should produce identical output using code you wrote yourself.</p><p>MD5 operates on the input message in 512-bit (64-byte) blocks. Before processing, the message must be padded so its length is a multiple of 512 bits. The padding works like this:</p><ol><li><p>Append a single <code>1</code> bit to the message (in practice, append the byte <code>0x80</code>).</p></li><li><p>Append zero bytes until the message length is 56 bytes short of a multiple of 64 (i.e., length mod 64 equals 56).</p></li><li><p>Append the original message length in bits as a 64-bit little-endian integer.</p></li></ol><p>This padding scheme means the final block always has room for the length field, and the <code>0x80</code> byte ensures the padding is unambiguous -- you can always tell where the original message ended.</p><p>Implement the padding, then split the padded message into 512-bit blocks. For now, just verify your padding is correct by checking it against the test vectors in RFC 1321 (Appendix A.5). You should also initialise the four 32-bit state variables (A, B, C, D) to the values specified in the RFC:</p><pre><code><code>A = 0x67452301
B = 0xefcdab89
C = 0x98badcfe
D = 0x10325476</code></code></pre><p>These are the starting values of the hash state, sometimes called the initialisation vector. They&#8217;re arbitrary constants chosen by Rivest.</p><p><strong>Testing:</strong> The empty string <code>""</code> has a length of 0 bits. After padding, you should have exactly one 64-byte block: <code>0x80</code>, followed by 55 zero bytes, followed by the 64-bit length (0) in little-endian. Print your padded block as hex and verify it looks right.</p><p>The string <code>"a"</code> has a length of 8 bits. After padding: <code>0x61 0x80</code>, 53 zero bytes, then <code>0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00</code>.</p><h3>Step 5</h3><p>In this step your goal is to implement the MD5 compression function, the core of the algorithm.</p><p>The compression function processes one 512-bit block at a time and updates the four state variables. For each block:</p><ol><li><p>Break the 64-byte block into sixteen 32-bit words (little-endian).</p></li><li><p>Initialise working variables <code>a, b, c, d</code> to the current state <code>A, B, C, D</code>.</p></li><li><p>Run 64 rounds, divided into four groups of 16. Each round applies a different auxiliary function to three of the four working variables, adds in one of the message words and a round constant, then rotates the result.</p></li></ol><p>The four auxiliary functions are:</p><ul><li><p><strong>F</strong> (rounds 0-15): <code>F(B, C, D) = (B AND C) OR (NOT B AND D)</code> - a bitwise conditional: &#8220;if B then C else D&#8221;</p></li><li><p><strong>G</strong> (rounds 16-31): <code>G(B, C, D) = (B AND D) OR (C AND NOT D)</code> - same idea, different arrangement</p></li><li><p><strong>H</strong> (rounds 32-47): <code>H(B, C, D) = B XOR C XOR D</code> - a parity function</p></li><li><p><strong>I</strong> (rounds 48-63): <code>I(B, C, D) = C XOR (B OR NOT D)</code> - another nonlinear mixing function</p></li></ul><p>Each round computes:</p><pre><code><code>a = b + left_rotate((a + func(b,c,d) + message_word + round_constant), shift_amount)</code></code></pre><p>Then the variables are rotated: the old <code>d</code> becomes the new <code>c</code>, the old <code>c</code> becomes the new <code>b</code>, and so on. The shift amounts and round constants are specified in the RFC -- there are 64 of each, and they&#8217;re fixed values you can hard-code as a table.</p><p>After all 64 rounds, add the working variables back into the state: <code>A += a</code>, <code>B += b</code>, <code>C += c</code>, <code>D += d</code>. This addition step is what makes the construction iterative, each block&#8217;s output becomes the next block&#8217;s input.</p><p>The final hash is the state variables A, B, C, D concatenated in little-endian byte order to produce the 128-bit (16-byte) digest, which is then printed as 32 hex characters.</p><p><strong>Testing:</strong> RFC 1321 provides test vectors in Appendix A.5. Your implementation should produce these exact hashes:</p><pre><code><code>MD5("") = d41d8cd98f00b204e9800998ecf8427e
MD5("a") = 0cc175b9c0f1b6a831c399e269772661
MD5("abc") = 900150983cd24fb0d6963f7d28e17f72
MD5("message digest") = f96b697d7cb7938d525a2f31aaf161d0
MD5("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b</code></code></pre><p>Work through these one at a time. If a hash doesn&#8217;t match, check your byte ordering, little-endian is the most common source of bugs. Once all five test vectors pass, swap your library hash for your own implementation and verify that your <code>ccmd5</code> tool still produces the same output as the system <code>md5sum</code> for every file you test.</p><h3>Step 6</h3><p>In this step your goal is to add support for SHA-256, so your tool can operate as both <code>md5sum</code> and <code>sha256sum</code>.</p><p>SHA-256 follows the same Merkle-Damg&#229;rd structure as MD5, pad the message, split into blocks, process each block through a compression function - but with different parameters. The blocks are still 512 bits, but the state is eight 32-bit words instead of four, the compression function runs 64 rounds with different operations (Ch, Maj, and two sigma functions instead of F/G/H/I), and the output is 256 bits instead of 128. The message length in the padding is big-endian rather than little-endian.</p><p>Add a <code>--algorithm</code> flag (or <code>-a</code>) that accepts <code>md5</code> or <code>sha256</code>, defaulting to <code>md5</code>. When SHA-256 is selected, the output format should match the system <code>sha256sum</code>, same layout, just a longer digest.</p><p>If you&#8217;ve structured your code well, the padding, block processing loop, and I/O code should be shared between both algorithms, with only the compression function and initialisation differing. This is a good test of how cleanly you&#8217;ve separated concerns.</p><p><strong>Testing:</strong> Verify against the system <code>sha256sum</code> and well-known test vectors:</p><pre><code><code>echo -n "" | ccmd5 -a sha256
echo -n "" | sha256sum</code></code></pre><p>Both should produce <code>e3b0c44298fc1c149afbf4c898fbf90a... -</code> (the SHA-256 of the empty string).</p><pre><code><code>echo -n "Hello, World!" | ccmd5 -a sha256
echo -n "Hello, World!" | sha256sum</code></code></pre><p>Test check mode with SHA-256 too -- generate a checksum file with <code>-a sha256</code> and verify it with <code>-c</code>.</p><h3>Going Further</h3><p>Here are some ideas to take your implementation further:</p><ul><li><p>Add support for SHA-1, SHA-384, and SHA-512 to build a complete family of hash tools</p></li><li><p>Add the <code>-tag</code> output format (BSD-style: <code>MD5 (filename) = digest</code>) which is what macOS <code>md5</code> uses by default</p></li><li><p>Implement HMAC-MD5 and HMAC-SHA256 using your hash functions - HMAC is how hash functions are used for message authentication in protocols like TLS</p></li><li><p>Benchmark your implementation against the system <code>md5sum</code> on a large file and see how close you can get - then try optimising with SIMD intrinsics or by processing multiple blocks in parallel</p></li><li><p>Read about the MD5 collision attacks (Wang et al., 2004) and try to understand how they exploit weaknesses in the auxiliary functions - it&#8217;s a fascinating bit of cryptographic history</p></li><li><p>Implement SHA-3 (Keccak), which uses a completely different construction (a sponge function) rather than Merkle-Damg&#229;rd - comparing the two designs is a great way to understand why the cryptographic community moved on from MD5&#8217;s family of designs</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #119 - AI Pong Player]]></title><description><![CDATA[This challenge is to build your own AI pong player with reinforcement learning.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-119-ai-pong-player</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-119-ai-pong-player</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 09 May 2026 08:01:53 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9e738510-2be1-459b-bd4d-753e0b749099_650x522.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #119 - AI Pong Player</h2><p>This challenge is to build your own reinforcement learning agent that learns to play Atari Pong directly from the pixels on the screen.</p><p>Pong is one of the oldest video games ever made, and it has a special place in the history of artificial intelligence. In 2013, DeepMind used Pong (and a handful of other Atari games) to show that a single algorithm could learn to play games at a human level, just by watching the screen and being told the score. That work kicked off the modern era of deep reinforcement learning. Pong is the friendliest of the Atari games to start with, the rules are simple, the screen is mostly empty, and the agent only needs to choose between moving the paddle up or down. That makes it the perfect first project for going from &#8220;I&#8217;ve read about reinforcement learning&#8221; to &#8220;I&#8217;ve actually trained an agent from raw pixels and watched it learn to win.&#8221; Building this project will introduce you to ideas you&#8217;ll come across again and again throughout your career: turning observations into features, sampling from a stochastic policy, computing returns, reducing variance, and the policy gradient itself.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Your Own AI Pong Player</h2><p>In this challenge you&#8217;re going to build a policy gradient agent that learns to play Pong from raw pixels using the REINFORCE algorithm. Your agent will start out playing randomly, lose 21-0 over and over, and then, if you&#8217;ve wired everything up correctly, gradually start scoring points, then winning rallies, and eventually beating the built-in opponent more often than it loses.</p><p>This challenge is a good fit if you&#8217;ve written some Python before, are comfortable with NumPy, and have at least a passing acquaintance with neural networks. You don&#8217;t need to be an reinforcement learning expert. REINFORCE is one of the simplest deep reinforcement learning algorithms there is, and the version we&#8217;ll build here is famously the one Andrej Karpathy described in his &#8220;<a href="https://karpathy.github.io/2016/05/31/rl/">Pong from Pixels</a>&#8220; blog post. A small policy network, no value function, no replay buffer, no target network. Just a policy, some episodes, and a gradient.</p><p>A word of warning before you start: training from pixels is slow. Even on a sensible setup, you should expect a few hours of CPU training before the agent really starts to win, and you may want to leave it running overnight. That&#8217;s part of the experience, watching the score curve crawl upwards over many thousands of episodes is genuinely exciting once you&#8217;ve built the thing yourself.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>Python is the natural choice for this challenge because the reinforcement learning ecosystem lives there, but the ideas transfer cleanly to any language with a deep learning framework.</p><p>You&#8217;ll need three things installed: <a href="https://gymnasium.farama.org/">Gymnasium</a> (the maintained successor to OpenAI Gym), the Atari environments via <a href="https://github.com/Farama-Foundation/Arcade-Learning-Environment">ALE-py</a>, and a deep learning framework, PyTorch, TensorFlow, or JAX are all fine, pick whichever you&#8217;d like to practise with. You&#8217;ll also want NumPy, Matplotlib, and probably <code>opencv-python</code> or <code>Pillow</code> for image work. Have a quick read of the <a href="https://gymnasium.farama.org/introduction/basic_usage/">Gymnasium docs</a> and the <a href="https://ale.farama.org/environments/pong/">Atari environment list</a> so you know what&#8217;s available.</p><p>Before you write any code, spend a few minutes playing Pong yourself if you&#8217;ve never seen it. Notice that the only thing that matters is your paddle&#8217;s vertical position, the ball&#8217;s position, and the ball&#8217;s direction of travel. Your agent will need to work this out from the screen, with no idea what any of those concepts mean.</p><h3>Step 1</h3><p>In this step your goal is to get a Pong environment running and have a &#8220;random agent&#8221; play a full game so you can see the data flowing.</p><p>Create the <code>ALE/Pong-v5</code> environment from Gymnasium and run a single episode where, at every step, you pick an action uniformly at random and step the environment with it. For each step, print or log the reward. You should see mostly zeros, with the occasional <code>-1</code> (the built-in opponent has scored against you) and very rarely a <code>+1</code> (you got lucky). The episode should end after twenty-one points have been scored on one side.</p><p>Have a look at the action space (<code>env.action_space</code>) and the observation space (<code>env.observation_space</code>). The action space has six entries, but for Pong you really only ever need two of them: the action that moves the paddle up and the action that moves it down. Constraining your agent&#8217;s choices to just those two actions makes learning much faster, because there are fewer wrong things it can do. Pick the two action indices you&#8217;ll use throughout the rest of the challenge and write them down somewhere obvious in your code.</p><p>The observation is a <code>210 x 160 x 3</code> RGB image - the raw screen. Have a look at one with Matplotlib so you know what your agent is seeing. There&#8217;s a lot of pixels there that have nothing to do with playing Pong: the score at the top, the borders down the sides, the colours. We&#8217;ll fix all of that in the next step.</p><p><strong>Testing:</strong> Run your random agent for one episode and confirm that:</p><ul><li><p>The episode terminates of its own accord (you don&#8217;t have to cap the step count)</p></li><li><p>The total reward is somewhere between roughly <code>21</code> and <code>15</code> (random play loses badly)</p></li><li><p>The observation shape is <code>(210, 160, 3)</code> with <code>uint8</code> values</p></li></ul><h3>Step 2</h3><p>In this step your goal is to turn the raw <code>210 x 160 x 3</code> screen into a much smaller representation that contains just the information your agent needs.</p><p>There are four things to do here, and they should all happen inside a single function that takes a raw frame and returns the preprocessed observation:</p><ol><li><p>Crop away the score area at the top of the screen and the borders on each side, leaving just the playing area.</p></li><li><p>Convert the result to greyscale - colour adds nothing useful in Pong.</p></li><li><p>Resize down to <code>80 x 80</code> pixels. The image was already mostly empty space; at this resolution you can still clearly see the paddles and the ball.</p></li><li><p>Flatten the <code>80 x 80</code> grid into a single 1D vector of length <code>6400</code>. This is the input format your policy network will expect.</p></li></ol><p>A static frame doesn&#8217;t tell your agent anything about which way the ball is moving, and direction is the most important thing in Pong. The classic trick - and the one used in the original Karpathy write-up - is to feed in the <em>difference</em> between the current preprocessed frame and the previous one. Pixels that didn&#8217;t change become zero, and pixels that did change show up as positive or negative values. The ball appears as a little bright streak pointing the way it&#8217;s travelling. Add this difference computation on top of your preprocessing function.</p><p><strong>Testing:</strong> Save a few raw frames and their preprocessed versions to disk and look at them with an image viewer. The preprocessed frame should clearly show the two paddles and the ball as bright pixels on a dark background, with nothing else. Display a frame difference - it should be almost entirely black except for the ball and the moving paddle.</p><p>A good sanity check: the output of your preprocessing function should be a 1D NumPy array of length <code>6400</code> (or whatever shape you&#8217;ve chosen) with <code>float32</code> values, not raw pixel bytes.</p><h3>Step 3</h3><p>In this step your goal is to build the neural network that maps a preprocessed observation to a probability distribution over actions, and use it to pick actions.</p><p>The policy network Karparthy describes is a tiny network - a single hidden layer with about 200 ReLU units, then an output layer that produces one number per action. Pass that output through a softmax (or a sigmoid if you&#8217;ve reduced things to a single output for &#8220;probability of moving up&#8221;) and you have a probability distribution. To pick an action, sample from that distribution rather than taking the most likely one.</p><p>Wire up an &#8220;act&#8221; function that takes a preprocessed frame, runs it through the network, and returns a sampled action plus whatever extra information you&#8217;ll need later for training (typically the log-probability of the action that was taken, or the network output itself).</p><p>Once that&#8217;s working, run another full episode - this time with your untrained network choosing the actions instead of <code>random.choice</code>. The agent will still lose badly (its weights are random), but the score should be in roughly the same ballpark as the random agent from Step 1. If you see something dramatically different, something is wrong with your preprocessing or your sampling.</p><p><strong>Testing:</strong> Run a single episode with the untrained policy. The total reward should be in the same <code>-21</code> to <code>-15</code> range as the random agent. The action distribution - if you log it - should be close to 50/50 at the start of training. Print the shape of the network output and the sampled action index for the first few steps to make sure everything lines up.</p><h3>Step 4</h3><p>In this step your goal is to collect a complete episode of experience and turn the rewards into the <em>returns</em> that will drive learning.</p><p>For each step in an episode, store three things: the observation that was fed in, the action that was taken (or its log-probability), and the reward that came back from the environment. At the end of the episode you&#8217;ll have three lists, all the same length.</p><p>Now compute the <em>discounted return</em> for each step. The return at step <code>t</code> is the sum of all the rewards from step <code>t</code> onwards, with rewards further in the future weighted by a discount factor <code>gamma</code> (use <code>0.99</code>). You should compute this as a single backwards pass over the reward list - much faster and cleaner than the obvious double loop. There&#8217;s one Pong-specific subtlety: every time someone scores a point, the rally ends and a new one begins inside the same episode. You probably want to reset the running sum when a non-zero reward appears, so credit for a point only flows back to the actions in that rally rather than all the way to the start of the game. This makes a big difference to learning speed.</p><p>Once you have the per-step returns, normalise them across the whole episode by subtracting the mean and dividing by the standard deviation. Normalised returns put roughly half the actions on the &#8220;this was better than average&#8221; side and half on the &#8220;this was worse&#8221; side, which gives the policy gradient a much more stable signal.</p><p><strong>Testing:</strong> Run an episode, compute the returns, and have a look:</p><ul><li><p>The length of your returns array matches the number of steps in the episode.</p></li><li><p>After normalisation, the mean should be close to zero and the standard deviation close to one.</p></li><li><p>For an action that was followed by a <code>+1</code> reward soon after, the return should be positive; for one followed by a <code>1</code>, it should be negative.</p></li></ul><p>A nice sanity print is to show, for the last twenty steps of an episode, the reward at that step and the discounted return - you&#8217;ll see the return building up smoothly and then jumping when a point is scored.</p><h3>Step 5</h3><p>In this step your goal is to actually update the policy in the direction that makes good actions more likely and bad actions less likely. This is the heart of the whole challenge.</p><p>The REINFORCE update is delightfully simple. For each step in your collected rollout, compute the loss as <code>-log(probability of the action taken) * normalised return for that step</code>, then sum (or average) across all the steps. Run that through your framework&#8217;s autograd, take a gradient step with an optimiser (Adam or RMSProp at a learning rate around <code>1e-3</code> to <code>1e-4</code> works well), and that&#8217;s it. Actions that led to better-than-average returns get pushed up; actions that led to worse-than-average returns get pushed down. You&#8217;re doing gradient <em>ascent</em> on expected return, even though you&#8217;re calling <code>loss.backward()</code>.</p><p>A single episode&#8217;s worth of gradient is very noisy. Batch up the gradients over multiple episodes - ten is a sensible starting point - before you actually call the optimiser. You can either accumulate gradients across episodes or concatenate the per-step data and do one bigger update; both work.</p><p>Now wrap the whole thing in a training loop that runs for thousands of episodes, prints a running average of the score after each one, and just leaves it going. Be patient. For the first few hundred episodes the score will hover around <code>-21</code> -- the policy is still essentially random and learning very slowly. After that, you should see the running average start to creep upwards. By the time you&#8217;ve trained for several thousand episodes (this can be many hours of wall-clock time on CPU), the running average should cross zero, meaning your agent is winning more rallies than it loses.</p><p><strong>Testing:</strong> This is the step where things either work or they very visibly don&#8217;t. A few things to check as training progresses:</p><ul><li><p>The running average reward should be trending upwards over time, not just bouncing around</p></li><li><p>After ~500 episodes, the agent should reliably score <em>some</em> points (running average above <code>21</code>)</p></li><li><p>After a few thousand episodes, the running average should be approaching zero or going positive</p></li><li><p>If the loss explodes or the score gets stuck at <code>21</code> forever, the most common culprits are: forgetting to reset the discounted return between rallies, an unnormalised return signal, the wrong sign on the loss, or feeding raw frames instead of frame differences</p></li></ul><p>If you&#8217;d like a stronger signal that things are alive, log the average length of an episode (in steps). Random play produces short episodes; an agent that&#8217;s learning to actually rally produces longer ones, well before the score itself starts to go up.</p><h3>Step 6</h3><p>In this step your goal is to make your training run something you can show off, not just a console of numbers scrolling by.</p><p>There are four things to add:</p><ol><li><p><strong>Save the model weights</strong> -- both periodically (every N episodes) and whenever a new best running average reward is achieved. You don&#8217;t want to leave a long training run going only to lose the weights.</p></li><li><p><strong>An evaluation mode</strong> that loads a saved set of weights, plays a fixed number of episodes with the policy fixed (no learning, ideally with greedy action selection rather than sampling), and reports the average score. This is what you&#8217;d use to honestly compare two different training runs.</p></li><li><p><strong>Video recording</strong> of the agent playing. Gymnasium has a <code>RecordVideo</code><a href="https://gymnasium.farama.org/api/wrappers/misc_wrappers/#gymnasium.wrappers.RecordVideo"> wrapper</a> that writes MP4s. Record a video of an early-training agent (it&#8217;ll be hilariously bad), a mid-training agent (starting to get the idea), and a late-training agent (winning, hopefully). Stitching these together is the single most satisfying artefact of the whole project.</p></li><li><p><strong>A training reward plot</strong> -- a simple Matplotlib chart showing the per-episode reward and a rolling average over the run. The shape of this curve, going from a flat line at <code>21</code> through random play and up into positive territory, is the picture of an agent learning.</p></li></ol><p><strong>Testing:</strong> Once you have all four bits in place:</p><ul><li><p>Kill your training script and restart it from a saved checkpoint. The running average should pick up roughly where it left off, not crash back to <code>21</code>.</p></li><li><p>Run evaluation on a checkpoint with sampling vs. greedy action selection; greedy should be at least as good as sampled.</p></li><li><p>Open one of your recorded videos and watch the agent play. It is unbelievably satisfying to see the paddle that you trained track the ball and put it past the opponent.</p></li></ul><h3>Going Further</h3><p>Here are some ideas to take your Pong agent further:</p><ul><li><p><strong>Add a baseline to reduce variance.</strong> REINFORCE has notoriously noisy gradients. Subtract a baseline - the simplest one is the running average reward, the next-simplest is a learned value function - from the returns before you scale the policy gradient. This is the first step from REINFORCE towards Actor-Critic.</p></li><li><p><strong>Replace the MLP with a small CNN.</strong> Convolutional layers are a much more natural fit for image input than a flattened MLP. You&#8217;ll lose the trick of feeding the frame difference and instead stack the last few frames as channels. Compare training time and final score against the MLP version.</p></li><li><p><strong>Try a different algorithm.</strong> Once you have all the scaffolding - environment, preprocessing, training loop, logging - you can swap the algorithm out without rewriting the rest. Implement A2C, PPO, or DQN against the same Pong setup and see how they compare on sample efficiency and final score.</p></li><li><p><strong>Run multiple environments in parallel.</strong> A single CPU core stepping through one game at a time is the bottleneck on most training runs. Use Gymnasium&#8217;s <code>AsyncVectorEnv</code> to step several Pong games at once and gather rollouts much faster.</p></li><li><p><strong>Train an opponent.</strong> The built-in Pong AI is fixed and not very good. Once your agent beats it consistently, you&#8217;ve topped out the score. A natural next step is self-play: have two copies of your agent play each other and improve together.</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #118 - Comm]]></title><description><![CDATA[This challenge is to build your own comm.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-118-comm</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-118-comm</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 02 May 2026 08:01:16 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c48e19e1-d27b-4b87-b143-8cd3e14a1199_2400x1350.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #118 - Comm</h2><p>This challenge is to build your own version of <code>comm</code>, the classic Unix utility that compares two sorted files line by line.</p><p><code>comm</code> is one of those small tools in the Unix toolbox that solves a single problem really well. Given two sorted files, it tells you three things at once: which lines are unique to the first file, which lines are unique to the second file, and which lines appear in both. It does this in a single streaming pass, taking advantage of the fact that the inputs are already sorted, so it never has to load whole files into memory and never has to do an O(n&#178;) comparison.</p><p>You&#8217;ll find <code>comm</code> used to diff lists of users, find files that exist in one directory tree but not another, work out the intersection of two datasets, and as a building block in countless shell pipelines. Building your own version is a lovely exercise in stream processing, careful state management, and the merge step that sits at the heart of merge sort, ideas you&#8217;ll reach for again and again throughout your career.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Comm</h2><p>In this challenge you&#8217;re going to build your own version of <code>comm</code>, a streaming, sorted file comparison tool. Your tool will read two sorted files, compare them line by line, and print three columns of output: lines unique to the first file, lines unique to the second file, and lines common to both. It will be compatible with the standard POSIX <code>comm</code> utility, which means you&#8217;ll be able to test your work directly against the system <code>comm</code> and use it as a drop-in replacement in shell pipelines.</p><p>The clever bit about <code>comm</code> is that it doesn&#8217;t sort the files for you, it relies on the fact that they are already sorted to do its work in a single pass with constant memory. Two read pointers, one per file, and a small handful of comparison rules are all you need.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>Choose your target platform and programming language. I&#8217;d encourage you to pick a language you&#8217;re comfortable with for reading files line by line and parsing command-line arguments. Pretty much any general-purpose language is a good fit for this challenge. The focus is on the algorithm, not on the language.</p><p>Before you start coding, have a read through the <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/comm.html">POSIX comm specification</a> and the man page on your own machine (<code>man comm</code>). Spend some time playing with the system <code>comm</code> so you get a feel for how it behaves, especially around the column indentation and the suppression flags.</p><p>Create a couple of small sorted test files to use throughout the challenge:</p><pre><code><code>printf "apple\nbanana\ncherry\ndate\nelderberry\n" &gt; file1.txt
printf "banana\ncherry\nfig\ngrape\n" &gt; file2.txt
</code></code></pre><p><code>file1.txt</code> and <code>file2.txt</code> are both sorted. The two files share <code>banana</code> and <code>cherry</code>, while <code>apple</code>, <code>date</code>, and <code>elderberry</code> are unique to the first and <code>fig</code> and <code>grape</code> are unique to the second. We&#8217;ll use these throughout the challenge.</p><p><strong>A quick note on locale.</strong> Both <code>sort</code> and the system <code>comm</code> only do byte-wise comparison when the locale is <code>C</code> or <code>POSIX</code>. With a default locale (especially on macOS), ordering becomes locale-aware -- <code>Apple</code> can sort <em>between</em> <code>apple</code> and <code>banana</code> rather than before them, which will confuse your test files and any comparisons you run against the system <code>comm</code>. Either set <code>LC_ALL=C</code> in your shell while working on this challenge, or prefix the relevant commands with it (<code>LC_ALL=C sort ...</code>, <code>LC_ALL=C comm ...</code>).</p><h3>Step 1</h3><p>In this step your goal is to read two sorted files and produce the three-column output that is the heart of <code>comm</code>.</p><p>Your tool should accept exactly two filenames as command-line arguments, open both files, and walk through them in sorted order using a single pass. For each line you should decide whether it belongs to column 1 (lines only in the first file), column 2 (lines only in the second file), or column 3 (lines in both). Column 1 has no leading tab, column 2 has one leading tab, and column 3 has two leading tabs. Lines should be compared byte by byte, the same way <code>LC_ALL=C sort</code> orders them.</p><p>You&#8217;ll also need to handle files of different lengths -- when one file runs out, the remaining lines from the other file should be emitted in the appropriate column. Empty files are a useful edge case to think about: if one file is empty, every line of the other file goes into its own column. Don&#8217;t worry about any flags or options yet; just get the basic three-column comparison working.</p><p><strong>Testing:</strong> Run your tool against the test files and check your output:</p><pre><code><code>ccomm file1.txt file2.txt
apple
&#9;&#9;banana
&#9;&#9;cherry
date
elderberry
&#9;fig
&#9;grape
</code></code></pre><p>Try it with files of different lengths, with one empty file, and with two completely disjoint files. Lines unique to the first file should appear with no leading tab, lines unique to the second file with one leading tab, and shared lines with two leading tabs.</p><h3>Step 2</h3><p>In this step your goal is to support the single-column suppression flags <code>-1</code>, <code>-2</code>, and <code>-3</code>.</p><p><code>comm</code> lets you hide any of the three columns from the output. <code>-1</code> hides lines unique to the first file, <code>-2</code> hides lines unique to the second file, and <code>-3</code> hides lines common to both. When a column is suppressed, the indentation for the remaining columns shifts left so the leading tabs go away. For example, with <code>-1</code> the second column should no longer have its leading tab, because there is no first column for it to sit next to.</p><p>Have a play with the system <code>comm</code> to see exactly how it lays things out. This is one of those details that is easier to copy than to describe.</p><p><strong>Testing:</strong></p><pre><code><code>ccomm -1 file1.txt file2.txt
&#9;banana
&#9;cherry
fig
grape
</code></code></pre><pre><code><code>ccomm -2 file1.txt file2.txt
apple
&#9;banana
&#9;cherry
date
elderberry
</code></code></pre><pre><code><code>ccomm -3 file1.txt file2.txt
apple
date
elderberry
&#9;fig
&#9;grape
</code></code></pre><h3>Step 3</h3><p>In this step your goal is to support combining the suppression flags.</p><p>The flags from Step 2 can be combined to suppress more than one column at a time. <code>-12</code> shows only the common lines, <code>-23</code> shows only lines unique to the first file, <code>-13</code> shows only lines unique to the second, and <code>-123</code> produces no output at all. The combinations can be supplied as a single argument (<code>-12</code>) or as separate arguments (<code>-1 -2</code>); both should behave identically.</p><p>If you wrote Step 2 in a flexible way -- tracking which columns are active rather than special-casing each flag -- this step should be a very small change.</p><p><strong>Testing:</strong></p><pre><code><code>ccomm -12 file1.txt file2.txt
banana
cherry
</code></code></pre><pre><code><code>ccomm -23 file1.txt file2.txt
apple
date
elderberry
</code></code></pre><pre><code><code>ccomm -13 file1.txt file2.txt
fig
grape
</code></code></pre><pre><code><code>ccomm -123 file1.txt file2.txt
</code></code></pre><pre><code><code>ccomm -1 -2 file1.txt file2.txt
banana
cherry
</code></code></pre><p>The fifth invocation should produce the same output as <code>-12</code>.</p><h3>Step 4</h3><p>In this step your goal is to support reading from standard input.</p><p>When <code>-</code> is given as one of the two filename arguments, your tool should read that input from <code>stdin</code> instead of opening a file. This is what lets <code>comm</code> slot into shell pipelines. Either of the two arguments can be <code>-</code>, but only one of them at a time -- if both are <code>-</code> your tool should report an error.</p><p><strong>Testing:</strong></p><pre><code><code>printf "apple\nbanana\ncherry\n" | ccomm - file2.txt
apple
&#9;&#9;banana
&#9;&#9;cherry
&#9;fig
&#9;grape
</code></code></pre><pre><code><code>cat file1.txt | ccomm file2.txt -
&#9;apple
&#9;&#9;banana
&#9;&#9;cherry
&#9;date
&#9;elderberry
fig
grape
</code></code></pre><p>The first command pipes a sorted list into your tool as the first input. The second pipes <code>file1.txt</code> in as the second input. Each should produce the same three-column comparison as the equivalent file-based invocation.</p><h3>Step 5</h3><p>In this step your goal is to support case-insensitive comparison with the <code>-i</code> flag.</p><p>When <code>-i</code> is set, lines that differ only in case (for example <code>Apple</code> and <code>apple</code>) should be treated as equal. The original case of the lines should still be preserved in the output -- only the comparison itself is case-insensitive.</p><p>This is the only step where the comparison rule changes, so it&#8217;s worth thinking about how you&#8217;ve structured your comparison code. If you&#8217;ve kept the comparison behind a single function, this should be a small change.</p><p><strong>Testing:</strong> Build a couple of files that differ only in case:</p><pre><code><code>printf "Apple\nBanana\nCherry\n" &gt; upper.txt
printf "apple\nbanana\ndate\n" &gt; lower.txt
</code></code></pre><p>Without <code>-i</code>, every line is unique to one file or the other (because <code>Apple</code> and <code>apple</code> compare differently):</p><pre><code><code>ccomm upper.txt lower.txt
Apple
Banana
Cherry
&#9;apple
&#9;banana
&#9;date
</code></code></pre><p>With <code>-i</code>, the matching pairs should appear in column 3:</p><pre><code><code>ccomm -i upper.txt lower.txt
&#9;&#9;Apple
&#9;&#9;Banana
Cherry
&#9;date
</code></code></pre><p>The third invocation combines <code>-i</code> with column suppression:</p><pre><code><code>ccomm -i -1 upper.txt lower.txt
&#9;Apple
&#9;Banana
date
</code></code></pre><h3>Going Further</h3><p>Here are some ideas to take your <code>comm</code> implementation further:</p><ul><li><p>Add the GNU <code>-check-order</code> and <code>-nocheck-order</code> flags so your tool can warn when its inputs aren&#8217;t really sorted</p></li><li><p>Add the GNU <code>-output-delimiter=STRING</code> flag for choosing a custom string between columns instead of tabs</p></li><li><p>Add the GNU <code>-total</code> flag to print a summary line with the count for each column at the end of the output</p></li><li><p>Add the GNU <code>z</code> / <code>-zero-terminated</code> flag so records are separated by <code>NUL</code> bytes instead of newlines, which is useful for working with filenames containing newlines or spaces</p></li><li><p>Add a <code>-header</code> flag that prints column headings before the output to make it easier to read interactively</p></li><li><p>Support comparing more than two files at once (you&#8217;ll need to think about what the output even looks like for three or more inputs)</p></li><li><p>Add a flag to compare on a specific field rather than the whole line, the way <code>join</code> and <code>sort -k</code> do</p></li><li><p>Support files compressed with <code>gzip</code> or <code>zstd</code> transparently, so your tool can read <code>.gz</code> files without an explicit <code>zcat</code></p></li><li><p>Build a streaming library version of your tool so other programs in your chosen language can use the comparison logic without shelling out</p></li><li><p>Benchmark your implementation against the system <code>comm</code> on very large files (think hundreds of millions of lines) and see if you can match or beat it</p></li><li><p>Try running your <code>comm</code> as part of a real pipeline in your own work -- finding files in one directory tree but not another, or comparing two snapshots of a database export, are good real-world tests</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #117 - AI Powered Support Bot]]></title><description><![CDATA[This challenge is to build your own AI powered support assistant.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-117-ai-powered-support</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-117-ai-powered-support</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 25 Apr 2026 08:01:45 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/17eee42d-3754-49c8-bd37-95e6adefd5b0_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #117 - AI Powered Support Bot</h2><p>This challenge is to build your own AI-powered customer support bot - and then discover, the hard way, why production AI applications need more than just an API key and a system prompt.</p><p><em>This challenge was created in collaboration with <strong><a href="https://orq.ai/">Orq.ai</a></strong>, whose Router provides a single API across 400+ models from 20+ providers - with built-in fallbacks, cost routing, and observability. Free to start, no markup on token costs.</em></p><p>Every developer has used a support bot. Most have opinions about them. In this challenge you&#8217;ll build one for a fictional version of Coding Challenges, giving it context about the available projects - Build Your Own Redis, Docker, Git and the rest - so it can answer questions about which challenge to tackle, what skills you&#8217;ll learn, how to get started, and general troubleshooting.</p><p>It starts simple. But step by step you&#8217;ll layer on the production concerns that real AI applications face: resilience when a provider goes down, observability so you know what&#8217;s happening, and cost routing so you&#8217;re not burning money on simple questions. By the time you&#8217;ve built all of that yourself, you&#8217;ll have a deep appreciation for what an AI gateway does - and you&#8217;ll see just how much code disappears when you use one.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Your Own AI Powered Support Bot</h2><p>You&#8217;re going to build an AI customer support bot that answers questions about Coding Challenges projects. Along the way you&#8217;ll experience the real production pain points of working with LLMs - provider lock-in, reliability, observability, and cost - and then see what happens when you replace your hand-rolled infrastructure with a single gateway endpoint.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>You&#8217;ll need to make a few decisions:</p><ol><li><p><strong>Choose your programming language.</strong> Pick something you&#8217;re comfortable building with. You&#8217;ll be making HTTP requests, handling JSON, and building a simple interactive loop. Python, TypeScript, Go, Java, and Rust all work well.</p></li><li><p><strong>Choose your first LLM provider.</strong> You need an API key from one of the major providers - OpenAI, Anthropic, Google (Gemini), or Mistral. You&#8217;ll be using their chat completions API. You can build using the REST API, an AI framework or install their SDK for your chosen language.</p></li><li><p><strong>Grab the context data </strong>from <a href="https://www.dropbox.com/scl/fi/kn7m3r6tuosz5vcs00gwy/challenge-data.json?rlkey=se3vp8q627dsg12cr66dw4o9a&amp;st=jvd0pnl6&amp;dl=0">here</a><strong>.</strong></p></li></ol><p><strong>Testing:</strong> Make a simple API call to your chosen provider with a basic prompt like &#8220;Hello, who are you?&#8221; and verify you get a coherent response back. If that works, you&#8217;re ready to move on.</p><h3>Step 1</h3><p>In this step your goal is to build a working support bot using a single LLM provider.</p><p>Build an interactive command-line application that takes user questions and responds using your chosen LLM. The bot should have a system prompt that includes the Coding Challenges context you downloaded in Step Zero, instructing it to act as a helpful customer support agent that answers questions based on that context.</p><p>Your bot should maintain a conversation history so follow-up questions work naturally. If a user asks &#8220;Which challenge should I start with?&#8221; and then follows up with &#8220;What will I learn from that one?&#8221;, the bot should understand what &#8220;that one&#8221; refers to.</p><p>Keep it simple. One provider, one model, one API key hardcoded (or read from an environment variable). No fallbacks, no logging, no clever routing. Just a bot that works.</p><p><strong>Testing:</strong></p><ul><li><p>Ask the bot a factual question about Coding Challenges based on the context data, like &#8220;What projects are available?&#8221; or &#8220;What are the frontend focused projects?&#8221; The answer should be accurate and drawn from your context.</p></li><li><p>Ask a follow-up question that references the previous answer. The bot should maintain context across the conversation.</p></li><li><p>Ask something the context doesn&#8217;t cover, like &#8220;Can you help me with my tax return?&#8221; The bot should politely explain that it can only help with Coding Challenges questions.</p></li><li><p>Ask for a personalised recommendation, like &#8220;I know Python and want to learn about networking - which challenge should I do?&#8221; The bot should give a thoughtful answer based on the available projects.</p></li></ul><h3>Step 2</h3><p>In this step your goal is to add resilience by introducing a fallback to a second LLM provider.</p><p>Imagine your primary provider goes down. Right now your bot is completely broken. To simulate this, temporarily use an invalid API key for your primary provider so every request fails.</p><p>Now fix it. Sign up for a second LLM provider and integrate their SDK alongside the first. When a request to the primary provider fails, your bot should automatically retry with the fallback provider. The user should get an answer either way.</p><p>This sounds straightforward, but pay attention to the friction. You now have two SDKs with different interfaces, two API keys to manage, two different authentication mechanisms, and subtly different request and response shapes. Your code needs to handle the differences, normalise the responses, and manage the error handling for both.</p><p>Once your fallback is working, remove the invalid API key and restore normal operation. Keep the fallback logic in place - you&#8217;ll want it for reliability.</p><p><strong>Testing:</strong></p><ul><li><p>With both providers working, verify the bot responds normally using the primary provider.</p></li><li><p>Invalidate your primary provider&#8217;s API key and send a question. The bot should seamlessly fall back to the second provider and still give a good answer. The user shouldn&#8217;t see an error - just a response.</p></li><li><p>Invalidate both API keys and verify the bot handles the failure gracefully with a clear error message rather than crashing.</p></li><li><p>Restore the primary key and verify the bot goes back to using the primary provider.</p></li><li><p>Take note of how much code you&#8217;ve added to handle two providers. Count the lines if you like. You&#8217;ll want that number later.</p></li></ul><h3>Step 3</h3><p>In this step your goal is to add observability by tracking token usage, latency, and cost for every request.</p><p>In production, you need to know what&#8217;s happening. How many tokens are you using? How much is each request costing? How long are responses taking? Without this information you&#8217;re flying blind.</p><p>Build a logging layer that captures the following for every LLM request:</p><ul><li><p><strong>Request ID</strong> - a unique identifier for each request, so you can correlate a user-visible response with its log entry.</p></li><li><p><strong>Timestamp</strong> of the request.</p></li><li><p><strong>Provider and model</strong> used (especially important now you have two providers).</p></li><li><p><strong>Token counts</strong> - prompt tokens and completion tokens. Most providers return these in the API response.</p></li><li><p><strong>Latency</strong> - how long the request took from start to finish.</p></li><li><p><strong>Estimated cost</strong> - calculate this from the token counts and the provider&#8217;s published pricing.</p></li><li><p><strong>Whether it was a primary or fallback request.</strong></p></li><li><p><strong>Error details</strong> - when a request fails, capture the error type, status code, and message. This is what tells you <em>why</em> the primary failed and the fallback kicked in.</p></li><li><p><strong>Finish reason</strong> - was it <code>stop</code>, <code>length</code>, content-filter, or a tool call? This matters when you&#8217;re investigating quality issues.</p></li></ul><p>Store these logs however you like - a local file, an in-memory list, a database. Add a way to view a summary: total requests, total tokens, total cost, average latency, and the breakdown between primary and fallback usage.</p><p>You might also consider capturing the request and response payloads (watch out for PII), a conversation or session ID so you can trace a whole support session, retry counts, and time-to-first-token once you&#8217;re streaming. These aren&#8217;t required, but they&#8217;re the kinds of things a production observability stack typically includes.</p><p>This is the kind of infrastructure that every production AI application needs, and building it yourself gives you an appreciation for how much work it is to get right. You need to handle it consistently across both providers despite their different response formats.</p><p><strong>Testing:</strong></p><ul><li><p>Send several questions to the bot and then check your logs. Every request should have a complete record with all the fields listed above.</p></li><li><p>Force a fallback by invalidating the primary key, send a question, then check the log. The entry should show the fallback provider was used.</p></li><li><p>View your summary statistics. They should accurately reflect the requests you&#8217;ve made - total cost, average latency, token counts.</p></li><li><p>Verify that token counts and cost calculations are consistent with what the providers report in their dashboards.</p></li></ul><h3>Step 4</h3><p>In this step your goal is to add cost-aware routing so that simple questions go to cheaper models and complex questions go to more capable (and more expensive) ones.</p><p>Not all questions are equal. &#8220;What&#8217;s the pricing?&#8221; is a simple lookup that any small model can handle. &#8220;I&#8217;m a backend developer who knows Python but wants to learn systems programming - which challenges should I do and in what order?&#8221; needs genuine reasoning ability.</p><p>Build a routing layer that classifies incoming questions and directs them to the appropriate model. You&#8217;ll need at least two tiers:</p><ul><li><p><strong>Simple queries</strong> - FAQs, greetings, straightforward factual lookups. Route these to a cheaper, faster model.</p></li><li><p><strong>Complex queries</strong> - personalised recommendations, multi-step reasoning, comparisons across projects, questions that require synthesising information. Route these to a more capable model.</p></li></ul><p>How you classify queries is up to you. You could use keyword matching, a separate lightweight LLM call to classify the question, message length heuristics, or some combination. The point is to reduce cost without noticeably reducing quality.</p><p>A note on what&#8217;s actually changing: the system prompt and context data stay the same for every request - the cheap model still needs to see the Coding Challenges context to answer &#8220;What&#8217;s the pricing?&#8221; The saving comes from running fewer parameters per token, not from sending less context. Don&#8217;t be tempted to trim the context for simple queries; that quickly leads to wrong answers.</p><p>This is where your codebase starts to feel the weight. You now have multiple providers, fallback logic, per-request logging across all of them, and routing logic that needs to work with all your models. Take a moment to look at your code. Count the lines dedicated to infrastructure versus the lines dedicated to the actual support bot logic.</p><p><strong>Testing:</strong></p><ul><li><p>Ask a simple question like &#8220;What&#8217;s the pricing?&#8221; and check your logs. It should be routed to the cheaper model.</p></li><li><p>Ask a complex question like &#8220;I want to learn distributed systems but I&#8217;ve never done any systems programming - what&#8217;s the best learning path through your challenges?&#8221; and check your logs. It should be routed to the more capable model.</p></li><li><p>Compare the costs in your logs between simple and complex queries. The simple queries should be noticeably cheaper.</p></li><li><p>Verify the quality of responses. Simple questions routed to the cheaper model should still be answered well. If the quality is poor, adjust your classification logic.</p></li><li><p>Send a mix of ten questions - some simple, some complex - and review the routing decisions in your logs. Most should be classified correctly.</p></li></ul><h3>Step 5</h3><p>In this step your goal is to replace all of the infrastructure you built in Steps 2 through 4 with the <a href="https://router.orq.ai/">Orq.ai Router</a>.</p><p>Sign up for a free <a href="http://orq.ai/">Orq.ai</a> account and get your API key. The Router provides an OpenAI-compatible endpoint, which means you can point any OpenAI SDK at it by changing the base URL and API key. That&#8217;s it.</p><p>Replace your multi-provider setup, your fallback logic, your logging infrastructure, and your routing layer with a single API call to the Router endpoint. The Router handles:</p><ul><li><p><strong>Fallbacks and retries</strong> - if a provider fails, the Router automatically retries with another. You only pay for successful completions.</p></li><li><p><strong>Observability</strong> - per-request traces with token counts, latency, cost, and error logs are available in the <a href="http://orq.ai/">Orq.ai</a> dashboard. No custom logging code needed.</p></li><li><p><strong>Cost routing</strong> - the Auto Router directs each prompt to the most cost-effective model that meets quality requirements, with a typical 50% cost reduction while retaining 98% quality.</p></li><li><p><strong>Access to 400+ models</strong> from 20+ providers through a single API key and a single interface.</p></li></ul><p>Now look at your code. The fallback handling from Step 2, the logging layer from Step 3, and the routing logic from Step 4 can all be removed. Your bot should be back to something close to the simplicity of Step 1, but with all the production capabilities you spent three steps building by hand.</p><p><strong>Testing:</strong></p><ul><li><p>Send the same mix of questions you used throughout the challenge. The responses should be at least as good as before.</p></li><li><p>Check the <a href="http://orq.ai/">Orq.ai</a> dashboard for your request logs. You should see token counts, costs, latency, and which model was used for each request - all without any custom logging code.</p></li><li><p>Simulate a provider failure (the Router handles this transparently). Send requests and verify they succeed even when individual providers have issues.</p></li><li><p>Compare the total lines of code in your solution now versus at the end of Step 4. The infrastructure code should have largely disappeared.</p></li><li><p>Compare the cost of your requests through the Router versus your manual routing from Step 4. The Router&#8217;s Auto Router should achieve similar or better cost optimisation.</p></li></ul><h3>Going Further</h3><p>You&#8217;ve built a support bot and experienced the full arc from simple prototype to production-ready AI application. Here are some ways to push further:</p><ul><li><p><strong>Add a web interface:</strong> Build a simple chat UI in front of your bot instead of using the command line. The backend stays the same - just add an HTTP API layer.</p></li><li><p><strong>Streaming responses:</strong> If you haven&#8217;t already, add streaming so responses appear token by token. This makes a big difference to the user experience, especially for longer answers.</p></li><li><p><strong>Conversation summarisation:</strong> For long support sessions, summarise older messages to keep the context window manageable while preserving important information.</p></li><li><p><strong>Multi-language support:</strong> Add the ability to detect the user&#8217;s language and respond in kind. This is a good test of your routing logic - you might want a different model for different languages.</p></li><li><p><strong>Evaluation harness:</strong> Build a test suite of questions with expected answers and measure your bot&#8217;s accuracy, response time, and cost across different configurations. <strong><a href="https://orq.ai/">Orq.ai</a></strong> can help you with this.</p></li><li><p><strong>RAG integration:</strong> Instead of putting all the context in the system prompt, build a retrieval pipeline that fetches relevant documentation chunks based on the user&#8217;s question. This scales much better as your knowledge base grows. <strong><a href="https://orq.ai/">Orq.ai</a></strong> can help you with this too.</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #116 - Awk]]></title><description><![CDATA[This challenge is to build your own awk.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-116-awk</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-116-awk</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 18 Apr 2026 08:00:51 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0e0bf6f8-97a3-469a-a56e-0679704bc16c_2400x1616.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #116 - Awk</h2><p>This challenge is to build your own version of awk, the classic text processing language.</p><p>Awk was created in 1977 by Alfred Aho, Peter Weinberger, and Brian Kernighan (the name comes from their initials). It&#8217;s a small but remarkably powerful language designed for processing structured text data.</p><p>Awk reads input line by line, splits each line into fields, and applies pattern-action rules to produce output. It sits in a sweet spot between sed (which is great for simple substitutions) and a full programming language like Perl or Python. Despite being nearly 50 years old, awk remains one of the most useful tools in a developer&#8217;s toolkit. You&#8217;ll find it in shell scripts, data pipelines, and one-liners across every Unix system in the world. Building your own awk will teach you about lexing, parsing, interpreters, and the design of small domain-specific languages.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Your Own Awk</h2><p>In this challenge you&#8217;re going to build your own version of the awk text processing tool. Your tool will read input line by line, split each line into fields, match lines against patterns, and execute actions -- producing output that is compatible with the standard POSIX awk utility.</p><p>Awk programs are built from pattern-action rules that look like this: <code>condition { action }</code>. For each line of input, awk checks every rule. If the condition matches, the action is executed. It&#8217;s a simple model that turns out to be surprisingly expressive.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>Choose your target platform and programming language. I&#8217;d encourage you to pick a language you&#8217;re comfortable with for building interpreters. You&#8217;ll be writing a lexer, a parser, and a tree-walking interpreter, so a language with good string handling and data structures will make your life easier.</p><p>Before you start coding, have a read through the <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/awk.html">POSIX awk specification</a> to get a feel for the language. Don&#8217;t worry about understanding every detail -- we&#8217;ll work through the features step by step. It&#8217;s also worth playing with the system <code>awk</code> on your machine to get a sense of how it behaves.</p><p>Create a test file to use throughout the challenge. Here&#8217;s one you can use:</p><pre><code><code>echo "John 25 London
Jane 30 New York
Bob 22 Paris
Alice 35 Tokyo
Charlie 28 Berlin" &gt; test.txt</code></code></pre><h3>Step 1</h3><p>In this step your goal is to support the basic print action with field splitting.</p><p>Your tool should read input from a file (or stdin if no file is given), split each line into fields on whitespace, and support the <code>print</code> statement. The special variable <code>$0</code> refers to the whole line, <code>$1</code> to the first field, <code>$2</code> to the second, and so on.</p><p>At this point, you only need to handle a bare action block with no pattern -- meaning the action runs for every line of input. Focus on getting the core loop right: read a line, split it into fields, execute the action, move to the next line.</p><p><strong>Testing:</strong> Run these commands and compare against the system <code>awk</code>:</p><pre><code><code>ccawk '{ print }' test.txt
ccawk '{ print $0 }' test.txt
ccawk '{ print $1 }' test.txt
ccawk '{ print $1, $3 }' test.txt
echo -e "hello\\nworld" | ccawk '{ print $0 }'</code></code></pre><p>The first two should print every line. The third should print just the first name from each line. The fourth should print the name and city, separated by a space (the default output field separator). The fifth should read from standard input and print each line. Your output should match <code>awk</code> exactly.</p><h3>Step 2</h3><p>In this step your goal is to support the <code>-F</code> flag for custom field separators and the built-in variables <code>NR</code>, <code>NF</code>, and <code>FS</code>.</p><p>The <code>-F</code> flag lets the user specify a custom field separator. For example, <code>-F:</code> splits on colons, which is useful for parsing files like <code>/etc/passwd</code>.</p><p>Implement the built-in variables <code>NR</code> (the current record number, starting at 1), <code>NF</code> (the number of fields in the current record), and <code>FS</code> (the field separator).</p><p><strong>Testing:</strong> Create a CSV-like test file and test with custom separators:</p><pre><code><code>echo "john:25:london
jane:30:new york
bob:22:paris" &gt; test2.txt

ccawk -F: '{ print $1 }' test2.txt
ccawk '{ print NR, $1 }' test.txt
ccawk '{ print NF }' test.txt</code></code></pre><p>The first command should print just the names from the colon-separated file. The second should print line numbers alongside names. The third should print the number of fields on each line. Compare all output against the system <code>awk</code>.</p><h3>Step 3</h3><p>In this step your goal is to support patterns, comparison operators, and regular expression matching.</p><p>Awk&#8217;s power comes from its pattern-action model. A pattern can be a comparison expression (like <code>$2 &gt; 25</code>), a regular expression (like <code>/London/</code>), or the special patterns <code>BEGIN</code> and <code>END</code>. If a line matches the pattern, the action is executed. If there&#8217;s no action, the default is <code>{ print }</code>.</p><p>Implement comparison operators (<code>==</code>, <code>!=</code>, <code>&lt;</code>, <code>&gt;</code>, <code>&lt;=</code>, <code>&gt;=</code>), regular expression matching with <code>/regex/</code> patterns and the <code>~</code> and <code>!~</code> operators, logical operators (<code>&amp;&amp;</code>, <code>||</code>, <code>!</code>), and the <code>BEGIN</code> and <code>END</code> special patterns. <code>BEGIN</code> runs before any input is read, and <code>END</code> runs after all input has been processed. With <code>BEGIN</code> available, you should also support setting <code>FS</code> within the program (e.g. <code>BEGIN { FS = ":" }</code>) as an alternative to the <code>-F</code> flag from Step 2.</p><p>Your program should support multiple pattern-action rules. Awk checks every rule for every line, so a single line can trigger multiple actions.</p><p><strong>Testing:</strong></p><pre><code><code>ccawk '$2 &gt; 25 { print $1 }' test.txt
ccawk '/London/ { print $1 }' test.txt
ccawk '$1 ~ /^[AJ]/ { print }' test.txt
ccawk 'BEGIN { print "Name Age" } { print $1, $2 } END { print "Done" }' test.txt
ccawk 'BEGIN { FS = ":" } { print $1 }' test2.txt
ccawk '$2 &gt; 25 &amp;&amp; $2 &lt; 35 { print $1, "mid-range" }' test.txt
ccawk '/London/ { print "City:", $3 } /^J/ { print "J-name:", $1 }' test.txt</code></code></pre><p>The first should print names of people older than 25. The second should print &#8220;John&#8221;. The third should print lines where the first field starts with A or J. The fourth should print a header, all names with ages, then &#8220;Done&#8221;. The fifth should set the field separator to colon inside <code>BEGIN</code> and print names from the colon-separated file. The sixth should print people whose age is between 25 and 35 exclusive. The seventh demonstrates multiple rules -- John&#8217;s line matches both patterns. Compare against <code>awk</code>.</p><h3>Step 4</h3><p>In this step your goal is to support variables, arithmetic operators, and assignment operators.</p><p>Awk variables are dynamically typed -- they can hold strings or numbers and convert between the two as needed. Uninitialised variables default to <code>0</code> when used as numbers and <code>""</code> when used as strings.</p><p>Implement arithmetic operators (<code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>, <code>%</code>, <code>^</code>), assignment operators (<code>=</code>, <code>+=</code>, <code>-=</code>, <code>*=</code>, <code>/=</code>, <code>%=</code>), and string concatenation (which in awk is done by placing values next to each other with no operator).</p><p><strong>Testing:</strong></p><pre><code><code>ccawk '{ total += $2 } END { print "Total age:", total }' test.txt
ccawk '{ print $1, $2 * 2 }' test.txt
ccawk '{ name = $1 " from " $3; print name }' test.txt
ccawk 'BEGIN { x = 2; print x ^ 10 }'</code></code></pre><p>The first should print the sum of all ages. The second should print names with doubled ages. The third should concatenate fields with text. The fourth should print 1024. Compare against <code>awk</code>.</p><h3>Step 5</h3><p>In this step your goal is to support control flow: <code>if</code>/<code>else</code>, <code>while</code>, <code>for</code>, <code>do-while</code>, and C-style <code>for</code> loops.</p><p>Also implement <code>break</code> and <code>continue</code> for loops, <code>next</code> to skip to the next input record, <code>exit</code> to stop processing entirely, and the ternary conditional operator (<code>condition ? value_if_true : value_if_false</code>).</p><p><strong>Testing:</strong></p><pre><code><code>ccawk '{ if ($2 &gt; 25) print $1, "senior"; else print $1, "junior" }' test.txt
ccawk '{ for (i = 1; i &lt;= NF; i++) print $i }' test.txt
ccawk '$1 == "Bob" { next } { print }' test.txt
ccawk '{ print; if (NR == 3) exit }' test.txt
ccawk '{ print ($2 &gt; 25) ? $1 " is senior" : $1 " is junior" }' test.txt</code></code></pre><p>The first should label people as senior or junior based on age. The second should print every field on its own line. The third should skip Bob&#8217;s line and print everything else. The fourth should print only the first three lines. The fifth uses the ternary operator to produce the same senior/junior labelling in a different style. Compare against <code>awk</code>.</p><h3>Step 6</h3><p>In this step your goal is to support associative arrays and the <code>for (key in array)</code> construct.</p><p>Associative arrays are one of awk&#8217;s most powerful features. They&#8217;re indexed by strings (not just integers) and can be used to count, group, and aggregate data. Implement the <code>in</code> operator for testing membership, <code>for (key in array)</code> for iterating over keys, and the <code>delete</code> statement for removing elements.</p><p><strong>Testing:</strong></p><pre><code><code>ccawk '{ count[$3]++ } END { for (city in count) print city, count[city] }' test.txt
ccawk '{ ages[$1] = $2 } END { if ("Bob" in ages) print "Bob is", ages["Bob"] }' test.txt
ccawk '{ a[$1] = $2 } END { delete a["Bob"]; for (k in a) print k, a[k] }' test.txt</code></code></pre><p>The first should count how many people live in each city. The second should check if Bob exists and print his age. The third should delete Bob and print the rest. Note that <code>for (key in array)</code> iterates in an unspecified order, so don&#8217;t worry about the ordering of output lines -- just make sure the content matches.</p><h3>Step 7</h3><p>In this step your goal is to support the <code>printf</code> statement and built-in string functions.</p><p>Implement <code>printf</code> with C-style format strings (supporting at least <code>%d</code>, <code>%f</code>, <code>%s</code>, <code>%c</code>, and <code>%x</code> with width and precision specifiers).</p><p>Implement these built-in string functions: <code>length</code>, <code>substr</code>, <code>index</code>, <code>split</code>, <code>sub</code>, <code>gsub</code>, <code>match</code>, <code>sprintf</code>, <code>tolower</code>, and <code>toupper</code>.</p><p><strong>Testing:</strong></p><pre><code><code>ccawk '{ printf "%-10s %3d %s\\n", $1, $2, $3 }' test.txt
ccawk '{ print length($1) }' test.txt
ccawk '{ print substr($1, 1, 3) }' test.txt
ccawk '{ gsub(/o/, "0", $1); print }' test.txt
ccawk '{ print toupper($1) }' test.txt</code></code></pre><p>The first should print a neatly formatted table. The second should print the length of each name. The third should print the first three characters of each name. The fourth should replace all &#8220;o&#8221; characters with &#8220;0&#8221; in the first field. The fifth should print names in uppercase. Compare against <code>awk</code>.</p><h3>Step 8</h3><p>In this step your goal is to support user-defined functions and built-in arithmetic functions.</p><p>Implement user-defined functions with the syntax <code>function name(params) { body }</code>. Functions should support local variables (declared as extra parameters in the function signature, which is how awk handles local scope) and return values with <code>return</code>.</p><p>Implement the built-in arithmetic functions: <code>int</code>, <code>sqrt</code>, <code>sin</code>, <code>cos</code>, <code>atan2</code>, <code>exp</code>, <code>log</code>, <code>rand</code>, and <code>srand</code>.</p><p><strong>Testing:</strong></p><pre><code><code>ccawk 'function max(a, b) { return a &gt; b ? a : b } { print $1, max($2, 30) }' test.txt
ccawk 'BEGIN { srand(42); for (i = 0; i &lt; 5; i++) printf "%.4f\\n", rand() }'
ccawk '{ print $1, int(sqrt($2)) }' test.txt</code></code></pre><p>The first should print each name alongside the greater of their age or 30. The second should print 5 random numbers. The third should print names with the integer square root of their age. Compare against <code>awk</code> (except for random numbers where the seed behaviour may differ).</p><h3>Step 9</h3><p>In this step your goal is to support the remaining output and input features.</p><p>Implement the output built-in variables: <code>OFS</code> (output field separator), <code>ORS</code> (output record separator), and <code>RS</code> (record separator). When <code>print</code> outputs multiple fields separated by commas, it uses <code>OFS</code> between them. Each <code>print</code> statement ends with <code>ORS</code>.</p><p>Implement the <code>-f</code> flag to read the awk program from a file instead of the command line, <code>-v var=value</code> for setting variables before execution, and support for reading from multiple input files. Implement the <code>FILENAME</code>, <code>ARGC</code>, and <code>ARGV</code> built-in variables.</p><p><strong>Testing:</strong></p><pre><code><code>ccawk 'BEGIN { OFS="-" } { print $1, $2, $3 }' test.txt
echo 'BEGIN { print "Start" } { print FILENAME, $0 }' &gt; prog.awk
ccawk -f prog.awk test.txt
ccawk -v threshold=25 '$2 &gt; threshold { print $1 }' test.txt
ccawk '{ print FILENAME, $0 }' test.txt test2.txt</code></code></pre><p>The first should print fields separated by dashes. The second should read the program from a file and print each line with its filename. The third should use the command-line variable. The fourth should process both files and show which file each line came from. Compare against <code>awk</code>.</p><h3>Step 10</h3><p>In this step your goal is to support piping output to shell commands and the <code>getline</code> function.</p><p>Implement the pipe operator for <code>print</code>, which lets you send output to an external command: <code>print "hello" | "sort"</code>. Awk keeps the pipe open across multiple print statements to the same command, so all the output goes to a single invocation of the command. Implement the <code>close()</code> function to close a pipe or file, which is needed when you want to reopen a pipe or when the external command needs to receive EOF to produce output.</p><p>Implement <code>getline</code> in its various forms: <code>getline</code> to read the next line from the current input, <code>getline var</code> to read into a specific variable, <code>getline &lt; "file"</code> to read from a file, and <code>"command" | getline</code> to read from a command.</p><p><strong>Testing:</strong></p><pre><code><code>ccawk '{ print $1 | "sort" }' test.txt
ccawk '{ while (("date" | getline line) &gt; 0) print line; close("date") }'
ccawk 'BEGIN { while ((getline line &lt; "test.txt") &gt; 0) print line }'</code></code></pre><p>The first should print names in sorted order. The second should print the current date. The third should read and print the test file from within a <code>BEGIN</code> block. The fourth is a classic awk one-liner that sums file sizes from <code>ls</code> output. Compare against <code>awk</code>.</p><pre><code><code>ls -l | ccawk 'NR &gt; 1 { total += $5 } END { print "Total bytes:", total }'</code></code></pre><h3>Going Further</h3><p>Here are some ideas to take your awk implementation further:</p><ul><li><p>Add support for multi-character record separators (which <code>gawk</code> supports but POSIX awk does not)</p></li><li><p>Implement the <code>OFMT</code> and <code>CONVFMT</code> variables for controlling numeric-to-string conversion</p></li><li><p>Add support for range patterns (<code>/start/,/stop/</code>) which match all lines between two patterns</p></li><li><p>Implement coprocess communication with <code>|&amp;</code> (a gawk extension)</p></li><li><p>Add support for <code>@include</code> to include other awk source files</p></li><li><p>Build a bytecode compiler and virtual machine instead of a tree-walking interpreter for better performance</p></li><li><p>Add support for the <code>ENVIRON</code> array for accessing environment variables</p></li><li><p>Implement <code>nextfile</code> to skip to the next input file</p></li><li><p>Try running your awk against real-world awk scripts (there are many collected online) and see how compatible your implementation is</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #115 - Code Sherpa]]></title><description><![CDATA[This challenge is to build your own AI Codebase assistant.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-115-code-sherpa</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-115-code-sherpa</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 11 Apr 2026 08:01:23 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/9757c332-d777-412b-8b9d-d0b1f8893558_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #115 - Code Sherpa</h2><p>This challenge is to build your own semantic code exploration tool - a system that helps developers make sense of large, unfamiliar codebases using natural language questions instead of reading files top to bottom.</p><p>We&#8217;ve all been there. You join a new team, or pick up a legacy project, and you&#8217;re staring at thousands of files with no idea where the interesting bits live. You grep for keywords, open file after file, and slowly piece together how things fit. It works, but it&#8217;s slow and frustrating.</p><p>Code Sherpa takes a different approach. You point it at a codebase and ask questions in plain English: &#8220;where do we handle payment failures?&#8221; or &#8220;how does the authentication flow work?&#8221; The system finds the relevant code by meaning, not by keyword matching, explains what it does, and remembers what you&#8217;ve already explored so each session builds on the last.</p><p>Under the hood, the system parses code into semantic chunks, embeds them into vectors, and stores them in a vector database such as Oracle Database 26ai. When you ask a question, it retrieves the most relevant code using both vector similarity and full-text search, and uses an LLM to explain it in context. The agentic behaviour - memory, follow-up questions, multi-step exploration - is orchestrated using LangGraph&#8217;s state machine model, while LangChain handles the retrieval chains and LLM integration. It&#8217;s a practical introduction to vector search, full-text search, embeddings, code parsing, agent orchestration, and building a web interface to tie it all together.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Code Sherpa</h2><p>You&#8217;re going to build a semantic code exploration tool. It starts by ingesting a codebase into a vector database, then lets you query it through a web interface using natural language. Step by step you&#8217;ll add code parsing, vector storage, semantic retrieval, LLM-powered explanations, project management, a web interface, agent memory, and intelligent navigation. By the end, you&#8217;ll have a tool that genuinely helps you understand unfamiliar code.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>You&#8217;ll need to make a few decisions and get some infrastructure running:</p><ol><li><p><strong>Set up your vector database.</strong> I suggest <a href="https://fandf.co/4e20faq">Oracle Database 26ai running in a local Docker container</a>. Pull the <code>container-registry.oracle.com/database/free:latest</code> image, start the container, and set a password for the admin account. You can find full setup instructions in the <a href="https://fandf.co/4m6JYmu">Oracle Database Free Get Started guide</a>. Once the container is running, connect using a SQL client and verify you can create a table. Store all credentials in an environment file, not hardcoded anywhere.</p></li></ol><pre><code><code>docker pull container-registry.oracle.com/database/free:latest
docker run -d -p 1521:1521 -e ORACLE_PWD=&lt;your-password&gt; container-registry.oracle.com/database/free:latest
</code></code></pre><ol><li><p><strong>Choose your embedding model.</strong> You need a code-aware embedding model - one that understands programming constructs, not just prose. Nomic&#8217;s <code>CodeRankEmbed</code> is open source (MIT), purpose-built for code retrieval, and lightweight enough to run locally on CPU without a GPU or API key. It produces 768-dimensional vectors. Install it via Hugging Face: <code>pip install sentence-transformers</code> and load it as <code>nomic-ai/CodeRankEmbed</code>. If you have a GPU available, Nomic&#8217;s larger <code>nomic-embed-code</code> (7B parameters, requires ~14GB VRAM) offers a few points better retrieval quality.</p></li><li><p><strong>Set up your LLM provider.</strong> You&#8217;ll need a language model for generating explanations later. Any provider with a chat API will work - Anthropic, OpenAI, Google, Mistral, or a local model.</p></li><li><p><strong>Set up LangChain and LangGraph.</strong> You&#8217;ll be using LangChain for retrieval chains and LLM integration, and LangGraph for orchestrating the agent&#8217;s behaviour as a state machine. Install both: <code>pip install langchain langgraph</code>. LangChain handles the plumbing of embedding, retrieval, and prompting. LangGraph handles the agentic flow - deciding when to search memory, when to retrieve code, when to ask follow-up questions, and how to route between these steps.</p></li></ol><p>Prepare a test codebase to work with throughout the challenge. Pick an open source project you&#8217;re curious about but haven&#8217;t explored in depth - something with a few thousand lines across multiple files and directories. A project with clear structure (like a web framework, CLI tool, or library) works well.</p><p><strong>Testing:</strong> Verify your Oracle Database container is running and you can connect to it. Load your embedding model and generate a test embedding to confirm it returns a vector of the expected dimensions. Make a test call to your LLM API to confirm it returns a valid response. Verify your environment file is being read correctly and no credentials are in your source code.</p><h3>Step 1</h3><p>In this step your goal is to build a code parsing pipeline that breaks a codebase into meaningful chunks.</p><p>The foundation of semantic code search is good chunking. Rather than splitting files at arbitrary character boundaries, you want to split at logical boundaries: functions, classes, and modules. A chunk should represent one coherent unit of code that makes sense on its own.</p><p>Point your parser at a local directory and have it walk the file tree, identify source files, and split each one into chunks. Each chunk should carry metadata: the file path it came from, what type of chunk it is (function, class, module), the programming language, and the character range within the original file.</p><p>Not every file will parse cleanly. Some might have syntax errors, use unusual language features, or be in a format your parser doesn&#8217;t support. When that happens, log the failure and keep going. A partial index is far more useful than no index at all.</p><p>Display progress in the terminal as ingestion runs: how many files have been processed, how many chunks have been created, and any failures encountered.</p><p><strong>Testing:</strong></p><ul><li><p>Run your parser against your test codebase and verify it produces chunks at function and class boundaries, not arbitrary splits.</p></li><li><p>Inspect several chunks and confirm each one contains a complete, coherent unit of code.</p></li><li><p>Check that the metadata on each chunk is correct - file path, chunk type, language, and character range should all match the source.</p></li><li><p>Introduce a file with a deliberate syntax error and verify the parser logs the failure and continues processing the rest.</p></li><li><p>Verify the terminal output shows meaningful progress: files processed, chunks created, and any errors.</p></li></ul><h3>Step 2</h3><p>In this step your goal is to embed the code chunks and store them in your vector database.</p><p>Take each chunk from your parser, generate a vector embedding for it using your chosen embedding model, and store the embedding alongside the chunk&#8217;s text and metadata in Oracle Database. The metadata fields - file path, chunk type, language, and character range - should all be stored and indexed so you can filter on them later.</p><p>Think about how you structure your storage. You&#8217;ll want to be able to search by vector similarity, but also filter by metadata (e.g. &#8220;only show me Python files&#8221; or &#8220;only functions, not classes&#8221;). Set up your vector indexes accordingly. Also create an Oracle Text full-text index on the code text column - this will let you fall back to keyword search when vector similarity alone doesn&#8217;t find good matches, and is particularly useful when developers search for exact identifier names, error messages, or string literals.</p><p><strong>Testing:</strong></p><ul><li><p>Run the full pipeline - parse then embed and store - against your test codebase.</p></li><li><p>Query Oracle Database directly to verify the data is there: check the total number of stored chunks matches what your parser reported.</p></li><li><p>Inspect a few stored entries and confirm they contain the embedding vector, the original code text, and all metadata fields.</p></li><li><p>Verify that both the vector index and the full-text index have been created on the appropriate columns.</p></li><li><p>Run the pipeline again against the same codebase and verify it handles the re-run sensibly (either updating existing entries or skipping duplicates).</p></li></ul><h3>Step 3</h3><p>In this step your goal is to implement semantic retrieval so you can ask natural language questions and get back the most relevant code.</p><p>This is where the tool starts to feel useful. Take a natural language question from the user, embed it using the same model you used for the code chunks, and search your vector database for the closest matches using cosine similarity. Also run the query through Oracle&#8217;s full-text search on the same table. Combine the results - vector search finds semantically related code even when the words don&#8217;t match, while full-text search catches exact identifier names and string literals that vector search might rank lower. Return the top results along with their file paths and line references.</p><p>Not every query will have good matches. Set a minimum cosine similarity threshold - start at around 0.3 for <code>CodeRankEmbed</code> embeddings, which is a reasonable baseline for filtering out unrelated results. You may need to adjust this based on your embedding model: if you&#8217;re getting too many irrelevant results, raise it; if you&#8217;re missing relevant code, lower it. When nothing exceeds the threshold and full-text search also returns no matches, the system should tell the user honestly rather than returning low-confidence results that waste their time.</p><p>Build this as a simple CLI interface for now - you&#8217;ll add the web interface later. The user types a question, and the system returns the matching code chunks with their locations. This is database search only so far.</p><p><strong>Testing:</strong></p><ul><li><p>Ask a question about something you know exists in your test codebase (e.g. &#8220;where is the main entry point?&#8221; or &#8220;how are errors handled?&#8221;). Verify the returned chunks are genuinely relevant.</p></li><li><p>Ask the same question using different phrasing and verify you get similar results. This is the whole point of semantic search - it matches by meaning, not keywords.</p></li><li><p>Search for an exact function or variable name. Verify the full-text search catches it even if the vector similarity score would be low.</p></li><li><p>Ask a question about something that definitely isn&#8217;t in the codebase. Verify the system tells you no relevant code was found rather than returning irrelevant results.</p></li><li><p>Check that every returned chunk includes its file path and line reference.</p></li></ul><h3>Step 4</h3><p>In this step your goal is to add LLM-powered explanations so the system doesn&#8217;t just find code - it explains what the code does.</p><p>Raw code chunks are useful, but an explanation in plain language is far more helpful when you&#8217;re trying to understand an unfamiliar codebase. Wire up your LLM through LangChain to take the retrieved chunks and generate a clear explanation. Use LangChain&#8217;s retrieval chain to handle the prompt construction - passing the retrieved code as context alongside the user&#8217;s question.</p><p>The explanation should cite the specific functions and files involved. When the user asks how two parts of the codebase relate to each other, the system should retrieve both and explain the connection. Where multiple implementations of the same concept exist, it should surface all of them and explain the differences.</p><p>Rather than hardcoding the retrieval orchestration, bind tool definitions to the LLM so it can invoke tools during response generation. Define three tools: <code>search_code(query)</code> for hybrid vector and full-text search, <code>read_file(file_path)</code> for full file contents, and <code>list_files(pattern)</code> for glob-based file listing. The agent loop sends the user&#8217;s question, executes any tool calls the LLM requests, returns the results, and repeats until the LLM produces a final text response or hits a configurable iteration limit (default 10). Each tool invocation should be emitted as a progress event so the frontend can show what the agent is doing.</p><p>Crucially, the system should not speculate beyond what the retrieved code supports. If a question can&#8217;t be fully answered from what&#8217;s been ingested, it should say so explicitly rather than making things up.</p><p><strong>Testing:</strong></p><ul><li><p>Ask what a specific function does. The explanation should be accurate, in plain language, and cite the file and function name.</p></li><li><p>Ask how two parts of the codebase relate (e.g. &#8220;how does the router connect to the request handlers?&#8221;). The system should retrieve relevant code from both areas and explain the relationship.</p></li><li><p>Verify the agent uses tool calls to gather information iteratively - it should invoke <code>search_code</code>, <code>read_file</code>, or <code>list_files</code> as needed and you should see progress events for each tool invocation.</p></li><li><p>Ask a question that requires multiple tool calls to answer fully (e.g. &#8220;trace the request lifecycle from entry point to response&#8221;). Verify the agent makes several tool calls before producing a final answer.</p></li><li><p>Ask a question that the codebase only partially answers. Verify the system explains what it can and explicitly flags what it can&#8217;t determine from the code.</p></li><li><p>Ask about a concept that has multiple implementations in the codebase. Verify the system surfaces all of them and explains how they differ.</p></li></ul><h3>Step 5</h3><p>In this step your goal is to add project management so a developer can maintain separate knowledge bases for different codebases.</p><p>A developer working across multiple projects needs each one indexed and searchable independently. Add support for named projects. Store project metadata - name, source path, creation date, last ingestion timestamp, file count, and chunk count - in Oracle Database alongside your embeddings. Each project should store its embeddings, metadata, and any agent memory in isolation, so queries against one project never return results from another.</p><p>The user should be able to create a new project, list existing projects, and select which project to query. When a codebase is re-ingested into an existing project, only the changed files should be re-embedded - unchanged files should keep their existing embeddings. This makes re-ingestion fast even for large projects.</p><p>All project data should persist between runs in Oracle Database. When the user comes back tomorrow and selects a project, everything should be exactly as they left it.</p><p><strong>Testing:</strong></p><ul><li><p>Create two projects from two different codebases. Query each one and verify the results come only from the correct project.</p></li><li><p>List your projects and verify both appear with the correct names and metadata (source path, file count, last ingestion time).</p></li><li><p>Query the project metadata directly in Oracle Database and verify it matches what the system reports.</p></li><li><p>Modify a single file in one of your test codebases, re-ingest, and verify that only the changed file&#8217;s chunks are re-embedded. Unchanged files should not be re-processed.</p></li><li><p>Stop and restart your system. Verify all project data is still intact and queryable.</p></li></ul><h3>Step 6</h3><p>In this step your goal is to build a web interface for browsing and querying your indexed codebases.</p><p>The web interface should launch automatically when the system starts, opening in the default browser or displaying the local URL clearly in the terminal. Build it as a three-panel layout: a file tree on the left, a chat panel in the centre, and a code viewer on the right.</p><p>The chat panel should present a chat-style conversation interface. User and assistant messages appear in a scrollable history as distinct message bubbles. When follow-up mode is enabled (the default), conversation history is sent with each request so the LLM can reference prior exchanges. A &#8220;New Chat&#8221; button resets the conversation. Assistant responses should render markdown and include source citations with expandable code snippet previews. Clicking a file reference in a citation or the file tree opens the code in the viewer panel alongside the chat.</p><p>The web interface should also include a dedicated Projects page, separate from the code exploration view, for managing projects. From this page, users can create new projects by specifying a name and source (local path or GitHub URL), trigger ingestion or re-ingestion, and monitor progress in real time. Progress updates should stream via Server-Sent Events (SSE), showing the current phase and batch progress (e.g. &#8220;Embedding chunks: batch 3 of 10&#8221;). On completion, display a summary of chunks stored and files skipped, updated, or removed. Concurrent ingestion on the same project should be prevented.</p><p>Add a browsable file tree of the ingested codebase structure, built from the stored metadata. This gives the user a visual overview of the project layout without needing to look at the actual file system.</p><p>While the agent is processing a query, show a loading state so the user knows something is happening.</p><p><strong>Testing:</strong></p><ul><li><p>Start the system and verify the web interface launches and is accessible in your browser.</p></li><li><p>Select a project and ask a question through the chat panel. Verify the response appears as a message bubble with markdown rendering, source citations, and expandable code previews.</p></li><li><p>Ask a follow-up question and verify the system uses conversation history to maintain context.</p></li><li><p>Click &#8220;New Chat&#8221; and verify the conversation resets.</p></li><li><p>Click a file reference in a citation and verify it opens in the code viewer panel.</p></li><li><p>Switch between projects and verify the results update to reflect the selected project.</p></li><li><p>Browse the file tree and verify it accurately reflects the structure of the ingested codebase.</p></li><li><p>Create a new project from the Projects page by providing a name and source path. Trigger ingestion and verify progress streams in real time, showing the current phase and batch progress.</p></li><li><p>Attempt to start a second ingestion on the same project while one is running. Verify it is prevented.</p></li><li><p>Submit a query and verify a loading indicator appears while the response is being generated.</p></li></ul><h3>Step 7</h3><p>In this step your goal is to add agent memory so the system remembers what&#8217;s been explored and what the developer has told it about the project.</p><p>Without memory, every session starts from scratch. The developer re-explains the same context, re-asks the same orientation questions, and the system re-explains things it&#8217;s already covered. Memory changes that.</p><p>Implement two types of memory, stored in Oracle Database alongside your code embeddings. Episodic memory tracks which areas of the codebase the developer has already explored, so the system can avoid re-explaining concepts that have already been covered. Semantic memory stores project-level context that the developer provides - things like &#8220;this service owns all payment logic&#8221; or &#8220;the legacy auth module is being deprecated&#8221; - and applies it to future responses within that project.</p><p>Use LangGraph to build a memory-aware query graph. When a question arrives, the graph should first check memory for relevant prior context, then decide how to handle the query: if the user has already explored this area, route to a node that builds on prior understanding rather than explaining from scratch; if it&#8217;s new territory, route to a full retrieval and explanation. This routing logic is where LangGraph&#8217;s state machine model pays off - each node in the graph handles one concern (check memory, retrieve code, generate explanation, update memory) and the edges encode the decision logic.</p><p>Since you already have Oracle Database storing your code embeddings, it&#8217;s a natural home for memory too. Store memory entries as vectors so they can be retrieved by semantic similarity - when a developer asks a question, the system can search its memory for relevant prior context the same way it searches the codebase for relevant code. Keep memory isolated per project, just like your code embeddings.</p><p>Both types of memory should persist across sessions in Oracle Database. When the user returns to a project, the system should pick up where it left off. When asked, the system should be able to provide a summary of what&#8217;s been explored so far and what remains unvisited.</p><p>Provide a dedicated Memory page in the web interface, accessible from the main navigation alongside the Explorer and Projects pages. The page should show two sections: episodic memory (exploration history with queries, files explored, summaries, and timestamps) and semantic memory (developer-provided context with content and timestamps). Users can search across both memory types using a unified search that combines text matching and vector similarity. Semantic memory entries can be added, edited, and deleted individually. Both memory types support individual deletion and bulk clear operations with confirmation dialogs.</p><p><strong>Testing:</strong></p><ul><li><p>Explore several areas of a codebase across a session. End the session, start a new one, and ask the system what you&#8217;ve explored so far. It should accurately summarise the areas you&#8217;ve already covered.</p></li><li><p>Tell the system something about the project (e.g. &#8220;the payments module is the most critical part of this service&#8221;). In subsequent queries, verify the system uses this context to inform its responses.</p></li><li><p>Ask about something you&#8217;ve already explored. The system should recognise this and build on prior understanding rather than explaining from scratch.</p></li><li><p>Ask for a summary of what&#8217;s been explored versus what remains unvisited. Verify it gives a reasonable breakdown.</p></li><li><p>Open the Memory page and verify it shows episodic and semantic memory entries with timestamps.</p></li><li><p>Add a new semantic memory entry from the Memory page. Return to the Explorer and verify the system uses it in subsequent responses.</p></li><li><p>Edit and delete individual memory entries. Verify changes take effect immediately.</p></li><li><p>Use the search function on the Memory page and verify it finds entries by both keyword and meaning.</p></li><li><p>Use the bulk clear operation on episodic memory and verify all entries are removed after confirmation.</p></li></ul><h3>Step 8</h3><p>In this step your goal is to add intelligent navigation so developers can drill deeper into code and follow connections naturally.</p><p>A good exploration tool doesn&#8217;t just answer isolated questions - it helps you follow threads. Start by classifying query intent using a single LLM call rather than regex patterns. The classifier should determine whether a query is a map request, a follow-up to a previous exchange, a broad exploration, or a specific question. Include recent conversation history in the classification prompt so the LLM can detect follow-ups contextually rather than relying on keyword matching.</p><p>Use LangGraph to model multi-step retrieval as a state graph. When a user asks &#8220;what calls this?&#8221;, the graph should: retrieve the current function&#8217;s code, identify references to it across the codebase, retrieve those callers, and generate an explanation that ties them together. Each step is a node in the graph, with state passed between them.</p><p>Add support for follow-up questions that drill deeper into a previous result without the user needing to re-state context. If the system just explained a function, the user should be able to ask &#8220;what calls this?&#8221; or &#8220;where is the return value used?&#8221; and get a meaningful answer. LangGraph&#8217;s state carries the conversation context forward, so the agent knows which function is being discussed.</p><p>Dependency extraction should be language-aware. Use the <code>language</code> field stored on each code chunk to select appropriate import and require patterns for the chunk&#8217;s language. Support at minimum Python (<code>import X</code>, <code>from X import Y</code>), JavaScript/TypeScript (<code>import ... from</code>, <code>require()</code>, <code>import()</code>), Go (<code>import "pkg"</code>, multi-line <code>import (...)</code>), and Java (<code>import com.example.Foo</code>) import styles, plus class inheritance patterns for each language. For unsupported languages, fall back to a generic regex that detects common import keywords.</p><p>When the system identifies a dependency or reference in retrieved code, it should offer to retrieve and explain the linked code. In the web interface, render these as clickable elements that trigger a follow-up retrieval.</p><p>Add an exploration planning capability using LangGraph. When a user asks a broad question like &#8220;how does the authentication system work?&#8221;, the agent should plan a multi-step exploration: find entry points, trace the authentication flow through the codebase, retrieve each step, and produce a coherent walkthrough. Model this as a graph where each retrieval step feeds into the next, building up a complete picture rather than returning a single set of search results.</p><p>Finally, add a &#8220;map&#8221; query that returns a high-level summary of the codebase structure: a breakdown of languages used, top-level modules, and entry points where identifiable. The map query should identify key files that signal project structure: build configuration (pyproject.toml, package.json, Cargo.toml, go.mod, pom.xml, build.gradle, Makefile, CMakeLists.txt), container definitions (Dockerfile, docker-compose.yml), documentation (readme, changelog, contributing, license), and entry points (main.<em>, app.</em>, index.<em>, server.</em>, cli.*). This gives the developer a bird&#8217;s-eye view before diving into specifics.</p><p><strong>Testing:</strong></p><ul><li><p>Ask about a function, then ask a follow-up like &#8220;what calls this?&#8221; without re-stating which function you mean. The system should understand from context and return relevant results.</p></li><li><p>Look for linked dependencies in a response. If the system identifies an import or function call, verify it offers to explain the linked code.</p></li><li><p>In the web interface, click a dependency link and verify it triggers a follow-up retrieval and explanation.</p></li><li><p>Verify that query classification correctly identifies a map request, a follow-up, a broad exploration, and a specific question.</p></li><li><p>Run the &#8220;map&#8221; query and verify you get a useful high-level summary: languages, modules, key files (build config, Dockerfiles, documentation), and entry points should all be represented.</p></li><li><p>Verify dependency extraction works across languages: test with Python imports, JavaScript/TypeScript requires, Go imports, and Java imports if your test codebase includes them.</p></li></ul><h3>Going Further</h3><p>You&#8217;ve built a working semantic code exploration tool. Here are some ways to take it further:</p><ul><li><p><strong>Cloud database support:</strong> Add an option to connect to a <a href="https://fandf.co/4cq3Cqk">cloud-hosted Oracle Database</a> instance instead of the local Docker container. Read the connection string from configuration so teams can share a single index.</p></li><li><p><strong>Multi-language parsing:</strong> Extend your parser to handle a wider range of programming languages. Tree-sitter is a good foundation for this - it provides consistent parsing across dozens of languages.</p></li><li><p><strong>Collaboration features:</strong> Allow multiple developers to share a project index and see each other&#8217;s exploration history. This is particularly useful during onboarding, where a senior engineer&#8217;s exploration trail becomes a guided tour for newcomers. Think about how to share the insights they provide and create a shared memory.</p></li><li><p><strong>IDE integration:</strong> Build an extension for VS Code or another editor that lets developers query Code Sherpa directly from their IDE, with results that link back to the exact line in the editor.</p></li><li><p><strong>Export and share:</strong> Let users export their exploration session - the questions asked, the code found, and the explanations generated - as a shareable document. This turns an exploration session into reusable documentation.</p></li></ul><p>This coding challenge was sponsored by Oracle.</p><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #114 - Gzip]]></title><description><![CDATA[This challenge is to build your own gzip.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-114-gzip</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-114-gzip</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 04 Apr 2026 08:00:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/5914f1c3-3233-45d7-aa56-c22b0dcde86c_2400x1350.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #114 - Gzip</h2><p>This challenge is to build your own version of gzip, the widely used file compression utility.</p><p>Gzip has been a cornerstone of computing since 1992. It&#8217;s used everywhere, compressing files on the command line, serving web content over HTTP, packaging up tarballs for distribution, and much more. Under the hood, gzip uses the DEFLATE compression algorithm (a combination of LZ77 and Huffman coding) wrapped in a simple file format defined by RFC 1952. Building your own gzip will give you a deep understanding of how data compression works, how file formats are structured, and how command-line tools handle the many options users expect.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get access to a monthly AMA and 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Gzip</h2><p>In this challenge you&#8217;re going to build your own version of the gzip compression tool. Your tool will compress and decompress files using the DEFLATE algorithm, producing output that is fully compatible with the standard <code>gzip</code> and <code>gunzip</code> utilities.</p><p>You can use a library for the DEFLATE algorithm if you want, but I&#8217;d really encourage you to implement it yourself by reading the spec. Data compression is one of those topics that sounds intimidating but becomes surprisingly approachable once you break it down. You&#8217;ll learn far more about how compression actually works by building LZ77 and Huffman coding from scratch than by calling a library function. The RFCs are well written and this challenge is structured to walk you through it incrementally.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>Choose your target platform and programming language. I&#8217;d encourage you to pick a language that gives you access to low-level byte manipulation, as you&#8217;ll be working with binary data, checksums, and bit-level operations.</p><p>Before you start coding, read through <a href="https://datatracker.ietf.org/doc/html/rfc1952">RFC 1952</a> (the gzip file format) and <a href="https://datatracker.ietf.org/doc/html/rfc1951">RFC 1951</a> (the DEFLATE compressed data format). Don&#8217;t worry about understanding every detail right now, we&#8217;ll work through the key parts step by step. The important thing is to get a feel for how the pieces fit together: gzip is a wrapper format, and DEFLATE is the compression algorithm inside it.</p><h3>Step 1</h3><p>In this step your goal is to produce a valid <code>.gz</code> file using DEFLATE stored (uncompressed) blocks.</p><p>Before you tackle actual compression, get the gzip file format right first. Your tool should take a filename as an argument, wrap its contents in a valid <code>.gz</code> file with the correct header and trailer, and write it out with the <code>.gz</code> extension appended. After writing the compressed file, remove the original (this is the default gzip behaviour).</p><p>The gzip format (RFC 1952) requires a 10-byte header containing the magic number (<code>1f 8b</code>), the compression method (<code>08</code> for DEFLATE), and flags. It also requires a trailer containing the CRC-32 checksum of the original data and the original file size (modulo 2^32).</p><p>For the DEFLATE payload, use stored blocks (block type <code>00</code> in RFC 1951, section 3.2.4). A stored block simply contains the raw data with a small header &#8212; no actual compression. This lets you get the gzip wrapper, CRC-32 calculation, and file handling working correctly before you add real compression. Your output will be valid gzip, just larger than the input.</p><p><strong>Testing:</strong> Create a test file and compress it with your tool, then decompress it with the system <code>gunzip</code> to verify your format is correct:</p><pre><code><code>echo "Hello, World!" &gt; test.txt
ccgzip test.txt
gunzip test.txt.gz
cat test.txt
</code></code></pre><p>You should see <code>Hello, World!</code> and the file should be identical to the original. The <code>.gz</code> file will be slightly larger than the original since you&#8217;re not compressing yet &#8212; that&#8217;s fine, the important thing is that <code>gunzip</code> accepts it.</p><h3>Step 2</h3><p>In this step your goal is to implement LZ77, the first half of the DEFLATE algorithm.</p><p>LZ77 works by sliding a window over the input data and looking for sequences that have already appeared. When it finds a match, instead of storing the bytes again, it stores a (length, distance) pair &#8212; &#8220;copy 5 bytes from 12 positions back&#8221;. This is how DEFLATE eliminates repeated patterns.</p><p>Implement a sliding window (up to 32,768 bytes as per the spec) and a match-finding algorithm. For each position in the input, search the window for the longest match. If you find a match of 3 bytes or more, emit a (length, distance) pair. Otherwise, emit the literal byte.</p><p>At this point, encode your LZ77 output using DEFLATE fixed Huffman codes (RFC 1951, section 3.2.6). Fixed codes use a predefined Huffman table built into the spec, so you don&#8217;t need to build your own trees yet &#8212; you just need to emit the right bit sequences for literals, lengths, and distances.</p><p><strong>Testing:</strong> Compress a file with your tool and decompress with <code>gunzip</code>:</p><pre><code><code>echo "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc" &gt; test.txt
ccgzip test.txt
gunzip test.txt.gz
cat test.txt
</code></code></pre><p>The <code>.gz</code> file should now be smaller than the original for repetitive data. Compare the compressed size to what the system <code>gzip</code> produces &#8212; yours won&#8217;t be as good yet (we&#8217;ll get there), but it should be noticeably smaller than the stored blocks from Step 1. Try it on a larger text file to see a more dramatic difference.</p><h3>Step 3</h3><p>In this step your goal is to implement Huffman coding, the second half of DEFLATE.</p><p>Fixed Huffman codes work, but they&#8217;re not optimal for every input. Dynamic Huffman codes (RFC 1951, section 3.2.7) let you build custom Huffman trees tailored to the actual frequency of symbols in your data, which gives much better compression.</p><p>Build a Huffman tree from the frequencies of the literal/length and distance symbols in your LZ77 output. Encode the tree itself into the DEFLATE block header (the spec describes exactly how to do this using code length codes), then encode the data using your custom tree.</p><p>This is the trickiest part of the challenge, take it slow and test frequently. The encoding of the Huffman tree in the block header is fiddly, with its own mini-Huffman encoding for the code lengths. The RFC walks through it methodically; follow it closely.</p><p><strong>Testing:</strong> Compress files with your tool and compare the sizes to the system <code>gzip</code>:</p><pre><code><code>echo "abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc" &gt; test.txt
cp test.txt test-original.txt
ccgzip test.txt
ls -la test.txt.gz
gunzip test.txt.gz
diff test.txt test-original.txt
</code></code></pre><p>Rename one of them to compare both. Your compressed output should now be close to what the system <code>gzip</code> produces. Test with a variety of files &#8212; text, source code, binary &#8212; and decompress each with <code>gunzip</code> to make sure everything round-trips correctly.</p><h3>Step 4</h3><p>In this step your goal is to decompress a <code>.gz</code> file using the <code>-d</code> flag.</p><p>When your tool is called with <code>-d</code>, it should read a <code>.gz</code> file, validate the gzip header, decompress the DEFLATE data, and write the original content to a new file with the <code>.gz</code> extension removed. After decompression, the <code>.gz</code> file should be removed.</p><p>You&#8217;ll need to reverse everything you built in the previous steps: parse the DEFLATE block headers, reconstruct the Huffman trees (for dynamic blocks), decode the symbols, and replay the LZ77 back-references to reconstruct the original data.</p><p>Your tool should validate the CRC-32 checksum and original file size stored in the gzip trailer. If the checksum doesn&#8217;t match the decompressed data, your tool should report an error and exit with a non-zero status code &#8212; this is how gzip detects corrupt files.</p><p><strong>Testing:</strong> Compress a file with the system <code>gzip</code>, then decompress it with your tool:</p><pre><code><code>echo "Testing decompression" &gt; test.txt
gzip test.txt
ccgzip -d test.txt.gz
cat test.txt
</code></code></pre><p>You should see <code>Testing decompression</code>. Also test with a corrupted file &#8212; use a hex editor to change a byte in a <code>.gz</code> file and verify your tool reports a CRC error.</p><h3>Step 5</h3><p>In this step your goal is to support reading from standard input and writing to standard output, along with the <code>-c</code>, <code>-k</code>, and <code>-f</code> flags.</p><p>When no filename is provided, your tool should read data from stdin, compress it, and write the compressed output to stdout. When decompressing, it should read compressed data from stdin and write the decompressed output to stdout.</p><p>Implement the <code>-c</code> flag, which writes compressed or decompressed output to stdout instead of to a file, leaving the original file unchanged. Implement the <code>-k</code> flag, which keeps (does not delete) the original file after compression or decompression. And implement the <code>-f</code> flag, which forces overwriting of existing output files without prompting.</p><p><strong>Testing:</strong> Test piping data through your tool:</p><pre><code><code>echo "Piped data" | ccgzip | gunzip
echo "Piped data" | gzip | ccgzip -d
</code></code></pre><p>Both should output <code>Piped data</code>. Test <code>-k</code> and <code>-f</code>:</p><pre><code><code>echo "Keep me" &gt; test.txt
ccgzip -k test.txt
ls test.txt test.txt.gz
ccgzip -f test.txt
</code></code></pre><p>The <code>-k</code> flag should leave both files in place. The second compression should succeed because <code>-f</code> forces the overwrite of the existing <code>.gz</code> file.</p><h3>Step 6</h3><p>In this step your goal is to support compressing and decompressing multiple files and to implement compression levels.</p><p>Your tool should accept multiple filenames as arguments and process them one at a time. If any file fails, your tool should report the error and continue processing the remaining files.</p><p>Implement compression levels from <code>-1</code> (fastest, least compression) to <code>-9</code> (slowest, best compression), with <code>-6</code> as the default. These levels control how aggressively the LZ77 stage searches for matches &#8212; at level 1, you might limit the search to short look-aheads and a smaller window, whilst at level 9, you search more thoroughly for the longest possible matches.</p><p><strong>Testing:</strong> Test multiple files:</p><pre><code><code>echo "File one" &gt; a.txt
echo "File two" &gt; b.txt
echo "File three" &gt; c.txt
ccgzip a.txt b.txt c.txt
ls *.gz
</code></code></pre><p>You should see <code>a.txt.gz</code>, <code>b.txt.gz</code>, and <code>c.txt.gz</code>. Test compression levels on a larger file:</p><pre><code><code>ccgzip -1 -k test.txt &amp;&amp; mv test.txt.gz test-fast.gz
ccgzip -9 -k test.txt &amp;&amp; mv test.txt.gz test-best.gz
ls -la test-fast.gz test-best.gz
</code></code></pre><p>For a sufficiently large file, the <code>-9</code> version should be smaller than the <code>-1</code> version.</p><h3>Step 7</h3><p>In this step your goal is to implement the <code>-l</code>, <code>-t</code>, and <code>-v</code> flags.</p><p>Implement the <code>-l</code> flag, which displays compression statistics for a <code>.gz</code> file without decompressing it. The output should include the compressed size, uncompressed size, compression ratio, and the original filename. The format should match the standard gzip output:</p><pre><code><code>         compressed        uncompressed  ratio uncompressed_name
                 73                  26  42.3% test.txt
</code></code></pre><p>Implement the <code>-t</code> flag, which tests the integrity of a <code>.gz</code> file by decompressing it and validating the CRC-32 checksum without writing the output to disk. If the file is valid, it exits silently with status 0. If it&#8217;s corrupt, it reports an error.</p><p>Implement the <code>-v</code> flag, which displays the name and compression ratio for each file as it is processed. This is useful when compressing multiple files so you can see progress.</p><p><strong>Testing:</strong> Test the <code>-l</code> flag:</p><pre><code><code>gzip -k test.txt
ccgzip -l test.txt.gz
gzip -l test.txt.gz
</code></code></pre><p>Compare the output of both commands &#8212; they should show the same statistics. Test <code>-t</code> on a valid and a corrupted file. Test <code>-v</code> by compressing multiple files and checking that each one shows its name and ratio.</p><h3>Step 8</h3><p>In this step your goal is to implement recursive directory compression, preserve file metadata, and ensure full compatibility with the standard gzip tools.</p><p>Implement the <code>-r</code> flag, which recursively traverses directories and compresses (or decompresses) all files found within them.</p><p>The gzip header has optional fields for the original filename and the modification timestamp of the source file. Your tool should store these when compressing and restore the modification timestamp when decompressing.</p><p>Your output should be fully compatible with the system <code>gzip</code> and <code>gunzip</code> , any file compressed by your tool should decompress correctly with <code>gunzip</code>, and any file compressed by <code>gzip</code> should decompress correctly with your tool. The <code>.gz</code> extension should be added on compression and removed on decompression.</p><p><strong>Testing:</strong> Test recursive compression:</p><pre><code><code>mkdir -p testdir/subdir
echo "Root file" &gt; testdir/file1.txt
echo "Sub file" &gt; testdir/subdir/file2.txt
ccgzip -r testdir
find testdir -name "*.gz"
</code></code></pre><p>You should find <code>testdir/file1.txt.gz</code> and <code>testdir/subdir/file2.txt.gz</code>. Test timestamp preservation:</p><pre><code><code>touch -t 202301151200.00 test.txt
ccgzip -k test.txt
ccgzip -d test.txt.gz
stat test.txt
</code></code></pre><p>The modification timestamp should match the original. Test full compatibility by compressing a variety of files (text, binary, empty) with both your tool and the system <code>gzip</code>, and cross-decompressing them to verify they produce identical output.</p><h3>Going Further</h3><p>Here are some ideas to take your gzip implementation further:</p><ul><li><p>Add support for concatenated gzip streams (multiple gzip members in a single file)</p></li><li><p>Implement the <code>-rsyncable</code> option which makes the compressed output more friendly to rsync&#8217;s delta transfer algorithm</p></li><li><p>Add support for the <code>-suffix</code> option to use a custom file extension</p></li><li><p>Build a parallel compression mode (like <code>pigz</code>) that uses multiple CPU cores to compress data faster</p></li><li><p>Add support for decompressing other formats that gzip can handle, such as compress (<code>.Z</code>) files</p></li><li><p>Experiment with different match-finding strategies (hash chains, binary trees, optimal parsing) and measure the compression ratio and speed trade-offs</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #113 - AI Writing Detector]]></title><description><![CDATA[This challenge is to build your own AI Writing Detector.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-113-ai-writing-detector</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-113-ai-writing-detector</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 28 Mar 2026 09:02:19 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/f16aedbf-cd3a-45dd-bc11-fc5f76904237_1392x752.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #113 - AI Writing Detector</h2><p>This challenge is to build your own AI writing detector that analyses text and determines the likelihood it was written by an AI rather than a human.</p><p>Some people believe and argue that AI-generated text has recognisable patterns and characteristics. They argue that language models tend to favour certain vocabulary, use particular phrasing structures, and employ specific rhetorical techniques. To a certain extent they&#8217;re right, LLMs can indeed sometimes produce text that is often unnaturally uniform in sentence length, excessively formal in tone, and littered with vague attributions that sound authoritative but say nothing specific. By identifying these patterns, you can build a system that scores text and provides a detailed analysis of what makes it look AI-generated or human-written.</p><p>By building this project you&#8217;ll learn how to implement some complex text analysis and just how hard it truly is to detect AI written text. Once you have a working solution try it on some famous text that pre-dates LLMs (i.e. written before 2017). The reality is it is hard to detect AI writing, but a great learning exercise.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get access to a monthly AMA and 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building an AI Writing Detector</h2><p>You&#8217;ll build a system that accepts text input and analyses it using a rule-based approach to detect linguistic patterns and characteristics commonly found in AI-generated writing. Your detector will evaluate text across multiple dimensions &#8212; vocabulary, sentence structure, rhetorical patterns, and statistical properties &#8212; and produce a detailed report showing the likelihood the text was AI-generated. And no, the em-dash &#8216;&#8212;&#8216; is not one of them!</p><p>This is a coding challenge that focuses on text analysis, pattern detection, and building a scoring system. You can implement it in any programming language you&#8217;re comfortable with.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>Choose a programming language you&#8217;re comfortable with for text analysis and string manipulation. You&#8217;ll be doing a fair amount of tokenisation, regular expression matching, and statistical calculation. Consider whether you&#8217;d like to build a command-line tool, a web-based interface, or both.</p><p>For testing, you&#8217;ll evaluate your detector against sample texts. Some AI-generated and some human-written. Prepare a small collection of texts to test against as you build. You can generate AI-written samples by asking any large language model to write an essay on a topic, and use your own writing or published articles for the human-written samples.</p><h3>Step 1</h3><p>In this step your goal is to accept text input for analysis and display basic statistics about it.</p><p>Your system should provide a way for users to submit text. Whether that&#8217;s reading from a file, accepting command-line input, or providing a text area in a web interface. As text is entered, display a character count and word count. If the input is empty, the analysis should not proceed.</p><p><strong>Testing guidance:</strong> Load a sample text and verify that your character count and word count are correct. Try submitting empty input and confirm your system handles it gracefully by preventing the analysis from running.</p><h3>Step 2</h3><p>In this step your goal is to identify vocabulary and phrases that are commonly overused in AI-generated text.</p><p>Language models have characteristic vocabulary patterns, they favour words like &#8220;delve into&#8221;, &#8220;navigate&#8221;, &#8220;robust&#8221;, &#8220;innovative solutions&#8221;, &#8220;transformative&#8221;, &#8220;leverage&#8221;, &#8220;streamline&#8221;, and &#8220;ecosystem&#8221;. Build a detector that scans for these LLM-signature words and phrases and tracks how many distinct ones appear. You can find examples in Wikipedia&#8217;s <a href="https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing#High_density_of_%22AI_vocabulary%22_words">Signs of AI writing article</a>.</p><p>Score this detector based on the number of distinct AI vocabulary terms found, with each contributing a few points up to a maximum cap. This gives you your first pattern detection category.</p><p><strong>Testing guidance:</strong> Write or generate a short AI-written essay and run your detector on it. You should find multiple hits. Then run it on a piece of your own writing. You should see far fewer. Verify that the score increases with more AI vocabulary terms but doesn&#8217;t exceed your cap.</p><h3>Step 3</h3><p>In this step your goal is to identify several common structural patterns in AI writing. Language models frequently use:</p><ul><li><p><strong>The &#8220;rule of three&#8221;</strong> &#8212; describing things in groups of three adjectives, three nouns, or three parallel phrases. AI text is full of these.</p></li><li><p><strong>Negative parallelism</strong> &#8212; rigid constructions like &#8220;not only... but also&#8221; and similar overly formal parallel structures.</p></li><li><p><strong>Outline-style conclusions</strong> &#8212; formulaic endings that follow the pattern: &#8220;Despite [challenges], [subject] offers [benefits/opportunities].&#8221;</p></li><li><p><strong>False ranges</strong> &#8212; &#8220;from X to Y&#8221; constructions where the two endpoints don&#8217;t form a coherent or logical scale.</p></li></ul><p>Build detectors for each of these patterns. When you find them, record where they occur in the text and contribute to the score. Each pattern category should have its own maximum cap so that no single category can dominate the overall score. Again you can find examples in Wikipedia&#8217;s <a href="https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing#High_density_of_%22AI_vocabulary%22_words">Signs of AI writing article</a>.</p><p><strong>Testing guidance:</strong> Craft test sentences for each pattern type and verify your detectors find them. For the rule of three, try: &#8220;This approach is efficient, scalable, and maintainable.&#8221; For negative parallelism, try: &#8220;It not only improves performance but also enhances reliability.&#8221; Test that your scoring caps are working, text saturated with one pattern type should hit the cap and stop accumulating.</p><h3>Step 4</h3><p>In this step your goal is to identify patterns where text makes broad claims without concrete evidence. AI writing often lacks specificity, hiding behind phrases that sound authoritative but name no actual sources.</p><p>Build detectors for three categories:</p><ul><li><p><strong>Vague attributions</strong> &#8212; phrases like &#8220;experts agree&#8221;, &#8220;studies show&#8221;, &#8220;research indicates&#8221;, and &#8220;industry insiders report&#8221; that appeal to unnamed sources.</p></li><li><p><strong>Superficial analysis</strong> &#8212; hedging and filler like &#8220;it is worth noting&#8221;, &#8220;significant developments&#8221;, &#8220;one could argue&#8221;, and &#8220;various sources indicate&#8221;.</p></li><li><p><strong>Overgeneralisation</strong> &#8212; patterns that frame limited information as universal, such as &#8220;everyone knows&#8221;, &#8220;it is well established&#8221;, and &#8220;universal consensus&#8221;.</p></li></ul><p>Each category should contribute to the score independently, with its own cap.</p><p><strong>Testing guidance:</strong> Write test sentences containing each type of vague language and verify your detectors find them. Then test with text that contains legitimate attributions (&#8221;According to a 2024 study by Smith et al. in Nature...&#8221;) and confirm your detector doesn&#8217;t flag specific, concrete citations.</p><h3>Step 5</h3><p>In this step your goal is to identify excessive emphasis and promotional language patterns. AI writing tends to oversell, using superlatives and marketing-style phrasing that reads more like advertising copy than natural writing.</p><p>Build detectors for three categories:</p><ul><li><p><strong>Undue emphasis</strong> &#8212; excessive use of superlatives, intensifiers (&#8221;tremendous&#8221;, &#8220;remarkable&#8221;, &#8220;groundbreaking&#8221;), and emphatic punctuation.</p></li><li><p><strong>Promotional language</strong> &#8212; marketing phrasing like &#8220;game-changer&#8221;, &#8220;revolutionary&#8221;, &#8220;impressive features&#8221;, and &#8220;transformative potential&#8221;.</p></li><li><p><strong>Elegant variation</strong> &#8212; where the same concept or entity is repeatedly referred to by different but equivalent terms across sentences. AI text often cycles through synonyms for the same thing rather than simply repeating the word as a human would.</p></li></ul><p><strong>Testing guidance:</strong> Run your detectors on marketing copy or AI-generated product descriptions &#8212; these should score highly. Compare with straightforward human writing about the same topic. For elegant variation, look for text where &#8220;the company&#8221; becomes &#8220;the organisation&#8221; becomes &#8220;the firm&#8221; becomes &#8220;the enterprise&#8221; within a few sentences.</p><h3>Step 6</h3><p>In this step your goal is to move beyond pattern matching and analyse the statistical properties of the writing itself. AI text has measurable differences from human writing in several dimensions.</p><p>Build analysers for some or all of these linguistic factors:</p><ul><li><p><strong>Lexical diversity</strong> &#8212; calculate the type-token ratio (unique words divided by total words). AI text often falls outside the normal range, either too uniform or artificially varied.</p></li><li><p><strong>Sentence length variation</strong> &#8212; measure the standard deviation and coefficient of variation of sentence lengths. Human writing naturally varies; AI text tends to be unnaturally uniform, with a coefficient of variation below 0.35.</p></li><li><p><strong>Passive voice frequency</strong> &#8212; identify passive voice constructions and calculate what percentage of sentences use them. Human writers typically use passive voice in 5-10% of sentences; AI text often exceeds 15%.</p></li><li><p><strong>Transition word density</strong> &#8212; detect formal discourse markers (&#8221;furthermore&#8221;, &#8220;moreover&#8221;, &#8220;consequently&#8221;, &#8220;additionally&#8221;) and calculate the percentage of sentences containing them. More than 20% suggests AI generation.</p></li><li><p><strong>Reading grade level</strong> &#8212; calculate the Flesch-Kincaid Grade Level. AI text often scores at an artificially high grade level (above 14), suggesting unnecessary complexity.</p></li><li><p><strong>Punctuation patterns</strong> &#8212; analyse the density of semicolons, em-dashes, colons, and ellipses. AI text tends to overuse semicolons and em-dashes whilst rarely using ellipses.</p></li><li><p><strong>Rare word usage</strong> &#8212; identify uncommon words and calculate their frequency. Human writers typically use rare words at 3-8% frequency; AI text often exceeds 12%.</p></li></ul><p>Display each factor as a labelled indicator with a percentage value and a brief explanation of what the result means. Beware that some of these fail on short text.</p><p><strong>Testing guidance:</strong> Run your analysers on both AI-written and human-written samples of similar length and topic. Compare the results &#8212; you should see measurable differences. Pay particular attention to sentence length variation and transition word density, which tend to be strong signals. Verify your Flesch-Kincaid calculation against an online readability calculator.</p><h3>Step 7</h3><p>In this step your goal is to aggregate all your pattern detections and linguistic analyses into an overall AI probability score from 0 to 100.</p><p>Combine the contributions from each detector. If the raw combined score exceeds 100, normalise the individual contributions proportionally so the final score is clamped to 100.</p><p>Then classify the text based on the score:</p><ul><li><p>Below 30: &#8220;Likely Human-Written&#8221;</p></li><li><p>30 to 59: &#8220;Possibly AI-Generated&#8221;</p></li><li><p>60 or above: &#8220;Likely AI-Generated&#8221;</p></li></ul><p>Display the score with a colour-coded indicator, green below 30, yellow for 30-59, red for 60 and above.</p><p><strong>Testing guidance:</strong> Run your complete detector on several sample texts. Verify that AI-written samples score above 60 and human-written samples score below 30. If your scores don&#8217;t separate well, experiment with the weighting of different detectors. Check that the individual detector contributions add up correctly to the total score, and that normalisation works when the raw total would exceed 100.</p><h3>Step 8</h3><p>In this step your goal is to produce a comprehensive report that shows what was detected and why the text received its classification.</p><p>Your report should include:</p><ul><li><p>The overall AI probability score and classification</p></li><li><p>Text statistics (word count, character count, average word length)</p></li><li><p>A breakdown of linguistic factors, each with its score and an explanation</p></li><li><p>A breakdown of pattern detections, each showing the category, occurrence count, score contribution, and explanatory text</p></li><li><p>The timestamp of when the analysis was performed</p></li></ul><p>Present the linguistic factors and pattern detections in separate labelled sections so the report is easy to scan.</p><p><strong>Testing guidance:</strong> Generate reports for both AI-written and human-written samples. The reports should tell a coherent story about why each text received its classification. Verify that the pattern breakdowns add up to the overall score. Check that the report is readable and the explanations make sense to someone who doesn&#8217;t know the internals of your system.</p><h3>Going Further</h3><p>Once you&#8217;ve built the core detector, here are ways to extend it:</p><ul><li><p><strong>Text highlighting</strong> &#8212; mark detected patterns directly in the original text with category-specific colours. When highlights from different detectors overlap, keep the first one and discard subsequent overlaps. Display category badges showing which patterns were found.</p></li><li><p><strong>Advanced linguistic analysis</strong> &#8212; implement Zipf&#8217;s Law comparison (comparing word frequency distribution against the expected power-law distribution), named entity density analysis, and paragraph coherence measurement through inter-sentence similarity.</p></li><li><p><strong>Copy and share</strong> &#8212; add a &#8220;Copy Results&#8221; button that copies the score and analysis to the clipboard, and an &#8220;Analyse Another Text&#8221; option to return to the input.</p></li><li><p><strong>Accuracy measurement</strong> &#8212; collect a larger corpus of human-written and AI-written samples and measure your detector&#8217;s precision and recall.</p></li><li><p><strong>Weighting experiments</strong> &#8212; try different weightings for each detector. Some patterns are stronger signals than others &#8212; which ones matter most?</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #112 - AI Coding Agent]]></title><description><![CDATA[This challenge is to build your own AI Coding Agent.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-112-ai-coding-agent</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-112-ai-coding-agent</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 21 Mar 2026 09:01:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c26d442e-306a-4d7d-86ec-b05d822099a4_1600x896.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #112 - AI Coding Agent</h2><p>This challenge is to build your own AI coding agent - a command-line tool that can read, understand, and modify code on your behalf by combining a large language model with practical software engineering tools.</p><p>AI coding agents have rapidly become part of the modern developer&#8217;s toolkit. Tools like Claude Code, Codex, Cursor, Kiro, and AmpCode let you describe what you want in natural language and the agent figures out which files to read, what changes to make, and how to verify its work. Under the hood, they&#8217;re surprisingly approachable: a loop that talks to an LLM, a set of tools the model can call, and some orchestration to keep everything on track.</p><p>By building your own, you&#8217;ll gain a deep understanding of how these tools actually work - the agentic loop, tool use, context management, and all the engineering that turns a chat API into a coding assistant. You&#8217;ll also end up with something you can actually use on your own projects.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get access to a monthly AMA and 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Your Own AI Coding Agent</h2><p>You&#8217;re going to build a command-line AI coding agent, a simplified version of tools like Claude Code, Codex and AmpCode. It starts as a simple chat interface, and step by step you&#8217;ll add the ability to read files, edit code, run shell commands, search a codebase and manage context. By the end, you&#8217;ll have a working agent that can navigate a real project and make meaningful changes to it.</p><p>To really get the most from this challenge I suggest you call the LLM provider&#8217;s REST API directly and manage all the data yourself, this will give you the best understanding of AI agents and how they work.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>You&#8217;ll need to make a few decisions:</p><ol><li><p><strong>Choose your LLM provider.</strong> You need a model that supports tool calling (also called function calling). Most major providers support this: Anthropic, OpenAI, Google (Gemini), Mistral, or local models via Ollama. Check your chosen provider&#8217;s documentation for their tool-calling API - you&#8217;ll be using it heavily throughout this challenge.</p></li><li><p><strong>Choose your programming language.</strong> Pick something you&#8217;re comfortable building CLI tools in. You&#8217;ll be doing a fair amount of file I/O, process spawning, and JSON handling. Python, TypeScript, Go, and Rust all work well. The language doesn&#8217;t matter nearly as much as your comfort with it.</p></li><li><p><strong>Get your API key set up.</strong> Make sure you can make a basic chat completion request to your chosen provider and get a response back before moving on.</p></li></ol><p>Prepare a small test project to use as a playground throughout the challenge - a simple application with a few files in a couple of directories. You&#8217;ll be pointing your agent at this project to test reading, editing, and searching.</p><p><strong>Testing:</strong> Make a simple API call to your LLM provider with a basic prompt like &#8220;Hello, who are you?&#8221; and verify you get a coherent response. If you&#8217;re using a local model, confirm it&#8217;s running and accessible. I suggest using <code>curl</code> to do this so you know how to call the REST API for your provider.</p><h3>Step 1</h3><p>In this step your goal is to build the core agentic loop with streaming responses.</p><p>The heart of any coding agent is the loop: read user input, send it to the LLM, display the response, repeat. Build a REPL (read-eval-print loop) that takes input from the terminal, sends it to your LLM as a chat message, and streams the response back to the terminal as it arrives.</p><p>Streaming matters here. LLM responses can take several seconds to generate in full, and watching text appear token by token is a much better experience than staring at a blank screen. Your provider&#8217;s API will have a streaming option - use it.</p><p>Your loop should maintain a conversation history so the model has context from earlier in the session. Each time you send a request, include the full conversation so far: all previous user messages and assistant responses.</p><p>Handle the basics gracefully: let the user exit the session cleanly, and don&#8217;t crash if the API returns an error.</p><p><strong>Testing:</strong></p><ul><li><p>Start your agent and have a multi-turn conversation. Ask a question, then ask a follow-up that references the previous answer. The model should understand the context.</p></li><li><p>Verify responses stream to the terminal incrementally rather than appearing all at once.</p></li><li><p>Check that you can exit the session cleanly (e.g. with Ctrl+C or typing &#8220;quit&#8221; or &#8220;exit&#8221;).</p></li><li><p>Disconnect from the network and send a message - verify the agent handles the error without crashing.</p></li></ul><h3>Step 2</h3><p>In this step your goal is to add tool calling and implement file reading as your first tool.</p><p>Tool calling is what turns a chatbot into an agent. Instead of just generating text, the model can request to call a function - read a file, run a command, search for something - and your agent executes it and feeds the result back. The model then uses that result to continue its response.</p><p>Define a tool interface that your LLM can call. The exact format depends on your provider, but typically you describe each tool with a name, a description, and a JSON schema for its parameters. Start with a single tool: <code>read_file</code>, which takes a file path and returns the file&#8217;s contents.</p><p>The agentic loop now becomes: send the conversation to the LLM. If the response includes a tool call, execute it, append the result to the conversation, and send it back to the LLM. Keep looping until the model responds with text instead of a tool call. The model might chain several tool calls before giving a final answer - your loop should handle that naturally.</p><p><strong>Testing:</strong></p><ul><li><p>Ask your agent &#8220;What&#8217;s in the file <code>README.md</code>?&#8221; (or any file in your test project). It should call the <code>read_file</code> tool, receive the contents, and summarise or discuss the file.</p></li><li><p>Ask it about a file that doesn&#8217;t exist. The tool should return an error, and the model should explain that the file wasn&#8217;t found rather than crashing.</p></li><li><p>Ask a question that requires reading multiple files. The model should make multiple tool calls in sequence to gather the information it needs.</p></li><li><p>Ask a question that doesn&#8217;t need any file reading (e.g. &#8220;What is a binary tree?&#8221;). The model should answer directly without calling any tools.</p></li></ul><h3>Step 3</h3><p>In this step your goal is to add file editing and codebase search tools.</p><p>A coding agent that can only read files isn&#8217;t much use - it needs to be able to make changes too. Add an <code>edit_file</code> tool that applies a targeted edit to a file. A good approach is to have the tool take the file path, the text to find, and the text to replace it with. This is safer than having the model rewrite entire files, which is both slow and error-prone.</p><p>Also add a <code>write_file</code> tool for creating new files. This takes a file path and the full content to write.</p><p>Next, add two search tools so the agent can navigate unfamiliar code. A <code>glob</code> tool that finds files matching a pattern (e.g. <code>**/*.py</code>, <code>src/**/*.ts</code>) and a <code>grep</code> tool that searches file contents for a pattern and returns matching lines with file paths and line numbers.</p><p>With these five tools - read, edit, write, glob, and grep - your agent can explore and modify a codebase in a meaningful way.</p><p><strong>Testing:</strong></p><ul><li><p>Ask your agent to add a comment to a specific function in your test project. It should read the file, make a targeted edit, and confirm the change. Open the file and verify the edit is correct.</p></li><li><p>Ask it to create a new file with some content. Verify the file is created with the correct contents.</p></li><li><p>Ask it to find all Python (or whatever language your test project uses) files in the project. It should use the glob tool and return the list.</p></li><li><p>Ask it to find where a specific function or variable is used across the codebase. It should use the grep tool to search.</p></li><li><p>Ask it to refactor something - rename a variable or extract a function. This should require multiple tool calls: search to find usages, then edit each one.</p></li></ul><h3>Step 4</h3><p>In this step your goal is to add shell command execution and a permission system.</p><p>Shell access makes your agent dramatically more capable. It can run tests, install dependencies, check build output, and interact with any command-line tool. Add a <code>bash</code> tool that takes a command string, executes it in a shell, and returns the stdout, stderr, and exit code.</p><p>But with great power comes the need for guardrails. You don&#8217;t want your agent silently running <code>rm -rf /</code> because the model hallucinated a cleanup step. Implement a permission system that controls which actions the agent can take without asking.</p><p>Your permission system should support at least three modes for each tool: <strong>allow</strong> (execute without asking), <strong>prompt</strong> (ask the user for confirmation before executing), and <strong>deny</strong> (never execute). A sensible default is to allow read-only operations (file reading, glob, grep) automatically, prompt for mutations (file edits, shell commands), and let the user configure overrides.</p><p>When a tool call requires confirmation, display the tool name and its arguments clearly and wait for the user to approve or reject before proceeding.</p><p><strong>Testing:</strong></p><ul><li><p>Ask your agent to run the test suite for your test project. It should use the bash tool to execute the appropriate test command and report the results.</p></li><li><p>Verify that shell commands require your approval before running (assuming you&#8217;ve set bash to &#8220;prompt&#8221; mode).</p></li><li><p>Deny a shell command when prompted and verify the agent adapts gracefully - it should acknowledge that you declined and try an alternative approach or explain what it was trying to do.</p></li><li><p>Configure file reading to &#8220;allow&#8221; and verify those calls execute without prompting.</p></li><li><p>Ask the agent to do something that involves both allowed and prompted tools in sequence. Verify the allowed tools execute silently and the prompted ones ask for confirmation.</p></li></ul><h3>Step 5</h3><p>In this step your goal is to add context window management so your agent can handle long sessions without breaking.</p><p>Every LLM has a context window limit, and coding sessions can generate a lot of content. Reading a few large files, running some commands, and having a back-and-forth conversation can fill up the context quickly. When you hit the limit, your API calls will fail.</p><p>Implement a strategy to manage this. A practical approach is conversation compression (aka compaction): when the conversation history approaches the context limit, summarise the older messages into a condensed form and keep only the recent messages intact. The summary preserves the key decisions, findings, and context from earlier in the conversation without using as many tokens. Most agents use a call to the LLM to generate the summary. Make that call in the background and don&#8217;t show the user.</p><p>You&#8217;ll need to track token usage. Most providers return token counts in their API responses. Keep a running total and trigger compression when you&#8217;re approaching the limit - leaving enough headroom for the model&#8217;s response.</p><p>After compression, the conversation should continue to work naturally. The model should still understand what it was doing and what decisions were made earlier, even if it can&#8217;t see the exact messages from the beginning of the session.</p><p><strong>Testing:</strong></p><ul><li><p>Have a long session with your agent where you read several large files and have an extended conversation. Verify it doesn&#8217;t crash when the context gets large.</p></li><li><p>After compression has occurred, ask the agent to recall something from earlier in the conversation. It should still have the key information from the summary.</p></li><li><p>Check your token tracking by asking the agent how much of the context window has been used (you might expose this in a status command or similar).</p></li><li><p>Verify that tool calls still work correctly after compression - the model should still know which tools are available and how to use them.</p></li></ul><h3>Step 6</h3><p>In this step your goal is to add project context loading and a configuration file hierarchy.</p><p>A good coding agent should understand the project it&#8217;s working in without being told everything from scratch. Add support for a project instruction file - a markdown file in the project root (e.g. <code>AGENTS.md</code> or <code>CLAUDE.md</code> I suggest you use <code>AGENTS.md</code>) that contains project-specific context. When the agent starts, it should look for this file and include its contents in the system prompt.</p><p>This file might contain information like the project&#8217;s architecture, coding conventions, how to run tests, which directories contain what, or anything else that would help the agent be more effective. The contents of this file are added to the context sent to the LLM as one of the first user messages.</p><p>Next, implement a configuration file hierarchy. Settings should cascade from three levels: global (user-wide defaults, e.g. in a home directory dotfile), project-level (in the project root), and local (for personal overrides that aren&#8217;t committed to source control). More specific settings override more general ones.</p><p>The configuration should cover at least: the default LLM provider and model, permission defaults for each tool, and any custom system prompt additions.</p><p><strong>Testing:</strong></p><ul><li><p>Create an <code>AGENTS.md</code> file in your test project with some specific instructions (e.g. &#8220;Always use snake_case for variable names&#8221; or &#8220;Run tests with <code>pytest</code>&#8220;). Ask the agent to make a change and verify it follows the project instructions.</p></li><li><p>Verify the agent works fine when no project instruction file exists - it should carry on without error.</p></li><li><p>Set up global and project-level configuration files with different values for the same setting. Verify the project-level setting takes precedence.</p></li><li><p>Add a local configuration override and verify it takes precedence over both project and global settings.</p></li></ul><h3>Step 7</h3><p>In this step your goal is to add persistent memory so your agent remembers context across sessions and conversation history so you can resume previous sessions.</p><p>Without persistence, every session starts from zero. If you told the agent about your project&#8217;s architecture yesterday, it&#8217;s forgotten it today. Implement a memory system that lets the agent store and retrieve information across sessions.</p><p>A file-based approach works well: the agent writes memories to a designated directory as individual files, with an index that tracks what&#8217;s stored. Memories might include things the user has asked the agent to remember, project decisions, or user preferences. When a new session starts, the agent loads relevant memories to inform its behaviour.</p><p>Also add conversation history persistence. Save completed sessions so the user can resume a previous conversation with its full context intact, or start a new session that has access to a summary of past work. Again use the LLM to generate summaries.</p><p><strong>Testing:</strong></p><ul><li><p>Tell your agent to remember something specific (e.g. &#8220;Remember that our API uses JWT authentication&#8221;). End the session, start a new one, and ask a question where that context is relevant. The agent should use the stored memory in its response.</p></li><li><p>Ask the agent what it remembers. It should be able to list or describe its stored memories.</p></li><li><p>Tell the agent to forget something it previously stored. Verify it&#8217;s removed.</p></li><li><p>End a session, then resume it. The conversation context should be intact.</p></li><li><p>Start a fresh session and verify it doesn&#8217;t carry over the conversation history from the previous one (though memories should still be accessible).</p></li></ul><h3>Step 8</h3><p>In this step your goal is to add subagent support and plan mode.</p><p>Some tasks benefit from being broken down and worked on in parallel, or from being planned before implementation begins. Add the ability for your agent to spawn subagents - separate agent instances that work on a specific subtask and report back.</p><p>A subagent should have its own conversation with the LLM, its own context, and access to the same tools as the main agent. The main agent describes a task, the subagent works on it independently, and returns a result. This is useful for things like &#8220;search the codebase for all usages of this pattern&#8221; or &#8220;read these five files and summarise what they do&#8221; - tasks that would clutter the main conversation with tool calls.</p><p>Also add a plan mode. When activated, the agent switches to an architect role: it reads code, asks questions, and produces a plan, but doesn&#8217;t make any changes. Once the user approves the plan, the agent switches back to implementation mode and follows the plan. This is valuable for larger tasks where you want to review the approach before any code is modified. Plan mode will often benefit from a reduced toolset and a customised system prompt.</p><p><strong>Testing:</strong></p><ul><li><p>Ask your agent to do something that benefits from subagents, like &#8220;summarise all the files in the <code>src</code> directory&#8221;. Verify it spawns subagents and combines their results.</p></li><li><p>Verify that subagent work doesn&#8217;t pollute the main conversation - the main agent should present a clean summary.</p></li><li><p>Activate plan mode and ask the agent to implement a feature. Verify it produces a plan without making any changes.</p></li><li><p>Approve the plan and verify the agent implements it.</p></li><li><p>Reject or modify the plan and verify the agent adapts.</p></li></ul><h3>Going Further</h3><p>You&#8217;ve built a working AI coding agent. Here are some ways to push it further:</p><ul><li><p><strong>Model Context Protocol (MCP):</strong> Add support for MCP, which lets your agent connect to external tool servers. This means anyone can extend your agent&#8217;s capabilities by writing an MCP server, without modifying the agent itself.</p></li><li><p><strong>Skills System:</strong> Add the ability to define reusable skills - pre-written prompts and tool configurations that can be invoked by name. For example, a <code>commit</code> skill that knows how to stage changes and create a well-formatted commit, or a <code>review</code> skill that analyses code for issues.</p></li><li><p><strong>Hooks:</strong> Let users define shell commands that trigger on agent events - before a tool executes, after a file is edited, when a session starts. This enables custom workflows like running a linter automatically after every file edit.</p></li><li><p><strong>Model Selection:</strong> Support switching between different models mid-session. Some tasks need the most capable model available, while others can use a faster, cheaper one.</p></li><li><p><strong>Headless Mode:</strong> Add a non-interactive mode where the agent receives a prompt, executes it, and exits. This enables CI/CD integration and scripted automation.</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #111 - AI Agent Scheduling System]]></title><description><![CDATA[This challenge is to build your own AI Agent scheduling system.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-111-ai-agent-scheduling</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-111-ai-agent-scheduling</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 14 Mar 2026 09:01:57 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ba3a362a-a4bb-4e53-b7bb-8aa29e7405a2_2400x1601.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #111 - AI Agent Scheduling System</h2><p>This challenge is to build your own AI agent scheduling system - a system that runs AI-powered tasks automatically on a cron schedule and delivers the results to you.</p><p>If you&#8217;ve ever wished you could have an assistant that checks your project&#8217;s dependencies for security advisories every Monday, summarises your inbox before you wake up, or keeps an eye on the status pages of the services your stack depends on throughout the day - that&#8217;s exactly what you&#8217;re building here. The idea is simple: you define a task, tell the agent scheduling system when to run it, and it takes care of the rest, delivering a summary straight to your email.</p><p>Scheduled agents are becoming a core pattern in AI-powered automation. By building your own, you&#8217;ll learn how to combine LLM orchestration with job scheduling, error handling, and notification delivery. Skills that apply well beyond this single project.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get access to a monthly AMA and 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Your Own Scheduled Agent</h2><p>You&#8217;ll build a system that lets you define named AI agents, each with a task description and a cron schedule, that run automatically and deliver their results by email. Along the way you&#8217;ll work with LLM integration, cron-based job scheduling, natural language parsing, retry logic, and email delivery.</p><p>The challenge starts with a basic AI agent that can execute a single task, then progressively adds scheduling, resilience, notifications, and management capabilities. Here are some use cases the full solution could handle:</p><ul><li><p><strong>Morning Briefing</strong> - An agent runs every day at 7am, pulls your email, calendar, and relevant news, and drops a structured brief into your inbox.</p></li><li><p><strong>Weekly Dependency Watch</strong> - Every Monday at 8am, an agent checks your project&#8217;s key dependencies for new releases, security advisories, and deprecation notices, and delivers a structured summary.</p></li><li><p><strong>Daily Outage &amp; Incident Digest</strong> - Every few hours, an agent checks the status pages of services your stack depends on (AWS, GitHub, npm, your CI provider) and flags any ongoing or recent incidents that could affect your work.</p></li><li><p><strong>Weekly Tech Radar</strong> - Every Monday, an agent scans Hacker News, tech blogs, and release notes for developments in your chosen technology areas and delivers a curated summary.</p></li><li><p><strong>PR Review Reminder</strong> - Twice a day, an agent checks your team&#8217;s open pull requests and sends you a summary of what&#8217;s awaiting review, what&#8217;s gone stale, and what&#8217;s been merged.</p></li></ul><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>You&#8217;ll need to make a few decisions:</p><ol><li><p><strong>Choose your LLM provider.</strong> You&#8217;ll need access to a language model for your agents to use when processing tasks. Options include OpenAI, Anthropic, Google Gemini, Mistral, or running models locally with Ollama. Any provider with a chat completions API will work.</p></li><li><p><strong>Choose your programming language.</strong> Pick something you&#8217;re comfortable with for both HTTP requests and background job processing. You&#8217;ll be building a long-running service that needs to make API calls, run tasks on a schedule, and send emails.</p></li><li><p><strong>Choose your persistence layer.</strong> You&#8217;ll need somewhere to store agent definitions, schedules, and execution history. A lightweight database like SQLite works well to start with, or Redis if you&#8217;d prefer something in-memory.</p></li></ol><h3>Step 1</h3><p>In this step your goal is to build a basic AI agent that can execute a single task.</p><p>An agent at this stage is straightforward: it takes a task description (a natural language prompt), sends it to your LLM, and returns the result. Think of it as a thin wrapper around an LLM call that adds structure.</p><p>Define an agent with a name, a task description, and optionally a system prompt that shapes how the agent behaves. For example, an agent called &#8220;Hacker News Summariser&#8221; might have the task &#8220;Summarise the top five technology news stories today&#8217;s hacker news posts which are {posts}&#8221;, where you substitute in the posts from Hacker News and a system prompt instructing it to be concise and use bullet points.</p><p>The agent should return a structured result containing the agent&#8217;s name, the time the task ran, whether it succeeded or failed, and the LLM&#8217;s response.</p><p><strong>Testing:</strong> Create a few agents with different task descriptions and run them manually:</p><ul><li><p>A simple summarisation agent - give it a prompt like &#8220;Summarise the key benefits of test-driven development&#8221; and verify you get a coherent response.</p></li><li><p>An agent with a specific system prompt - verify the response style matches the instructions in the system prompt.</p></li><li><p>An agent with a deliberately impossible task - verify it completes without crashing and returns something sensible.</p></li><li><p>Check that the structured result includes the agent name, timestamp, status, and response content.</p></li></ul><h3>Step 2</h3><p>In this step your goal is to add cron-based scheduling so agents run automatically at defined intervals.</p><p>It should be possible to configure the invocation of each agent based on a cron expression that defines when it runs. Support the full five-field cron format: minute, hour, day of month, month, and day of week. This gives you the flexibility to express schedules like &#8220;every hour&#8221; (<code>0 * * * *</code>), &#8220;weekdays at 9am&#8221; (<code>0 9 * * 1-5</code>), or &#8220;first Monday of every month at 8am&#8221; (<code>0 8 1-7 * 1</code>).</p><p>Your scheduler should evaluate all registered agents, determine which ones are due to run, execute them, and then wait for the next tick. When multiple agents are due at the same time, they should all run - don&#8217;t let one agent&#8217;s execution block another.</p><p>Agents should also have an enabled/disabled flag so you can pause an agent&#8217;s schedule without deleting it.</p><p><strong>Testing:</strong></p><ul><li><p>Create an agent with a schedule of <code>* * * *</code> (every minute) and verify it fires once per minute.</p></li><li><p>Create two agents scheduled for the same time and verify both run.</p></li><li><p>Disable an agent and verify it stops running on schedule. Re-enable it and verify it resumes.</p></li><li><p>Set up an agent with a more complex expression like <code>/5 * * * *</code> (every five minutes) and verify the timing is correct.</p></li><li><p>Check that the scheduler continues running reliably over a period of at least an hour without drifting or missing executions.</p></li></ul><h3>Step 3</h3><p>In this step your goal is to add natural language schedule parsing so users can describe when they want an agent to run in plain English instead of writing cron expressions.</p><p>Expressions like &#8220;every weekday at 9am&#8221;, &#8220;every Monday at 8am&#8221;, &#8220;twice a day&#8221;, &#8220;every 3 hours&#8221;, or &#8220;the first of every month&#8221; should be parsed into the corresponding cron expression. You can use your LLM to handle this translation, or a dedicated natural language parsing library - either approach works.</p><p>When a user provides a natural language schedule, show them the interpreted cron expression and a human-readable description of what it means (e.g. &#8220;Runs at 09:00 on Monday through Friday&#8221;) so they can confirm it&#8217;s correct before saving.</p><p>If the input is ambiguous or can&#8217;t be parsed, the system should say so clearly and ask the user to rephrase rather than guessing incorrectly.</p><p><strong>Testing:</strong></p><ul><li><p>Try a range of natural language inputs and verify each produces the correct cron expression:</p><ul><li><p>&#8220;every day at 7am&#8221; should produce <code>0 7 * * *</code></p></li><li><p>&#8220;every weekday at 9am&#8221; should produce <code>0 9 * * 1-5</code></p></li><li><p>&#8220;every Monday at 8am&#8221; should produce <code>0 8 * * 1</code></p></li><li><p>&#8220;every 3 hours&#8221; should produce <code>0 */3 * * *</code></p></li><li><p>&#8220;twice a day&#8221; should produce something reasonable like <code>0 9,18 * * *</code> (the exact times may vary)</p></li></ul></li><li><p>Try an ambiguous input like &#8220;sometimes in the morning&#8221; and verify you get a clear error or clarification request rather than a bad cron expression.</p></li><li><p>Verify the human-readable confirmation message accurately describes the interpreted schedule.</p></li></ul><h3>Step 4</h3><p>In this step your goal is to add timeout handling and retry logic so your agents are resilient to transient failures.</p><p>LLM API calls can be slow, rate-limited, or simply fail. Your agents need to handle this gracefully. Each agent should have a configurable execution timeout - if the task hasn&#8217;t completed within that time, it should be terminated cleanly. A sensible default is 60 seconds, but agents that do heavier processing might need longer.</p><p>When an execution fails (whether from a timeout, an API error, or any other exception), the agent should retry automatically. Each agent should have a configurable maximum number of retries and a backoff strategy. Exponential backoff with jitter is a solid default - it avoids hammering a struggling API with rapid retries.</p><p>After all retries are exhausted, the agent should record a permanent failure for that execution with the error details. Failed executions should never block future scheduled runs of the same agent.</p><p><strong>Testing:</strong></p><ul><li><p>Set an agent&#8217;s timeout to something very short (e.g. 2 seconds) with a task that takes longer than that. Verify the execution times out and a retry is attempted.</p></li><li><p>Configure an agent with 3 maximum retries and simulate a failing LLM call (you could temporarily use an invalid API key or point to a non-existent endpoint). Verify it retries exactly 3 times before recording a permanent failure.</p></li><li><p>Check that the backoff delay increases between retries rather than retrying immediately each time.</p></li><li><p>After a failed execution with all retries exhausted, verify the agent still runs on its next scheduled time as normal.</p></li><li><p>Run an agent that succeeds on the first attempt and verify no retries are triggered.</p></li></ul><h3>Step 5</h3><p>In this step your goal is to add email delivery so agent results are sent to you automatically.</p><p>When an agent completes its scheduled run, it should send the results to a configured email address. The email should include the agent&#8217;s name in the subject line, the timestamp of the execution, and the full response from the LLM formatted for readability.</p><p>Use an email sending service such as Resend or SendGrid or use SMTP directly to deliver the messages. For development and testing, a local SMTP tool like Mailpit or MailHog lets you capture emails without actually sending them.</p><p>If the email delivery fails, it should be retried independently of the agent execution itself - the agent&#8217;s task already succeeded, so you don&#8217;t want to re-run the whole thing just because the notification didn&#8217;t go through. Log delivery failures for visibility.</p><p>For failed agent executions (after all retries are exhausted), send a failure notification email instead, including the error details so you know something went wrong without having to check logs.</p><p><strong>Testing:</strong></p><ul><li><p>Run an agent and verify an email arrives with the correct subject line, agent name, timestamp, and response content.</p></li><li><p>Verify the email content is well-formatted and readable, not a raw text dump.</p></li><li><p>Check that a failed agent execution sends a failure notification email with error details.</p></li><li><p>Temporarily break your email configuration and verify the agent execution still completes successfully - only the delivery should fail, not the whole run.</p></li><li><p>Verify email delivery failures are logged and retried.</p></li></ul><h3>Step 6</h3><p>In this step your goal is to build a management interface for creating, editing, listing, and deleting agents.</p><p>Up to now you&#8217;ve probably been configuring agents directly in code or a configuration file. In this step, add an API (or CLI, if you prefer) that lets you manage agents without touching the code.</p><p>You should be able to create a new agent by providing a name, task description, system prompt, schedule (cron expression or natural language), email recipient, timeout, and retry settings. You should also be able to list all registered agents with their current status (enabled, disabled, last run time, last run result), edit any agent&#8217;s configuration, and delete agents you no longer need.</p><p>Each agent&#8217;s configuration and execution history should be persisted so everything survives a restart of the service. The execution history for each agent should include the timestamp, status (success or failure), duration, and a summary of the result or error.</p><p><strong>Testing:</strong></p><ul><li><p>Create a new agent through the management interface and verify it starts running on its defined schedule.</p></li><li><p>List all agents and check the output shows correct status information.</p></li><li><p>Edit an agent&#8217;s schedule and verify the new schedule takes effect without needing to restart the service.</p></li><li><p>Delete an agent and verify it stops running and is removed from the listing.</p></li><li><p>Restart the service and verify all agent configurations and execution history are preserved.</p></li><li><p>View the execution history for an agent and verify it includes timestamps, statuses, and durations for recent runs.</p></li></ul><h3>Step 7</h3><p>In this step your goal is to add execution logging and a monitoring endpoint so you can observe your system&#8217;s health at a glance.</p><p>Build a status endpoint (or command) that reports the overall system health: how many agents are registered, how many are enabled, upcoming scheduled runs, and aggregate statistics like total executions, success rate, and average execution time.</p><p>Each execution should be logged with enough detail to diagnose problems: the agent name, start time, duration, whether it succeeded or failed, the number of retries attempted, and a truncated version of the response or error. Keep a rolling window of execution history - the last 100 runs per agent is a reasonable default.</p><p>If any agent has failed its last three consecutive runs, flag it in the status output as unhealthy so you know to investigate.</p><p><strong>Testing:</strong></p><ul><li><p>Query the status endpoint and verify it reports the correct number of registered and enabled agents.</p></li><li><p>Run several agents and check the aggregate statistics update correctly (total executions, success rate, average duration).</p></li><li><p>Verify upcoming scheduled runs are listed with the correct next-run times.</p></li><li><p>Simulate three consecutive failures for an agent and verify it appears as unhealthy in the status output.</p></li><li><p>Check the execution log for an agent and verify it contains the expected detail for each run.</p></li></ul><h3>Going Further</h3><p>You&#8217;ve built a scheduled AI agent system with cron scheduling, retry logic, and email delivery. Here are some ways to push it further:</p><ul><li><p><strong>Skill or MCP support</strong>: Add the ability for agents to call skills, use tools and MCP.</p></li><li><p><strong>Slack Delivery:</strong> Add the option to send agent results to a Slack channel using the Slack Web API. Let each agent choose between email, Slack, or both as its delivery method.</p></li><li><p><strong>Discord Delivery:</strong> Add Discord webhook support as another delivery option. Discord&#8217;s webhook API makes this straightforward - you post a formatted message to a webhook URL and it appears in the configured channel.</p></li><li><p><strong>Web Dashboard:</strong> Build a web-based dashboard that visualises agent status, execution history, and upcoming schedules. A timeline view showing when each agent last ran and when it will run next is particularly useful.</p></li><li><p><strong>Agent Chaining:</strong> Let one agent&#8217;s output become another agent&#8217;s input. For example, a data-gathering agent runs first, and its results are passed to a summarisation agent that formats and delivers the final report.</p></li><li><p><strong>External Data Sources:</strong> Give your agents the ability to fetch live data as part of their tasks - pulling from RSS feeds, APIs, or web searches before asking the LLM to process the results. This turns your agents from simple prompt runners into genuine automation tools.</p></li><li><p><strong>Rate Limit Management:</strong> Add awareness of your LLM provider&#8217;s rate limits. If multiple agents are scheduled close together, stagger their execution to stay within your API quota rather than having them all fail from rate limiting.</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #110 - RTFM For Me Agent]]></title><description><![CDATA[This challenge is to build your own AI Agent to read documentation and answer questions on it.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-110-rtfm-for-me</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-110-rtfm-for-me</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 07 Mar 2026 09:01:38 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4e29a1fd-bdf1-4751-81cf-99899bf328cc_1600x896.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #110 - RTFM For Me Agent</h2><p>This challenge is to build your own AI-powered documentation assistant - a tool that can ingest technical documentation, answer questions about it using AI, and remember context across conversations.</p><p>If you&#8217;ve ever used an AI chatbot and wished it could answer questions specifically about your own documentation, that&#8217;s exactly what you&#8217;re building here. The technique behind it is called Retrieval-Augmented Generation (RAG). Instead of relying solely on what an AI model was trained on, you retrieve the specific documents relevant to a question and feed them to the model as context. The result is grounded, accurate answers with source citations rather than hallucinated guesses.</p><p>Redis is the backbone of this project. It handles vector search for finding relevant documents, semantic caching for avoiding redundant AI calls, session storage for conversation history, and long-term memory for remembering user context across sessions. Everything else - the AI model, the embedding provider, your programming language, and your framework - is entirely your choice. You can read all about <a href="https://fandf.co/4uiEHMD">Redis&#8217; AI offerings here</a>.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get access to a monthly AMA and 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building The RTFM For Me Agent</h2><p>In this challenge you&#8217;re going to build a RTFM For Me Agent, a full-stack AI assistant that ingests documentation files, answers questions using retrieval-augmented generation, caches semantically similar queries to reduce costs, and maintains memory across sessions. By the end, you&#8217;ll have a system that gets more useful the more you interact with it and can search and read documentation for you.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>You&#8217;ll need Docker and Docker Compose to run <a href="https://fandf.co/4aUAeGY">Redis Stack</a>, which provides Redis with built-in vector search capabilities. Set up a <code>docker-compose.yml</code> that runs Redis Stack.</p><p>Next, make three decisions that will shape the rest of your build:</p><ol><li><p><strong>Choose your LLM and embedding provider.</strong> You&#8217;ll need a language model for generating answers and an embedding model for converting text into vectors. Options include OpenAI, Anthropic, Google Gemini, Mistral, Cohere, or running models locally with Ollama. Whatever embedding model you choose, note its output dimensions - you&#8217;ll need this when creating your Redis vector index. If you&#8217;re new to this, an embedding model turns your text into a list of numbers (called a vector) that captures its meaning. This is what lets the system find relevant chunks of text later, by comparing how similar those number lists are.</p></li><li><p><strong>Choose your programming language and framework.</strong> Redis has client libraries for Python, TypeScript, Java, Go, Rust, C#, and more. You can use an AI agent framework like PydanticAI, LangChain, or LlamaIndex - or skip the framework entirely and call your LLM&#8217;s API directly.</p></li><li><p><strong>Choose your Redis client library.</strong> Python developers might want to look at <a href="https://fandf.co/4bjhBxF">The Python version of RedisVL</a>, Java developer can grab the new <a href="https://fandf.co/4r5XcAP">Java version of RedisVL</a>, both of which provides high-level abstractions for vector search, caching, and sessions.</p></li></ol><p>Prepare some sample documentation files (markdown, text, or HTML) that you&#8217;ll use to test your system throughout the challenge. Technical documentation with clear sections works well - API references, getting started guides, or architecture documents. A great example would be the Pro Git book: <a href="https://github.com/progit/progit2">https://github.com/progit/progit2</a> allowing you to create an agent to help with git commands.</p><p><strong>Testing:</strong> Verify Redis is running by connecting with <code>redis-cli</code> and running <code>PING</code>. You should receive <code>PONG</code> in response. Verify you can call your chosen LLM and embedding APIs successfully.</p><p>By the way, there is also a coding challenge that has you <a href="https://codingchallenges.fyi/challenges/challenge-redis">build your own Redis</a>.</p><h3>Step 1</h3><p>In this step your goal is to build a document ingestion pipeline that loads documentation files, splits them into chunks, generates vector embeddings for each chunk, and stores everything in a Redis vector index.</p><p>Start by loading your sample documentation files. Then split the text into smaller chunks - roughly 500 tokens each with some overlap between consecutive chunks so you don&#8217;t lose context at the boundaries. Try to split on natural boundaries like paragraphs rather than cutting mid-sentence.</p><p>For each chunk, generate a vector embedding using your chosen embedding provider. Then store the chunk in Redis along with its embedding and metadata: the source file name, the section heading, and the chunk&#8217;s position in the document. This metadata will become important later when you add filtering.</p><p>You&#8217;ll need to create a Redis vector index that supports similarity search over these embeddings. The index should make the chunk text full-text searchable, the metadata fields filterable, and the embedding vectors searchable by similarity. Refer to the <a href="https://redis.io/docs/latest/develop/interact/search-and-query/">Redis vector search documentation</a> for details on creating indexes with the <code>FT.CREATE</code> command.</p><p><strong>Testing:</strong> Ingest your sample documentation files and verify the data is in Redis:</p><ul><li><p>Run <code>FT.INFO</code> on your index to confirm it exists and shows the correct number of documents.</p></li><li><p>Run <code>HGETALL</code> on one of your stored document keys to verify it contains the chunk text, metadata fields, and embedding vector.</p></li><li><p>Try ingesting the same files again and verify your pipeline handles duplicates sensibly.</p></li></ul><h3>Step 2</h3><p>In this step your goal is to implement vector search and RAG-based answer generation. When a user asks a question, your system should find the most relevant document chunks and use them to generate a grounded answer.</p><p>The flow works like this: take the user&#8217;s question, convert it to an embedding using the same model you used for your documents, then search your Redis vector index for the most similar chunks. Take the top results and pass them to your LLM as context alongside the question.</p><p>Your system prompt should instruct the LLM to answer using only the provided context and to cite which source file each piece of information comes from. If the context doesn&#8217;t contain enough information to answer the question, the LLM should say so honestly rather than making something up.</p><p>Wrap this in a REST API with at least two endpoints: one for ingesting documents and one for asking questions. A streaming endpoint for the chat response is a nice addition if your framework supports server-sent events.</p><p><strong>Testing:</strong> Ask questions that you know the answers to based on your sample documentation:</p><ul><li><p>Ask a question that&#8217;s directly covered in your docs. The response should be accurate, cite the correct source file, and not include information that isn&#8217;t in the docs.</p></li><li><p>Ask a question that isn&#8217;t covered at all. The system should tell you it doesn&#8217;t have enough information rather than hallucinating an answer.</p></li><li><p>Ask a question that spans multiple documents. The system should pull context from several sources.</p></li><li><p>Test with <code>curl</code> to verify your API endpoints work correctly.</p></li></ul><h3>Step 3</h3><p>In this step your goal is to add semantic caching so that repeated or similar questions get instant answers without an LLM call.</p><p>Traditional caching uses exact string matches, which means &#8220;how do I authenticate?&#8221; and &#8220;what&#8217;s the authentication process?&#8221; would be treated as completely different queries. Semantic caching embeds the question and checks whether any previously cached question is close enough in vector space. If it is, the cached answer is served without touching the LLM at all.</p><p>You&#8217;ll need a separate Redis vector index for your cache entries. Each entry stores the original question, its embedding, and the generated response. When a new question comes in, search this cache index first. If the closest match is within your similarity threshold, return the cached response. Otherwise, proceed with the full RAG pipeline and cache the result afterwards.</p><p>Start with a similarity threshold of around 0.15 (cosine distance) and tune from there. Too strict and you&#8217;ll rarely get cache hits. Too loose and you&#8217;ll serve wrong answers for questions that are only loosely related.</p><p>Python developers can use <a href="https://fandf.co/4bjhBxF">RedisVL</a>&#8217;s <a href="https://fandf.co/4b1K7mn">SemanticCache</a> or <a href="https://fandf.co/4l1pjjg">LangCache</a> which handle much of this for you. In other languages, it&#8217;s straightforward to build yourself - it&#8217;s just a vector index with a similarity check.</p><p>Track your cache metrics: hit rate, average latency for cached versus uncached responses, and estimated cost savings. Store these counters in Redis using <code>INCR</code> so they persist across restarts. Expose them through a <code>/metrics</code> endpoint.</p><p><strong>Testing:</strong></p><ul><li><p>Ask the same question twice. The second time should be noticeably faster and your metrics should show a cache hit.</p></li><li><p>Rephrase the question slightly (e.g. &#8220;how does auth work?&#8221; then &#8220;what&#8217;s the authentication process?&#8221;). If your threshold is tuned correctly, the second should also be a cache hit.</p></li><li><p>Ask a completely different question and verify it&#8217;s a cache miss.</p></li><li><p>Check your <code>/metrics</code> endpoint to see hit rate and latency comparisons.</p></li><li><p>Add a cache flush endpoint and verify that clearing the cache causes previously cached queries to miss again.</p></li></ul><h3>Step 4</h3><p>In this step your goal is to add session memory so your assistant can handle follow-up questions within a conversation.</p><p>Without session memory, each question is treated in isolation. If a user asks &#8220;what&#8217;s the authentication flow?&#8221; and then follows up with &#8220;how do I refresh the token?&#8221;, the system has no idea what &#8220;the token&#8221; refers to. Session memory fixes this by maintaining conversation history.</p><p>Store conversation messages in Redis, keyed by session ID. Each time a user sends a message, append it to the session&#8217;s history. When building the prompt for the LLM, include the recent conversation messages so the model has context for follow-up questions. Redis lists or streams both work well for this.</p><p>Set a time-to-live on your sessions so they clean up automatically after a period of inactivity - 24 hours is a reasonable default.</p><p><strong>Testing:</strong></p><ul><li><p>Start a new session and ask a question about a specific topic in your docs.</p></li><li><p>Ask a follow-up question that relies on context from the first answer (e.g. use &#8220;it&#8221;, &#8220;that&#8221;, or &#8220;the same endpoint&#8221; to refer back). The assistant should understand what you&#8217;re referring to.</p></li><li><p>Start a different session and verify it has no memory of the first conversation.</p></li><li><p>Wait for the session TTL to expire (or set a short TTL for testing) and verify the session data is cleaned up from Redis.</p></li></ul><h3>Step 5</h3><p>In this step your goal is to add long-term agent memory so your assistant remembers user context across sessions and uses it to personalise answers.</p><p>Session memory disappears when a session ends. Long-term memory persists. If a user tells the assistant &#8220;I&#8217;m working on the payments microservice in Go&#8221; in one session, the assistant should remember that context in future sessions and tailor its answers accordingly.</p><p>Set up the <a href="https://fandf.co/4rHv9J9">Redis Agent Memory Server</a> as a Docker container alongside your Redis instance. The memory server provides a REST API for storing and searching memories, with built-in support for topic extraction, entity recognition, and semantic search over stored memories. It supports over 100 LLM providers via LiteLLM, so whatever model you&#8217;re using for your main application will work here too.</p><p>Integrate the memory server into your chat flow. After each conversation, extract any important context - user preferences, project details, technical decisions - and store it as a long-term memory. Before generating answers, search for relevant memories and include them in the prompt.</p><p>Your LLM prompt should now assemble context from three sources: document chunks from vector search, recent messages from the session, and relevant long-term memories. The memories help the assistant give more relevant answers - if the user has previously mentioned they use Python, documentation examples should lean towards Python where possible.</p><p><strong>Testing:</strong></p><ul><li><p>In one session, tell the assistant about your project context (e.g. &#8220;I&#8217;m building a payment service in Go&#8221;).</p></li><li><p>End the session and start a new one. Ask a general question. The assistant&#8217;s answer should reflect your project context even though it&#8217;s a new session.</p></li><li><p>Search the memory server&#8217;s REST API directly to verify memories were stored with the correct topics and entities.</p></li><li><p>Ask the assistant what it knows about your project - it should surface relevant stored memories.</p></li></ul><h3>Step 6</h3><p>In this step your goal is to add hybrid search and production hardening to make your system more robust and precise.</p><p>Pure vector search works well for general questions, but sometimes users want answers from a specific document or section. Hybrid search combines vector similarity with metadata filtering. For example, a user might ask &#8220;how does authentication work in the API reference?&#8221; - the vector search finds semantically relevant chunks, and the metadata filter narrows results to only the API reference document.</p><p>You can also use context from long-term memory to apply filters automatically. If the assistant knows the user is working on authentication, it can prioritise chunks from authentication-related sections without being asked.</p><p>Add conversation summarisation to handle long sessions gracefully. When the conversation history grows beyond a token threshold, summarise the older messages and keep only the recent ones intact. This prevents your context window from overflowing while preserving important information from earlier in the conversation.</p><p>Finally, make your system degrade gracefully when non-critical components fail. If the semantic cache is unavailable, skip it and call the LLM directly. If the memory server is down, answer without long-term context. Only the vector search and LLM are truly essential - everything else should fail silently with appropriate logging.</p><p><strong>Testing:</strong></p><ul><li><p>Ask a question scoped to a specific document (e.g. &#8220;based on the getting started guide, how do I...&#8221;). Verify the results come only from that document.</p></li><li><p>Have a long conversation (15+ messages) and verify the system still responds correctly as older messages get summarised.</p></li><li><p>Stop the memory server container and verify the chat still works, just without personalisation.</p></li><li><p>Stop and restart the semantic cache and verify the system recovers gracefully.</p></li><li><p>Check your observability metrics: response latency, cache hit rate, token usage, and estimated cost.</p></li></ul><h3>Going Further</h3><p>You&#8217;ve built a documentation assistant with RAG, semantic caching, and persistent memory. Here are some ways to push further:</p><ul><li><p><strong>Semantic routing:</strong> Classify incoming queries before processing them. Is it a documentation question, an off-topic chat, or a request for an action? Route each type differently.</p></li><li><p><strong>Multi-tenant support:</strong> Scope all indexes, caches, and memories by organisation or team using Redis key prefixes, so multiple teams can share one deployment.</p></li><li><p><strong>Document versioning:</strong> Track document versions and warn users when answers are based on outdated documentation.</p></li><li><p><strong>MCP integration:</strong> Expose your assistant as an MCP server so other AI agents can use it as a tool. The Agent Memory Server already supports MCP natively.</p></li><li><p><strong>Evaluation suite:</strong> Build a test harness that measures retrieval precision, answer accuracy, and cache effectiveness across a standard set of questions.</p></li><li><p><strong>Real-time updates:</strong> Use Redis Pub/Sub or Streams to notify a frontend when document ingestion completes or new memories are created.</p></li><li><p><strong>Multi-model strategy:</strong> Use a cheaper model for memory extraction and caching, and a more capable model for final answers. Redis doesn&#8217;t care which model generates the content it stores.</p></li><li><p>Web crawl: find and ingest documentation from the web.</p></li></ul><p>This coding challenge was sponsored by Redis.</p><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback is greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #109 - Ebook Reader]]></title><description><![CDATA[This challenge is to build your own ebook reader.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-109-ebook-reader</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-109-ebook-reader</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 28 Feb 2026 09:01:20 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2f6cd22c-19ff-46cb-b0fb-33cf52cddd10_2400x1800.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #19 - Ebook Reader</h2><p>This challenge is to build your own ebook reader application.</p><p>EPUB is the most widely used open standard for digital books. Unlike proprietary formats, EPUB is built on web technologies you probably already know; XHTML for content, CSS for styling, and a ZIP container to package it all together. Almost every ebook outside the Amazon Kindle ecosystem uses it. By building an EPUB reader, you&#8217;ll work with file format parsing, content rendering, text processing, and UI design all in one project.</p><p>But first, many thanks to this week&#8217;s sponsor, <a href="https://getunblocked.com/?utm_source=codingchallenges&amp;utm_medium=email&amp;utm_campaign=contextengine&amp;utm_content=260228_primary">Unblocked</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://getunblocked.com/?utm_source=codingchallenges&amp;utm_medium=email&amp;utm_campaign=contextengine&amp;utm_content=260228_primary" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UHKq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png 424w, https://substackcdn.com/image/fetch/$s_!UHKq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png 848w, https://substackcdn.com/image/fetch/$s_!UHKq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png 1272w, https://substackcdn.com/image/fetch/$s_!UHKq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UHKq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png" width="1200" height="600" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:600,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:307185,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:&quot;https://getunblocked.com/?utm_source=codingchallenges&amp;utm_medium=email&amp;utm_campaign=contextengine&amp;utm_content=260228_primary&quot;,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/189366587?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!UHKq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png 424w, https://substackcdn.com/image/fetch/$s_!UHKq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png 848w, https://substackcdn.com/image/fetch/$s_!UHKq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png 1272w, https://substackcdn.com/image/fetch/$s_!UHKq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3b338c-6cc5-47d3-bcde-f06c363ab8a4_1200x600.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Give your agents the understanding they need to generate reliable code, reviews, and answers. Unblocked builds context from your team&#8217;s code, PR history, conversations, documentation, planning tools, and runtime signals. It surfaces the insights that matter so AI outputs reflect how your system actually works.</p><p><em>&#8220;Unblocked has reversed my AI fatigue completely. The level of precision is wild.&#8221; - Senior developer, Clio</em></p><p><a href="https://getunblocked.com/?utm_source=codingchallenges&amp;utm_medium=email&amp;utm_campaign=contextengine&amp;utm_content=260228_primary">See how it works.</a></p><h2>The Challenge - Building Your Own EBook Reader</h2><p>In this coding challenge you&#8217;ll build an ebook reader that can open and display EPUB files, manage a library of books, and provide a comfortable reading experience with features like annotations, dictionary lookups, and customisable themes. By the end, you&#8217;ll have a fully functional reader that works with any standard EPUB file.</p><p>This is an advanced challenge that covers file format parsing, content rendering, persistent storage, and building a polished user interface. Choose a platform and tech stack that gives you good tools for displaying rich text content.</p><h3>Step Zero</h3><p>Set up your development environment and get some EPUB files to work with.</p><p>Pick a platform for your reader. It could be desktop, web, or mobile. Then a tech stack you&#8217;re comfortable building user interfaces with. You&#8217;ll need to handle ZIP archives, parse documents, render styled text, and build interactive UI elements, so make sure your chosen stack has good support for these.</p><p>For test content, grab a few free EPUB files from <a href="https://www.gutenberg.org/">Project Gutenberg</a> or <a href="https://standardebooks.org/">Standard Ebooks</a>. Standard Ebooks is particularly good because their files are well-formatted EPUB 3 files with proper styling. Get at least three or four books of different lengths so you can test properly throughout the challenge.</p><h3>Step 1</h3><p>In this step your goal is to open an EPUB file and display its content for reading. The <a href="https://www.w3.org/publishing/epub3/">EPUB file format</a> is documented on the W3 website.</p><p>An EPUB file is a ZIP archive containing XHTML content files, a CSS stylesheet, metadata, and a manifest that describes how everything fits together. Your reader needs to unpack this structure, understand the reading order from the manifest, and render each chapter as readable, styled text that you can scroll or page through.</p><p>You should be able to open an EPUB, see the book&#8217;s title and author, navigate between chapters using a table of contents, and read the content with the book&#8217;s own styling applied. The text should reflow cleanly when you resize the window.</p><p><strong>Testing guidance:</strong> Open one of your Project Gutenberg or Standard Ebooks files and verify you can read the full book from start to finish. Check that the table of contents works and that you can jump to any chapter. Try resizing the window and confirm the text reflows without breaking. Open a second, different book to make sure you&#8217;re not accidentally hardcoding anything specific to one file.</p><h3>Step 2</h3><p>In this step your goal is to manage a collection of ebooks and search across them.</p><p>Rather than opening one file at a time, your reader should maintain a library. Users should be able to import books, see them organised with their cover images and metadata (title, author, language), and sort or filter the collection. Creating a full-text search is where this gets interesting, index the actual content of every book in the library so users can search for a quote or topic and find it across all their books.</p><p><strong>Testing guidance:</strong> Import your collection of test EPUBs into the library. Verify that metadata displays correctly for each book. Search for a word or phrase you know appears in one of the books and confirm the search returns the right results with the book title and location in the text. Search for something that appears in multiple books and check that all matches are found. Try sorting and filtering the library by different criteria.</p><h3>Step 3</h3><p>In this step your goal is to give readers control over how their books look.</p><p>A good ebook reader lets you adjust fonts, font sizes, colours, margins, line spacing, and background colour. You&#8217;ll need a theming system that overrides the book&#8217;s built-in stylesheets without breaking them, applying user preferences on top of the author&#8217;s intended styling. Support at least light and dark themes, and let users adjust the core typography settings.</p><p><strong>Testing guidance:</strong> Open a book and switch between your light and dark themes. Verify the text remains readable in each. Change the font, font size, and line spacing and confirm the content reflows properly. Close the book and reopen it, your settings should persist. Open a different book with its own stylesheet and check that your theme still applies cleanly without breaking the book&#8217;s formatting (bold text, italics, headings should all still look right).</p><h3>Step 4</h3><p>In this step your goal is to build a layer on top of the rendered text that supports highlights, bookmarks, and notes.</p><p>Users should be able to select text and highlight it in different colours, attach written notes to any highlighted passage, and set bookmarks to return to later. All annotations need to persist &#8212; when you close the book and reopen it, everything should still be there in the right place. The tricky part is that EPUB text is reflowable, so your annotations need to be anchored to the text itself, not to a position on the screen.</p><p><strong>Testing guidance:</strong> Open a book, highlight several passages in different colours, and add notes to a couple of them. Set a few bookmarks. Close the book completely and reopen it &#8212; all your annotations should be exactly where you left them. Change the font size or window width to reflow the text and verify annotations stay attached to the correct passages. Check that you can view a list of all your bookmarks and annotations for the book.</p><h3>Step 5</h3><p>In this step your goal is to let readers look up any word instantly without leaving the book.</p><p>When a user selects a word, they should see a definition. For dictionary lookups, you&#8217;ll want this to work offline so it doesn&#8217;t break the reading flow. For additional context, provide a way to look up the selected word or phrase on Wikipedia using their public API. The lookup should appear as a clean overlay or popup that doesn&#8217;t lose the reader&#8217;s place.</p><p><strong>Testing guidance:</strong> Open a book and select a word &#8212; verify you get a dictionary definition quickly. Try selecting an uncommon word and check that it&#8217;s handled (either a definition or a clear &#8220;not found&#8221; message). Select a proper noun or topic and try the Wikipedia lookup, verify you get a relevant summary. Check that the lookup popup doesn&#8217;t disrupt your reading position, when you dismiss it, you should be right where you were.</p><h3>Step 6</h3><p>In this step your goal is to make your reader fully usable through keyboard navigation and screen readers.</p><p>Every feature in your reader should be reachable and operable without a mouse. The reading view, table of contents, library, annotations, and all controls should be keyboard-navigable with a logical tab order. For screen readers, all interactive elements need proper labels and roles so assistive technology can describe what&#8217;s on screen and what actions are available. This is one of those features that seems straightforward but requires careful attention to how your UI communicates with the accessibility layer of your platform.</p><p><strong>Testing guidance:</strong> Put your mouse away and try to use every feature of your reader using only the keyboard. Navigate the library, open a book, move between chapters, create a bookmark, and switch themes, all without clicking. Then test with a screen reader (VoiceOver on macOS, NVDA on Windows, Orca on Linux). Verify that the screen reader announces book titles, chapter names, and button labels correctly. Check that the reading content is announced in the proper order.</p><h3>Going Further</h3><p>Once you&#8217;ve built the core ebook reader, here are some ways to extend it:</p><ul><li><p><strong>Text-to-Speech with Sentence Highlighting</strong>: Add narration that highlights the current sentence as it reads aloud. The interesting part is synchronising the audio timing with text positions so the highlight tracks smoothly. Handle pausing, resuming, and adjusting playback speed.</p></li><li><p><strong>OPDS and Calibre Integration</strong>: OPDS is an open catalog protocol for discovering and downloading ebooks. Build a catalog browser that connects to OPDS feeds and local Calibre libraries on your network. It&#8217;s a good exercise in protocol parsing and network discovery without needing cloud infrastructure.</p></li><li><p><strong>PDF Support</strong>: Extend your rendering engine to handle PDF files alongside EPUB. PDF is a fixed-layout format, so the rendering approach is quite different from reflowable EPUB &#8212; a worthwhile challenge in its own right.</p></li><li><p><strong>Reading Position Sync</strong>: If you&#8217;ve built a web-based reader, add the ability to sync reading positions across devices. This involves persisting state to a server and handling conflicts when the same book is read on multiple devices.</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #108 - Online Coding Playground]]></title><description><![CDATA[This challenge is to build your own online coding playground.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-108-online-coding</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-108-online-coding</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 21 Feb 2026 09:01:33 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/cc110ee6-b37d-4214-a23c-90db50c49dd0_2400x1350.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #108 - Online Python Playground</h2><p>This challenge is to build your own online code playground where users can write and run Python code directly in their web browser.</p><p>An online code playground is a web-based environment where developers can experiment with code without needing to install anything on their computer. Think of it like a digital sandbox, you can write code, click a button, and see the results instantly. It&#8217;s incredibly useful for learning, testing ideas quickly, or sharing code snippets with others.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get access to a monthly AMA and 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Your Own Online Code Playground</h2><p>You&#8217;re going to build an online code playground for Python (or another programming language of your choice). Users will be able to write code in an editor, run it with the click of a button, and see the output or errors displayed immediately. Over several steps, you&#8217;ll add features like saving code snippets, syntax highlighting, error messages with line numbers, and performance safeguards to keep the browser responsive.</p><h3>Step Zero</h3><p>Set up your development environment and get familiar with the technologies you&#8217;ll be using.</p><p>You&#8217;ll need to choose a tech stack that works well for building web applications with a rich code editor. You&#8217;ll be using a Python WebAssembly runtime (like Pyodide or PyScript) to execute Python code in the browser, so make sure you understand how it works before you begin.</p><h3>Step 1</h3><p>In this step, your goal is to execute Python code in the browser and display the output to the user.</p><p>You&#8217;ll create a simple HTML page with a code editor (a basic textarea is fine for now) and a &#8220;Run&#8221; button. When the user clicks the button, take the code from the editor, send it to the Python WASM runtime, and display whatever the code prints to standard output in an output panel below.</p><p><strong>Testing:</strong> Write a simple Python script that prints &#8220;Hello, World!&#8221; and verify it displays correctly. Try a few more examples: print multiple lines, do some math (<code>print(2 + 2)</code>), and test with variables.</p><h3>Step 2</h3><p>In this step, your goal is to catch and display Python errors in a user-friendly way.</p><p>When code fails to run (syntax errors, runtime errors, etc.), the user should see a clear error message that tells them what went wrong and where. Include the line number and the error traceback so they can debug their code.</p><p><strong>Testing:</strong> Deliberately write broken code, missing colons, undefined variables, division by zero, and verify that each error is clearly shown with line numbers and helpful messages.</p><h3>Step 3</h3><p>In this step, your goal is to replace the basic textarea with a proper code editor that highlights Python syntax.</p><p>Your users will write code more efficiently and catch mistakes faster if the syntax is color-coded. You can use an existing code editor library (like CodeMirror or Monaco Editor). The editor should support undo/redo, copy/paste, and basic keyboard shortcuts.</p><p><strong>Testing:</strong> Type Python code into the editor and verify that keywords (like <code>def</code>, <code>if</code>, <code>for</code>) appear in different colours. Test undo/redo, and make sure basic shortcuts like Ctrl+Z or Cmd+Z work.</p><h3>Step 4</h3><p>In this step, your goal is to let users save their code snippets to the browser&#8217;s local storage and load them again later.</p><p>Users should be able to save their current code with a name, see a list of their saved snippets, and load any snippet back into the editor. They should also be able to delete saved snippets they no longer need.</p><p><strong>Testing:</strong> Write some code, save it with a descriptive name, close the browser tab (or reload the page), and verify the code is still there when you come back. Create multiple snippets and switch between them.</p><h3>Step 5</h3><p>In this step, your goal is to prevent long-running code from freezing the browser and provide users with feedback about what&#8217;s happening.</p><p>Add a timeout for code execution&#8212;if code takes too long to run, stop it and show a message. Display a loading indicator while code is executing. Optionally, show memory usage warnings if code is consuming too much resources.</p><p><strong>Testing:</strong> Write code that runs for a long time (like an infinite loop or a deep recursion). Verify it stops after a reasonable timeout and the user sees a clear message. Test that the loading indicator appears and disappears at the right times.</p><h3>Step 6</h3><p>In this step, your goal is to handle edge cases gracefully so your playground doesn&#8217;t break.</p><p>Handle situations where the Python WASM runtime fails to initialise, the browser doesn&#8217;t support WebAssembly, or the user&#8217;s browser runs out of localStorage space. Show helpful messages in each case so users know what&#8217;s happening.</p><p><strong>Testing:</strong> Try disabling WebAssembly in your browser to test the fallback message. Fill up localStorage and try to save a large code snippet, and verify a clear error message appears.</p><h3>Going Further</h3><p>Once you&#8217;ve built the basic playground, consider adding more advanced features:</p><ul><li><p><strong>Autocomplete and code hints</strong> - Help users discover Python functions and methods as they type</p></li><li><p><strong>Multiple files</strong> - Let users create projects with multiple Python files</p></li><li><p><strong>Import libraries</strong> - Allow users to import common Python libraries (many are available in Pyodide)</p></li><li><p><strong>Share snippets</strong> - Generate a link that someone else can click to load and run your code</p></li><li><p><strong>Dark mode</strong> - Add a theme toggle so users can write code comfortably at night</p></li><li><p><strong>Keyboard shortcuts guide</strong> - Show users what shortcuts are available</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenges - Does AI Write Good Code? Let's Find Out.]]></title><description><![CDATA[Evaluating AI-Generated Code with the SonarQube MCP Server.]]></description><link>https://codingchallenges.substack.com/p/coding-challenges-does-ai-write-good</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenges-does-ai-write-good</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 14 Feb 2026 09:01:44 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a9cb2faa-d339-41d6-9d90-7f4fbba02b00_1600x896.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi, this is John with this week&#8217;s Coding Challenge and it&#8217;s going to be a bit different.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h1>Coding Challenge - Does AI Write Good Code? </h1><p>AI is changing software engineering. AI can write code faster than you or I can. That's exciting, but it creates a new problem: just because code works doesn't mean it's good. How do you know if what your LLM generated is secure, maintainable, and ready for production?</p><p>There are two things you can do. Firstly, follow industry research, like <strong><a href="https://fandf.co/4qzxhBe">Sonar&#8217;s LLM Leaderboard</a></strong>, which looks at the quality, security, complexity, and maintainability of the code created using the leading LLMs. It&#8217;s well worth a read to understand the strengths and weaknesses of the models. I found it particularly eye-opening to see that GPT 5.2 High generates 50% more code than Opus 4.5 for the same tasks and Opus 4.5 was still generating around 200% more code than Gemini 3 Pro! I know which codebase I&#8217;d rather be responsible for!</p><p>Secondly, there are many tools we can leverage to evaluate aspects of code quality, maintainability and security. They include compilers, type checkers, linters, and automated code review tools like SonarQube. In today&#8217;s Coding Challenge we&#8217;re going to look at how we can leverage them to guide and evaluate AI when building software.</p><h2>Step Zero</h2><p>In this step your goal is to pick a <a href="https://codingchallenges.fyi/challenges/intro/">Coding Challenge</a>, technology stack and AI coding agent of your choice. If you primarily use <a href="https://github.com/features/copilot/cli">Copilot</a> at work, consider trying <a href="https://ampcode.com/">Amp Code</a>, if you mainly use <a href="https://code.claude.com/docs/en/overview">Claude</a>, try Copilot. In short, try a different coding agent and learn something new.</p><h2>Step 1</h2><p>In this step your goal is to build a solution to one of the <a href="https://codingchallenges.fyi/challenges/intro/">Coding Challenges</a> using your favourite agent / LLM. I&#8217;ll go into more detail on how to leverage AI agents in a future newsletter, but for now I suggest prompting the agent to tackle one step of the Coding Challenge at a time. Between steps, or if the context window starts to fill up or it hallucinates, clear the context window.</p><p>Once your solution is complete, head to step 2 to start leveraging tools to assess the quality and security of the code produced by your AI.</p><h2>Step 2</h2><p>In this step your goal is to prompt your agent to review the code quality using the compiler, code formatter and linter appropriate to your programming language and stack.</p><p>For example, if you&#8217;re using Python run checks with <code>ruff</code>, <code>ty</code>, <code>pyrefly</code> or <code>pyright</code>. If you&#8217;re using JavaScript, switch to TypeScript &#128519;. If you&#8217;re using Rust, use <code>clippy</code>, for Go check out Golangci-lint. You get the idea.</p><h2>Step 3</h2><p>In this step your goal is to install the SonarQube MCP Server and use it to evaluate the code created by your agent. </p><p>SonarQube is an automate code review solution that performs deep code analysis to detect bugs, security vulnerabilities, and quality issues that simple linters miss. It can also be used to enforce rules for maintainability, reliability, and security before merging. It&#8217;s been widely used in the organisations I&#8217;ve worked for and I&#8217;ve often been grateful for the feedback it provided my teams. So for this step you&#8217;re going to use it, via the MCP server to analyse the code your agent has produced.</p><p>To get started, see Sonar&#8217;s instructions on how to install their <a href="https://fandf.co/4kEHrPV">MCP server</a>. I found the <a href="https://fandf.co/4qDwUWz">quickstart guide</a> particularly useful and chose to use the container running in Docker. Then configured my agent to use SonarQube via the MCP server.</p><p>Once you have it installed, check with your agent to verify the MCP server is configured. For example, with Copilot run:</p><pre><code>/mcp show</code></pre><p>You should then see some output that includes something like this:</p><pre><code><code>Configured MCP servers: sonarqube

MCP Server Configuration:
  sonarqube (local): Command: docker</code></code></pre><p>Once you have that done, ask your agent to review the project with SonarQube, for example:</p><pre><code>Review this code with SonarQube</code></pre><p>I tried it on the Monkey compiler project I did last year and got the following output:</p><pre><code>Total Issues Found: 40

  Issue Breakdown by Severity:

   - CRITICAL: 37 issues
   - MAJOR: 1 issue
   - MINOR: 2 issues

  Key Issues Identified:

  1. High Cognitive Complexity (6 issues):

   - compiler/compiler.go:72 - Complexity: 96 (allowed: 15)
   - vm/vm.go:72 - Complexity: 69 (allowed: 15)
   - vm/vm_test.go:66 - Complexity: 34 (allowed: 15)
   - evaluator/evaluator.go:15 - Complexity: 25 (allowed: 15)
   - compiler/symbol_table_test.go:166 - Complexity: 16 (allowed: 15)
   - compiler/compiler_test.go:649 - Complexity: 16 (allowed: 15)

  2. Duplicated String Literals (14 issues): Various test files contain repeated error message strings that should be defined as
  constants.

  3. Empty Functions (13 issues): Multiple empty functions in ast/ast.go lack explanatory comments about why they're intentionally
  empty.

  4. Other Issues:

   - Empty code block in compiler/compiler_test.go:661
   - Interface naming convention in object/object.go:72
   - Parameter grouping in compiler/compiler.go:353

  The most critical issues are the high cognitive complexity in the compiler and VM modules, which could impact maintainability.</code></pre><p> You can then use the agent to explore the issues in more depth.</p><h2>Step 4</h2><p>In this step your goal is to pick out three of the points that SonarQube highlights and investigate them. Review the code yourself and either fix it, or prompt your agent to fix the code, including the output from SonarQube in your prompt.</p><p>I did this by prompting the agent for the highest priority issues:</p><pre><code>Show me the three highest priority issues</code></pre><p>Which gave me:</p><pre><code>Top 3 Highest Priority Issues

  1. compiler/compiler.go:72 - Cognitive Complexity: 96 &#9888;&#65039; CRITICAL

  Function: Compile(node ast.Node)

  Problem: This massive switch statement handles all AST node compilation with deeply nested conditionals and error handling. The complexity is 6.4x over the allowed limit (96
  vs 15).

  Why it matters: This is the core compiler function - extremely difficult to understand, test, and modify. Each new language feature adds more nesting.

  Recommendation: Refactor using the strategy pattern - extract each case into separate compilation methods:

   func (c *Compiler) compileProgram(node *ast.Program) error { ... }
   func (c *Compiler) compileInfixExpression(node *ast.InfixExpression) error { ... }
   func (c *Compiler) compileIfExpression(node *ast.IfExpression) error { ... }

  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  2. vm/vm.go:72 - Cognitive Complexity: 69 &#9888;&#65039; CRITICAL

  Function: Run()

  Problem: Main VM execution loop with a giant switch statement for all opcodes. Complexity is 4.6x over the limit (69 vs 15).

  Why it matters: This is the VM's heart - any bug here affects runtime execution. The nested switch cases, error checks, and instruction pointer manipulation make it hard to
  reason about.

  Recommendation: Extract opcode handlers into separate methods:

   func (vm *VM) handleOpConstant(ins code.Instructions, ip int) error { ... }
   func (vm *VM) handleOpJump(ins code.Instructions, ip int) error { ... }

  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  3. vm/vm_test.go:66 - Cognitive Complexity: 34 &#9888;&#65039; CRITICAL

  Function: Test helper function

  Problem: Test code with complexity 2.3x over the limit (34 vs 15), likely due to nested loops validating complex test cases.

  Why it matters: Complex test code is harder to maintain and can hide bugs in the tests themselves.

  Recommendation: Break into smaller helper functions for specific validation types (integers, booleans, arrays, etc.).

Impact: These three functions represent the core of your interpreter - the compiler, VM, and tests. High complexity makes maintenance risky and onboarding difficult.</code></pre><p>Look at the issues you find and either fix them by hand or have the agent fix them. Then use the agent and SonarQube to review the fixes, it&#8217;s a great way to improve your code quality.</p><h2>Step 5</h2><p>In this step your goal is to reflect on how to use LLMs and how tools like compilers, linters and SonarQube can help you improve the quality of the software you build.</p><p>I recommend adding them to your normal agentic coding process, for me that means incorporating them into the prompt for each task, something like this in my prompts:</p><pre><code>After reading the specification: 
1. Create a set of tests to verify the implementation behaves correctly. 
2. Then create the code required to meet the specification. 
3. Verify the functionality is correct using the tests.
4. Verify the code lints and passes quality checks with no warnings or errors.</code></pre><p>My AGENTS.md usually defines how to run the linter and quality checks for the project.</p><h2>Going Further</h2><p>Review the <strong><a href="https://fandf.co/4qzxhBe">LLM Leaderboard</a></strong> that Sonar created to provide transparency into how models build code, not just what they build. By running thousands of AI-generated solutions through SonarQube, they evaluated the models on the metrics that matter to engineering leaders: security, reliability, maintainability, and complexity.</p><p>To generate the leaderboard, Sonar analysed code quality from leading AI models (GPT-5.2 High, GPT-5.1 High, Gemini 3 Pro, Opus 4.5 Thinking, and Claude Sonnet 4.5). </p><p>It was interesting to see that while these models pass functional benchmarks well, they have significant differences in code quality, security, and maintainability.</p><p>Higher performing models tend to generate more verbose and complex code for example:</p><ul><li><p><strong>Opus 4.5 Thinking</strong> leads with 83.62% pass rate but generates 639,465 lines of code (more than double the less verbose models).</p></li><li><p><strong>Gemini 3 Pro</strong> achieves similar performance (81.72%) with much lower complexity and verbosity.</p></li><li><p><strong>GPT-5.2 High</strong> hits 80.66% pass rate but produces the most code (974,379 lines) and shows worse maintainability than GPT-5.1.</p></li></ul><p>I found it particularly interesting to see that Gemini produced only 289k lines. That&#8217;s a lot less code to review and maintain!</p><p>Many thanks to Sonar for sponsoring this issue of Coding Challenges.</p><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> GitHub repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p><p></p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #107 - Loom Clone]]></title><description><![CDATA[This challenge is to build your own personal version of Loom.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-107-loom-clone</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-107-loom-clone</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 07 Feb 2026 09:02:14 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8c947281-d90a-4ec6-8b91-d91d0ea7d0cc_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #107 - Loom Clone</h2><p>This challenge is to build your own version of <a href="https://www.loom.com/">Loom</a>, a screen recording and video messaging tool.</p><p>Loom is a popular tool for creating quick video messages, tutorials, and screen recordings. It&#8217;s simple to use - you select what you want to record (screen, camera, microphone), click record, and it saves your video. This challenge will have you building the core features of Loom, giving you hands-on experience with media capture APIs, file handling, and building a user-friendly recording interface.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get access to a monthly AMA and 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building Loom</h2><p>You&#8217;ll be building a simplified version of Loom that lets users record their screen, camera, and microphone, then save and playback those recordings. The key is to create an intuitive interface for setting up the recording, managing devices, and reviewing what you&#8217;ve captured.</p><p>You can build it as a web based application like Loom itself, or a desktop application.</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</p><p>Choose your target platform and programming language. Loom is cross-platform, so you could build this as a web application, a desktop app using Electron or a similar framework, or a native application. Pick a tech stack that you&#8217;re comfortable with and that has good support for media capture and file handling.</p><h3>Step 1</h3><p>In this step your goal is to create the home page of your application.</p><p>Your home page should have a simple, clean interface that serves as the starting point for users. The main feature here is the ability to select and confirm a folder where recordings will be saved. Users should be able to browse and select a destination folder before they start recording.</p><p>Test your implementation by starting your application and verifying that you can select a folder on your system and see that selection reflected in the UI.</p><h3>Step 2</h3><p>In this step your goal is to let users select which microphone and camera devices they want to use for recording.</p><p>Before recording begins, users need to choose their audio input device (microphone) and video input device (camera). Your application should detect available devices on the system and present them in a dropdown or list. Users might have multiple microphones or cameras, so make sure they can see all available options and select the ones they want.</p><p>Test your implementation by checking that your application detects the devices available on your system. Verify that you can list them and select different ones.</p><h3>Step 3</h3><p>In this step your goal is to add the ability to mute or unmute the microphone and enable or disable the camera before and during recording.</p><p>Users often want to toggle their microphone or camera on and off - maybe they want to record their screen but not their voice, or show their screen but not their face. Add toggle buttons or checkboxes so users can control whether the microphone and camera are active. These controls should be available both before recording starts and while recording is in progress.</p><p>Test your implementation by toggling the microphone and camera on and off, verifying that the UI reflects the current state.</p><h3>Step 4</h3><p>In this step your goal is to implement the core recording functionality - starting and stopping a recording, and saving it to the folder the user selected.</p><p>Add a prominent button to start recording. Once recording begins, capture the audio from the selected microphone and video from the selected camera. Users should be able to stop the recording at any time. When they stop, the application should save the complete recording to the folder they selected in Step 1. The file should have a name or timestamp so users can identify it later.</p><p>Test your implementation by starting a recording, speaking or moving in front of the camera, stopping the recording, and verifying that a file has been created in your selected folder.</p><h3>Step 5</h3><p>In this step your goal is to add screen sharing and screen recording capabilities.</p><p>Extend your application so users can choose to record their screen instead of (or in addition to) the camera. Add a toggle or option to enable screen sharing mode. When enabled, users should be able to select which screen or window to record. This is in addition to the microphone and camera options - they should still be able to record audio and camera video alongside their screen. Ensure the shared screen is the main focus of the video.</p><p>Test your implementation by enabling screen recording, selecting a screen or window to record, and verifying that the screen content is captured when you record.</p><h3>Step 6</h3><p>In this step your goal is to display a list of previous recordings on the home page and allow users to play them back.</p><p>After users have made at least one recording, the home page should display a list of all recordings saved in the selected folder. Users should be able to click on a recording to play it back. The playback should handle both audio and video content, and work seamlessly with whatever format you chose to save recordings in.</p><p>Test your implementation by making a few recordings, returning to the home page, and verifying that all recordings appear in the list. Click on each one and verify that playback works correctly.</p><h3>Going Further</h3><p>Here are some ways you could extend this project:</p><ul><li><p><strong>Recording preview</strong>: Add a preview window so users can see themselves or their screen before they start recording.</p></li><li><p><strong>Trimming and editing</strong>: Allow users to trim recordings to remove unwanted sections at the beginning or end.</p></li><li><p><strong>Multiple simultaneous captures</strong>: Let users record screen + camera + microphone all at once and composite them into a single video.</p></li><li><p><strong>Recording metadata</strong>: Store additional information with each recording like creation date, duration, and custom notes.</p></li><li><p><strong>Share functionality</strong>: Add the ability to export recordings in different formats or upload them to cloud storage.</p></li><li><p><strong>Keyboard shortcuts</strong>: Add keyboard shortcuts for common actions like start/stop recording, mute microphone, etc.</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Will AI Kill Coding?]]></title><description><![CDATA[What is the future of software engineering?]]></description><link>https://codingchallenges.substack.com/p/will-ai-kill-coding</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/will-ai-kill-coding</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 31 Jan 2026 09:01:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/6bece9ff-2e91-4075-ad8f-e2d00383955c_1424x752.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Some people claim that AI is already writing 90%+ of our code. Others tell us it will be writing 100% of it with six months.</p><p></p><p><strong>They&#8217;re both wrong</strong>. But software engineering is changing. AI and AI-assisted coding is behind the change.</p><p>That means Coding Challenges has to change too.</p><p>I&#8217;d love your feedback on what you would like to see in this newsletter. Is it more coding challenges, more coding tutorials or more focus on how to change with the industry and leverage AI?</p><p>Hit reply, or comment on Substack and let me know. Thanks!</p><p></p><p>If you&#8217;re skeptical about AI and don&#8217;t think it can complete a specific coding task, I am on the hunt for challenges to put to AI. You can find full details and submit your challenge on the <a href="https://github.com/JohnCrickett/ai-coding-challenges">AI Coding Challenges GitHub repo</a>.</p><p>Regards, John</p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #106 - JSON Validator And Prettier]]></title><description><![CDATA[This challenge is to build your own JSON validation and prettier tool.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-106-json-validator</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-106-json-validator</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 24 Jan 2026 09:01:33 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7f824e71-fc12-4242-8e63-e0e592fc3448_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Hi this is John with this week&#8217;s Coding Challenge.</em></p><p>&#128591; <em>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</em></p><p><em>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</em></p><h2>Coding Challenge #106 - JSON Validator And Prettier</h2><p>This challenge is to build a JSON validator and formatter. JSON is everywhere, in APIs, configuration files, and data exchange. If you&#8217;re a software engineer it&#8217;s hard to avoid.</p><p>Sometimes that also means you have to cope with broken or unreadable large globs of JSON, as a result I&#8217;ve often found myself using JSON linters or prettiers. They&#8217;re simple tools but incredibly useful for anyone working with JSON data. This Coding Challenge is to build one.</p><h3>If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039;, with the bonus that you get access to a monthly AMA and 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my self-paced <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Join one of my live courses where I personally teach you <a href="https://codingchallenges.fyi/live-courses/learn-go">Go by building five of the coding challenges</a> or <a href="https://codingchallenges.fyi/live-courses/redis">systems software development by building a Redis</a> clone.</p></li></ol><h2>The Challenge - Building a JSON Validator and Formatter</h2><p>You&#8217;re going to build an application that lets users paste in some JSON, check if it&#8217;s valid, and format it in a useful way. The tool will parse JSON, validate its structure, and provide useful transformations.</p><p>You can build it as a web application, desktop application or following the trend in CLI tools that is seeing a resurgence because of CLI based AI agents, a CLI tool. It&#8217;s your project, your choice!</p><h3>Step Zero</h3><p>In this introductory step you&#8217;re going to set up your development environment and create the basic project structure.</p><p>Choose your target platform and programming language.</p><h3>Step 1</h3><p>In this step your goal is to build the initial user interface for the JSON tool.</p><p>Create a UI with a large text input box where users can paste JSON into. The interface should be clean and responsive. Think about how to display error messages when validation fails - users need to know what went wrong and where.</p><p>At the end of this step you should have a UI that the user can enter JSON into, by either typing or pasting the JSON in.</p><h3>Step 2</h3><p>In this step your goal is to build the JSON validation and formatting.</p><p>Write the code to parse the input string as JSON (if you haven&#8217;t before, now might be a good time to do the <a href="https://codingchallenges.fyi/challenges/challenge-json-parser">JSON parser</a> project) and detect whether it is valid. When the user clicks the validate button, your tool should check the JSON syntax. If the JSON is valid, display it in a nicely formatted way with consistent indentation so the structure is easy to read.</p><p>If the JSON is invalid, highlight the error with a clear message about what went wrong and where (line number, character position, or a descriptive message). Seeing well-formatted JSON when validation succeeds helps users understand their data structure and spot issues visually.</p><p>Test it with valid JSON:, for example</p><pre><code><code>{"name": "John", "job": "software engineer", "country": "United Kingdom"}</code></code></pre><p>When validated, it should display it nicely formatted:</p><pre><code><code>{
  "name": "John",
  "job": "software engineer",
  "country": "United Kingdom"
}</code></code></pre><p>Test with invalid JSON (for example a missing comma):</p><pre><code><code>{"name": "John" "job": "software engineer"}</code></code></pre><p>Verify that the tool shows an appropriate error message rather than formatted output.</p><p>Test with invalid JSON (trailing comma):</p><pre><code><code>{"name": "John",  "job": "software engineer",}</code></code></pre><p>Again verify the error is clearly identified. Test that minified valid JSON gets properly formatted when validated.</p><h3>Step 3</h3><p>In this step your goal is to implement a sort feature. Write code that takes valid JSON and reorganises it so that keys are sorted alphabetically at each level of the object.</p><p>For example, if the input has keys in the order [city, age, name], the sorted output should have them as [age, city, name]. The sorting should apply independently at each nesting level - if you have nested objects, each one gets sorted by its own keys.</p><p><strong>Testing:</strong> Test with an unsorted object:</p><pre><code><code>{"zebra": 1, "apple": 2, "banana": 3}</code></code></pre><p>The output should be:</p><pre><code><code>{"apple": 2, "banana": 3, "zebra": 1}</code></code></pre><p>Test with nested objects:</p><pre><code><code>{"z": {"b": 1, "a": 2}, "a": {"y": 3, "x": 4}}</code></code></pre><p>The output should sort both the top level and each nested object:</p><pre><code><code>{"a": {"x": 4, "y": 3}, "z": {"a": 2, "b": 1}}</code></code></pre><h3>Step 4</h3><p>In this step your goal is to implement the compress feature.</p><p>Write code that removes all non-essential whitespace from the JSON while preserving any whitespace that appears inside string values. This means removing spaces, newlines, and tabs between structural elements like braces, brackets, and commas, but leaving the content of strings unchanged.</p><p><strong>Testing:</strong> Test with formatted JSON:</p><pre><code><code>{
  "name": "John",
  "message": "Hello World"
}</code></code></pre><p>The compressed output should be:</p><pre><code><code>{"name":"John","message":"Hello World"}</code></code></pre><p>Note that the space in the string &#8220;Hello World&#8221; is preserved. Don&#8217;t forget to test with newlines in strings too.</p><p>Ensure that it is possible to return to the prettier version with the validate button.</p><h3>Step 5</h3><p>In this step your goal is to implement a JSON to YAML converter.</p><p>Write code that takes valid JSON and converts it to YAML format. Your converter should handle objects, arrays, strings, numbers, booleans, and null values, translating the JSON structure into proper YAML syntax.</p><p><strong>Testing:</strong> Test with a simple object:</p><pre><code><code>{"name": "John", "age": 30}</code></code></pre><p>The output should be:</p><pre><code><code>name: John
age: 30</code></code></pre><p>Test with nested objects and arrays:</p><pre><code><code>{"person": {"name": "John", "hobbies": ["reading", "coding"]}}</code></code></pre><p>The output should be:</p><pre><code><code>person:
  name: John
  hobbies:
    - reading
    - coding</code></code></pre><p>Test with various data types:</p><pre><code><code>{"active": true, "count": 0, "message": null, "score": 9.5}</code></code></pre><p>The output should be:</p><pre><code><code>active: true
count: 0
message: null
score: 9.5</code></code></pre><p>Verify that your converter handles indentation correctly and produces valid YAML.</p><h3>Going Further</h3><p>You can take this further by:</p><ul><li><p>Add copy-to-clipboard functionality so users can quickly copy the result.</p></li><li><p>Making the formatting configurable.</p></li><li><p>Create a dark mode for the interface.</p></li></ul><h3>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</h3><ol><li><p>Refer a friend or colleague to the newsletter. &#128591;</p></li><li><p>Sign up for a <a href="https://codingchallenges.substack.com/subscribe">paid subscription</a> - think of it as buying me a coffee &#9749;&#65039; twice a month, with the bonus that you also get 20% off any of my <a href="https://shop.codingchallenges.fyi/">courses</a>.</p></li><li><p>Buy one of my <a href="https://shop.codingchallenges.fyi/">courses</a> that walk you through a Coding Challenge.</p></li><li><p>Subscribe to the <a href="https://www.youtube.com/@codingchallengesfyi">Coding Challenges YouTube channel</a>!</p></li></ol><h3>Share Your Solutions!</h3><p>If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know via <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a> or <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="https://github.com/CodingChallengesFYI/SharedSolutions">Coding Challenges Shared Solutions</a> Github repo</p><h3>Request for Feedback</h3><p>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback greatly appreciated.</p><p>You can reach me on <a href="https://bsky.app/profile/johncrickett.bsky.social">Bluesky</a>, <a href="https://www.linkedin.com/in/johncrickett/">LinkedIn</a> or through <a href="https://codingchallenges.substack.com/">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item></channel></rss>