<?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>Tue, 11 Aug 2026 21:51:07 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 #130 - Sort Visualiser]]></title><description><![CDATA[This challenge is to build your own tool to visualise how sorting algorithms work.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-130-sort-visualiser</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-130-sort-visualiser</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 08 Aug 2026 08:01:19 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2898b4f0-3234-4c8a-90be-f05ef24f353d_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 #130 - Sort Visualiser</h2><p>This challenge is to build your own sorting algorithm visualiser, a tool that animates how common sorting algorithms operate on an array of values.</p><p>Sorting is one of the first topics every programmer meets, and it&#8217;s a topic many of us never properly <em>see</em>. We learn that quick sort is faster than bubble sort, that merge sort divides and conquers, that heap sort uses a clever tree structure, but the names blur together until you watch them work.</p><p>A visualiser turns each algorithm into a little dance: bars or numbers shuffling, settling, and finally clicking into order. Once you&#8217;ve watched insertion sort sweep across a nearly-sorted array, or seen quick sort partition a chaotic mess into neatly halved buckets, the differences between the algorithms become intuitive in a way no Big-O table can match. Building the visualiser yourself is even better, you&#8217;ll gain a deep, hands-on understanding of eight classic algorithms and pick up some practical skills around UI rendering, animation timing, and clean abstractions along the way.</p><h1><strong>Win &#128176;&#128176;&#128176; - Join The Ready, Spec, Ship Hackathon I&#8217;m Running!</strong></h1><p>I&#8217;m pleased to be hosting the <strong><a href="https://codingagents.fyi/hackathon/kiro/">Ready, Spec, Ship Hackathon</a></strong> alongside Angie Jones, and<span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Gregor Ojstersek&quot;,&quot;id&quot;:106098672,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!TiaG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b7fdc30-d8c4-45f2-b0df-0b60baf9d4f4_1000x1000.jpeg&quot;,&quot;uuid&quot;:&quot;351c044e-2d92-4768-8e7b-a328fadbfa87&quot;}" data-component-name="MentionToDOM"></span>; and sponsored by <a href="https://kiro.dev/">Kiro</a>. You can enter solo or as a team of up to three people, and you&#8217;re welcome to submit more than one project.</p><p>There&#8217;s a <strong>$9,600</strong> prize pool to be won, plus 2000 FREE Kiro credits for every verified entry. Submissions close on 23 August, with the winners announced on 6 September.</p><p>Find out more and take part:<strong><a href="https://codingagents.fyi/hackathon/kiro/">https://codingagents.fyi/hackathon/kiro/</a></strong></p><p>It&#8217;s a great chance to checkout the Kiro agentic coding environment!</p><p>OK, on with the coding challenge!</p><h2>The Challenge - Building Your Own Sorting Visualiser</h2><p>In this challenge you&#8217;re going to build a tool that animates a sorting algorithm at work on an array of values. By the time you&#8217;re done, your tool will support eight classic algorithms: bubble, insertion, selection, merge, quick, heap, shell, and radix. And let the user pick the algorithm, the sample size, the initial order of the data, the display mode, and the animation speed.</p><p>Something like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!24ZO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!24ZO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png 424w, https://substackcdn.com/image/fetch/$s_!24ZO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png 848w, https://substackcdn.com/image/fetch/$s_!24ZO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png 1272w, https://substackcdn.com/image/fetch/$s_!24ZO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!24ZO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png" width="1450" height="931" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:931,&quot;width&quot;:1450,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:157113,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/210227004?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!24ZO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png 424w, https://substackcdn.com/image/fetch/$s_!24ZO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png 848w, https://substackcdn.com/image/fetch/$s_!24ZO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png 1272w, https://substackcdn.com/image/fetch/$s_!24ZO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b5d6db9-1a15-47a6-983c-9b6b1e862adb_1450x931.png 1456w" sizes="100vw" loading="lazy"></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 aria-hidden="true" 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><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>You can pick the platform that suits you best. A terminal/CLI implementation (using something like ncurses, blessed, or a similar library) is a great fit if you want to stay close to the keyboard. A browser/web implementation gives you the most flexibility for visuals and is easy to share. A native desktop GUI (using something like Qt, Tauri, GTK, or your language&#8217;s standard toolkit) is somewhere in between. Pick the one that interests you every step in this challenge applies to all three.</p><p>A small note before we start: you&#8217;ll be tempted to just run the algorithm and update the screen at each iteration. That works at first, but quickly tangles your sorting logic with your rendering logic. We&#8217;ll structure things so that algorithms produce a stream of events (compared these two indices, wrote this value to that index) and the renderer consumes them. That separation is what&#8217;ll make adding new algorithms and new display modes painless later on.</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, terminal/CLI, browser/web, or native desktop GUI, and the programming language to go with it. I&#8217;d encourage you to pick a stack you&#8217;re comfortable with for both UI rendering and handling user input, because you&#8217;ll be doing plenty of both.</p><p>Have a think about how you&#8217;ll handle the animation loop. Most platforms have a natural answer, a render loop tied to the frame rate in a browser or a GUI toolkit, a timed redraw in a terminal. Whichever route you take, you&#8217;ll want a way to do work, redraw, and pause for a configurable interval between steps.</p><h3>Step 1</h3><p>In this step your goal is to render an array of values on screen and watch a single sorting algorithm run end-to-end.</p><p>Start by generating an array of, say, 30 random integers. Render each value as a vertical bar whose height is proportional to its value, with the bars laid out left-to-right across the screen. Don&#8217;t worry about colours, controls, or configuration yet, get the rendering working first.</p><p>Now implement bubble sort and animate it. After every comparison and every swap, redraw the array and pause for a short delay (something like 50&#8211;100 ms is a good starting point). When the algorithm finishes, the bars should be in ascending order from left to right.</p><p>This is your end-to-end skeleton. Everything else in the challenge will hang off this foundation.</p><p><strong>Testing:</strong> Run your tool and watch the array sort itself. The bars should start in random order and finish in ascending order. Try a few different sample sizes (10, 30, 50) and confirm the layout still looks right. If your bars overflow the screen or look squashed, adjust your scaling so the tallest bar uses most of the available height.</p><h3>Step 2</h3><p>In this step your goal is to highlight what the algorithm is doing right now and show some live statistics.</p><p>Up to now your bars have all been the same colour. That makes it hard to see what the algorithm is up to at any given moment. Pick three colours: a resting/idle colour for bars that aren&#8217;t being touched, a comparison colour for the two indices the algorithm is currently comparing, and a swap/write colour for indices that are currently being swapped or written. Update your bubble sort so that the right indices are highlighted at the right moment.</p><p>Next, add a small statistics overlay somewhere on the screen showing the number of comparisons, the number of swaps/writes, the number of array accesses, and the elapsed wall-clock time. Update these as the algorithm runs so the user can watch the counters tick up.</p><p>Finally, when the algorithm finishes, validate that the array is actually sorted. If it isn&#8217;t, surface a clear error, this&#8217;ll save you a lot of pain later when you&#8217;re implementing trickier algorithms and a subtle bug slips in.</p><p><strong>Testing:</strong> Run your tool and confirm the highlighted bars match what bubble sort is doing, the two compared bars should light up in the comparison colour, and when a swap happens you should see the swap colour. Watch the stats counters and sanity check them: for a small array, count a few comparisons by eye and confirm the counter agrees. At the end of the run, deliberately break the algorithm (e.g. comment out the swap) and confirm your validation catches it.</p><h3>Step 3</h3><p>In this step your goal is to give the user playback controls and a configurable speed.</p><p>Right now the visualisation runs straight through from start to finish with no way to pause or replay it. Add controls that let the user start a run, pause it mid-way, resume from where it paused, restart it from the beginning, and step forward by a single operation at a time. The exact bindings are your call, keys in a terminal, buttons in a GUI, both in a web app.</p><p>Add a speed control too. Provide at minimum slow, medium, and fast presets, plus a way to set a custom value. Speed can be expressed as a frames-per-second rate or as a delay-per-step, pick whichever feels natural for your platform.</p><p>Make sure your controls remain responsive at all speeds. At the fastest preset, it&#8217;s tempting to do many algorithm steps per redraw to keep up, but the user still needs to be able to hit pause and have it actually pause. Test that.</p><p><strong>Testing:</strong> Start a run, pause it half-way through, and confirm the bars freeze in place with the comparison/swap highlights still visible. Resume and confirm it picks up from where it left off. Hit restart and confirm the array goes back to its original state and the stats reset to zero. Use the step control to advance one operation at a time and watch the comparison/swap highlights move with each press. Try the fast preset and confirm pause still responds promptly.</p><h3>Step 4</h3><p>In this step your goal is to refactor bubble sort behind a clean algorithm interface, then add two more algorithms.</p><p>So far your sorting code and your rendering code are probably mixed together. We&#8217;re going to separate them. Define an interface for a &#8220;sortable run&#8221;, something that, given an array, produces a stream of events describing what the algorithm is doing. The two essential events are <em>compared these two indices</em> and <em>wrote this value to that index</em>. A generator (or coroutine, or async iterator, depending on your language) is a natural fit: each <code>yield</code> is one event, and the renderer pulls events out one at a time.</p><p>Refactor your bubble sort to fit this interface. Your renderer should now consume events from the algorithm rather than calling sort logic directly. The stats counters become trivial to maintain, every comparison event increments the comparison counter, every write event increments the write counter, and so on.</p><p>With the interface in place, add insertion sort and selection sort. Each one should be its own implementation of the interface, with no changes needed to your renderer. Add an algorithm selector to your UI so the user can pick which one to run.</p><p><strong>Testing:</strong> Run each of the three algorithms on the same input and compare. Bubble sort should swap adjacent elements; insertion sort should sweep one element at a time into its correct place in the sorted prefix; selection sort should make far fewer swaps but lots of comparisons. The stats counters should reflect the differences, selection sort, for example, will show roughly the same number of comparisons as bubble sort but far fewer writes.</p><h3>Step 5</h3><p>In this step your goal is to add merge sort, quick sort, and heap sort.</p><p>Each algorithm should slot into the same algorithm interface you defined in Step 4. If the interface is right, your renderer shouldn&#8217;t need any changes, every algorithm is just a different sequence of comparison and write events.</p><p>A small word on merge sort: because it uses an auxiliary array, you&#8217;ll need to decide how to represent writes back into the main array. The simplest approach is to emit a write event for each element placed back into the main array during the merge. The user will see the array reform piece by piece as the merges complete, which is exactly the behaviour you want.</p><p><strong>Testing:</strong> Run each of the three algorithms and watch them. Merge sort should produce the classic &#8220;halves coming back together&#8221; pattern. Quick sort should show pivots being placed and partitions taking shape. Heap sort should show a chaotic phase (heap construction) followed by an orderly phase (extracting the max from the back of the array). Confirm the correctness check from Step 2 passes for all three on a variety of input sizes.</p><h3>Step 6</h3><p>In this step your goal is to add shell sort and radix sort.</p><p>Radix sort is the odd one out. It&#8217;s not comparison-based, it sorts by repeatedly distributing values into buckets based on individual digits. You&#8217;ll need to decide how to express what radix sort is doing through your event stream. One approach is to emit write events as values move from the array into the buckets and back. Another is to extend your event vocabulary with something like a <em>bucketed this value</em> event. Either is a reasonable design choice, the goal is for the viewer to be able to <em>see</em> the algorithm working.</p><p><strong>Testing:</strong> Run shell sort and watch how the array goes from chaotic to &#8220;fairly ordered&#8221; to fully sorted as the gap shrinks. Run radix sort on a fixed-width integer array and confirm it sorts correctly. Compare radix sort&#8217;s stats counters to the comparison sorts, radix sort should have zero comparisons (or very few, depending on how you count) and a number of writes proportional to the number of digits times the array size.</p><h3>Step 7</h3><p>In this step your goal is to add the numbers display mode behind a pluggable rendering interface.</p><p>Up to now everything has been bars. The numbers display mode renders each value as a small labelled tile or box showing the numeric value, laid out in a row. When two indices are being compared or swapped, the corresponding tiles should be visually lifted or separated from the main row so the active operation is unambiguous, with the comparison/swap colours from Step 2 applied.</p><p>That should look something like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5M1_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5M1_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png 424w, https://substackcdn.com/image/fetch/$s_!5M1_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png 848w, https://substackcdn.com/image/fetch/$s_!5M1_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png 1272w, https://substackcdn.com/image/fetch/$s_!5M1_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5M1_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png" width="1371" height="598" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:598,&quot;width&quot;:1371,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:76564,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/210227004?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5M1_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png 424w, https://substackcdn.com/image/fetch/$s_!5M1_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png 848w, https://substackcdn.com/image/fetch/$s_!5M1_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png 1272w, https://substackcdn.com/image/fetch/$s_!5M1_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1e0398d8-8b77-4ac0-98aa-4debc1be7146_1371x598.png 1456w" sizes="100vw" loading="lazy"></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 aria-hidden="true" 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><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>Just as you separated algorithms from rendering in Step 4, now separate the bars renderer from a generic rendering interface. Each renderer should accept the same array and event stream and decide how to draw it. Adding the numbers renderer should not require changes to any of the algorithms.</p><p>Add a display mode selector to your UI so the user can switch between bars and numbers.</p><p><strong>Testing:</strong> Run each of your eight algorithms in numbers mode and confirm the lift behaviour matches what you expect, when bubble sort compares two adjacent elements, those two tiles should lift; when it swaps them, both should be highlighted in the swap colour. Switch between bars and numbers mid-run if you can; the algorithm should keep running and just the rendering should change.</p><h3>Step 8</h3><p>In this step your goal is to add sample size and sample order configuration, polish the user experience, and finish the documentation.</p><p>Add controls that let the user select the sample size (the number of elements in the array, with sensible minimum and maximum bounds for your platform) and the initial sample order. Support at least three orders: random, reversed, and already sorted. When the user restarts a run, regenerate a fresh sample of the configured size and order, except for already sorted, which is deterministic by definition. This means consecutive runs of the same algorithm exercise different inputs.</p><p>Show the current configuration on screen at all times: the chosen algorithm name, the sample size, the sample order, and the display mode. The user should never have to wonder what they&#8217;re looking at.</p><p>When a sort completes, present a clear &#8220;sorted&#8221; state. A nice touch is a final sweep through the array, a brief animation where each element is briefly highlighted in turn, left to right, to confirm visually that everything is in order. Freeze the final stats so the user can read them.</p><p>Finally, write a short README documenting how to run your tool, how to choose between platforms (if you&#8217;ve implemented more than one), and, importantly, how a future contributor would add a new algorithm or a new display mode. If your interfaces from Steps 4 and 7 are clean, this should be easy to write. If it&#8217;s hard to write, that&#8217;s a signal that the interfaces could be better.</p><p><strong>Testing:</strong> Try every combination of configuration: each algorithm, each sample size from your minimum to your maximum, each sample order, each display mode. Confirm the on-screen configuration always matches what you&#8217;ve selected. Confirm that two consecutive runs of bubble sort with random order produce different starting arrays, but two consecutive runs with already sorted order produce the same one. Watch the completion sweep at the end of a run and confirm the stats freeze on the final values. Hand your README to a friend and ask them to add a hypothetical &#8220;cocktail sort&#8221; algorithm; if they can do it without reading your renderer code, you&#8217;ve nailed the abstraction.</p><h3>Going Further</h3><p>Here are some ideas to take your sorting visualiser further:</p><ul><li><p>Add more algorithms: cocktail sort, comb sort, gnome sort, Tim sort, intro sort, or even bogo sort for fun.</p></li><li><p>Add a head-to-head mode that runs two algorithms side-by-side on the same input array, so the user can race them.</p></li><li><p>Add an export-to-video or export-to-GIF option so users can share their favourite runs.</p></li><li><p>Build an algorithm-comparison view that runs every algorithm across a range of sample sizes and plots comparisons or writes against input size, giving a visual confirmation of each algorithm&#8217;s Big-O behaviour.</p></li><li><p>Visualise additional algorithm-specific metadata: recursion depth for merge and quick sort, the heap structure for heap sort, or the gap sequence for shell sort.</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[Join the Ready, Spec, Ship Hackathon Hackathon]]></title><description><![CDATA[Try Kiro for free and win prizes!]]></description><link>https://codingchallenges.substack.com/p/join-the-ready-spec-ship-hackathon</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/join-the-ready-spec-ship-hackathon</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Tue, 04 Aug 2026 15:03:54 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/007b617b-a208-430f-a6ea-6d066e40dc96_1448x1086.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><span>Hi All,</span></p><p><span>&#128591;</span> <em><span>Thank you for being a subscriber, I&#8217;m honoured to have you as a reader. &#127881;</span></em><span><br><br>I&#8217;m pleased to be hosting the </span><a href="https://codingagents.fyi/hackathon/kiro/"><span>Ready, Spec, Ship Hackathon</span></a><span> alongside Angie Jones, </span><span class="mention-wrap" data-attrs="{&quot;name&quot;:&quot;Gregor Ojstersek&quot;,&quot;id&quot;:106098672,&quot;type&quot;:&quot;user&quot;,&quot;url&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!TiaG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1b7fdc30-d8c4-45f2-b0df-0b60baf9d4f4_1000x1000.jpeg&quot;,&quot;uuid&quot;:&quot;ee271ee2-8f27-46db-ab49-2eddb00e3223&quot;}" data-component-name="MentionToDOM"></span><span>, and sponsored by </span><a href="https://kiro.dev/"><span>Kiro</span></a><span>. You can enter solo or as a team of up to three people, and you&#8217;re welcome to submit more than one project.</span></p><p><span>There&#8217;s a $9,600 prize pool, plus 2000 FREE Kiro credits for every verified entry. Submissions close on 23 August, with the winners announced on 6 September.</span></p><p><span>Find out more and take part:<br></span><a href="https://codingagents.fyi/hackathon/kiro/"><span>https://codingagents.fyi/hackathon/kiro/</span></a></p><p>It&#8217;s a great chance to checkout the Kiro agentic coding environment!<br><br>I hope to see you taking part. <br><br>Please let me know if you have any questions.<br><br>Regards, John</p><p></p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #129 - Coding Challenges Coach]]></title><description><![CDATA[This challenge is to build your own AI coach for Coding Challenges, learning how to build and deploy production grade AI agents.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-129-coding-challenges</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-129-coding-challenges</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Fri, 31 Jul 2026 08:02:05 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c9107af6-5c15-4eb1-906e-2ebbd106c849_1712x864.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 #129 - Coding Challenges Coach</h2><p><span>This challenge is to build your own AI coaching agent that helps developers learn by working through the Coding Challenges projects.</span></p><p><span>We&#8217;ve all wished for a patient mentor who sits with us while we learn: someone who helps us pick the right thing to work on, talks through our approach, lets us run our code, and remembers where we got to last time. In this challenge you&#8217;ll build exactly that, an AI coaching agent built with Google&#8217;s Agent Development Kit (ADK) and the </span><a href="https://fandf.co/4fMDaJe"><span>Gemini Enterprise Agent Platform</span></a><span>.</span></p><p><span>You&#8217;ll start with a small prototype running on your own machine, then progressively harden it into a production-ready service that runs on Google Cloud, tracks its own costs, watches its own behaviour, and passes a security audit. This coding challenge is all about the process of taking a demo through to something you&#8217;d be happy to run for real.</span></p><p><span>This project is kindly sponsored by </span><a href="https://fandf.co/4fMDaJe"><span>Google Cloud</span></a><span>.</span></p><h2>The Challenge - Building Your Own Coding Challenges Coach</h2><p>You&#8217;re going to build a coaching agent that can hold a conversation with a developer, help them choose a Coding Challenge that suits their interests and skill level, reads the challenge specifications, searches the web for extra context, runs and tests the developer&#8217;s code in a safe sandbox, and remembers the whole journey so they can stop and come back later. Then you&#8217;ll take that agent and turn it into a proper cloud service: deployed, identified, secured, costed, and observed.</p><p>This is a challenge that&#8217;s aimed at engineers who are comfortable writing code and now want to learn what it takes to build and operate an AI agent responsibly. Going from a local prototype through to a hardened, audited production service. The core skills here, working with an agent framework, wiring up tools, managing state, deploying to the cloud, and thinking about security and cost, transfer to any agent platform. We&#8217;ll use the Google stack throughout because it gives us a complete, coherent path from prototype to production, all driven from local CLI tools.</p><p>The interesting and tricky parts are in the second half. Getting an agent to chat is easy. Giving it tools it can safely run, making it remember the right things without repeating itself, deploying it without a single static credential in sight, keeping its costs under control, and proving to an auditor that it&#8217;s secure, that&#8217;s where the real learning is.</p><p>Each step below covers one whole area of the agent, so treat a step as a mini-project of its own rather than a quick hour of work. Get each area solid before you move on, because the later steps build directly on the earlier ones.</p><h3>Step Zero</h3><p><span>In this introductory step you&#8217;re going to set your environment up ready to begin developing and testing your solution.</span></p><p><span>This challenge uses Google&#8217;s Agent Development Kit, you can find installation instructions here: </span><a href="https://fandf.co/4vR2x1C"><span>ADK Installation</span></a><span>. You&#8217;ll also need a Google Cloud account and the gcloud CLI, because the later steps deploy to and lean on Google Cloud. Installation instructions for the gcloud CLI are </span><a href="https://fandf.co/4wZgM5n"><span>here</span></a><span>.</span></p><p>Everything you do will run from your local command line, so get comfortable with the <code>adk</code> CLI commands (<code>adk run</code>, <code>adk web</code>, and <code>adk deploy</code>).</p><p>It&#8217;s also worth spending some time reading through the ADK documentation so you understand the core building blocks: agents, tools, and the runtime. Have a look at how the built-in Dev UI works, as that&#8217;s where you&#8217;ll do most of your local testing.</p><p>Finally, gather a handful of Coding Challenge specification files to coach against. Grab two or three challenge descriptions and save them locally as markdown, JSON, or YAML files in a folder your agent will be able to read. These are your test data for the rest of the challenge.</p><p><strong>Testing:</strong> Confirm your environment is ready by creating an empty starter agent and launching the Dev UI:</p><pre><code><code>adk web</code></code></pre><p>You should be able to open the Dev UI in your browser and see your agent listed, ready to chat, even if it doesn&#8217;t do anything useful yet:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ix7d!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ix7d!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png 424w, https://substackcdn.com/image/fetch/$s_!ix7d!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png 848w, https://substackcdn.com/image/fetch/$s_!ix7d!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png 1272w, https://substackcdn.com/image/fetch/$s_!ix7d!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ix7d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png" width="1078" height="789" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:789,&quot;width&quot;:1078,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:39550,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/206464077?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ix7d!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png 424w, https://substackcdn.com/image/fetch/$s_!ix7d!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png 848w, https://substackcdn.com/image/fetch/$s_!ix7d!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png 1272w, https://substackcdn.com/image/fetch/$s_!ix7d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c955c34-ede6-471a-8de4-b6578099ebeb_1078x789.png 1456w" sizes="100vw" loading="lazy"></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 aria-hidden="true" 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><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><h3>Step 1</h3><p>In this step your goal is to build a coaching agent that runs entirely on your machine and helps a developer choose a Coding Challenge through conversation.</p><p><span>Using the ADK, create an agent that holds a natural-language conversation, asking about the developer&#8217;s interests, what they want to learn, and their experience level, then recommending a challenge that suits them. Give the agent a clear coaching persona through its ADK configuration, including a system prompt that makes it behave like a mentor: encouraging, Socratic, and focused on guiding the developer to the answer rather than handing it over. All of your development and testing at this stage should happen locally through the ADK Dev UI. You&#8217;ll find some Google ADK examples to build on </span><a href="https://fandf.co/4fcyiNt"><span>here</span></a><span>.</span></p><p>Check out the previous coding challenge, <a href="https://codingchallenges.fyi/challenges/challenge-llm.chatbot">building an AI-powered chatbot</a> for some guidance on creating this agent and setting the persona via the system prompt.</p><p><strong>Testing:</strong> Run your agent with <code>adk web</code> and open the Dev UI. Tell the agent you&#8217;re a beginner interested in networking and confirm it asks sensible follow-up questions and recommends a suitable challenge. Try again as an experienced developer wanting to learn about databases and confirm it recommends something more advanced. Then ask it to &#8220;just give me the full solution&#8221; and confirm it declines politely and offers hints or questions instead of the answer.</p><p>Your ADK session should look something like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!34VM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!34VM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png 424w, https://substackcdn.com/image/fetch/$s_!34VM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png 848w, https://substackcdn.com/image/fetch/$s_!34VM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png 1272w, https://substackcdn.com/image/fetch/$s_!34VM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!34VM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png" width="1153" height="922" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:922,&quot;width&quot;:1153,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:235279,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/206464077?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!34VM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png 424w, https://substackcdn.com/image/fetch/$s_!34VM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png 848w, https://substackcdn.com/image/fetch/$s_!34VM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png 1272w, https://substackcdn.com/image/fetch/$s_!34VM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F08cef745-7f9c-4c18-8b91-93233917ee3a_1153x922.png 1456w" sizes="100vw" loading="lazy"></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 aria-hidden="true" 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><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>And your agent should refuse to provide the full solution:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XVsp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XVsp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png 424w, https://substackcdn.com/image/fetch/$s_!XVsp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png 848w, https://substackcdn.com/image/fetch/$s_!XVsp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png 1272w, https://substackcdn.com/image/fetch/$s_!XVsp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XVsp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png" width="679" height="465" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/71642998-b321-4139-b2c4-b4f695979c7f_679x465.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:465,&quot;width&quot;:679,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:87168,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/206464077?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XVsp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png 424w, https://substackcdn.com/image/fetch/$s_!XVsp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png 848w, https://substackcdn.com/image/fetch/$s_!XVsp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png 1272w, https://substackcdn.com/image/fetch/$s_!XVsp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71642998-b321-4139-b2c4-b4f695979c7f_679x465.png 1456w" sizes="100vw" loading="lazy"></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 aria-hidden="true" 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><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><h3>Step 2</h3><p>In this step your goal is to give your agent tools so it can ground its coaching in real information rather than guessing.</p><p>Implement a custom tool that reads Coding Challenge specification files (markdown, JSON, or YAML) from the local filesystem and returns the content relevant to the developer&#8217;s question.</p><p>Implement a second tool that performs a web search using an external search API of your choice, so the agent can find extra context, documentation, or alternative approaches when the developer is stuck. Bind both tools to the agent so it can chain them together within a single turn, for example reading the relevant part of a challenge spec and then searching the web for a related concept before it replies.</p><p>When building agents there&#8217;s a fine line between providing too few tools, reducing the agent&#8217;s flexibility, and too many tools, resulting in the agent picking the wrong tool. If several tools are likely to be used in the same order repeatedly, see if you can build a workflow or a composite tool, this saves both time and tokens.</p><p>For guidance, refer to the ADK docs on <a href="https://adk.dev/tutorials/multi-tool-agent/">building a multi-tool agent</a>.</p><p><strong>Testing:</strong> In the Dev UI, ask a question about one of your saved challenge specs, for example &#8220;what does step 3 of this challenge ask me to do?&#8221;, and confirm the agent reads the file and answers from its real content. Ask something not in your specs, for example &#8220;what&#8217;s a good library for parsing this format?&#8221;, and confirm it runs a web search. Finally ask a question that needs both, for example &#8220;I&#8217;m on step 2, what background reading would help?&#8221;, and confirm you can see both tool calls in the trace, the spec read first and the search second, in a single turn.</p><h3>Step 3</h3><p>In this step your goal is to give your agent a safe place to run the developer&#8217;s code, in more than one language.</p><p>ADK handles code execution through the <code>BaseCodeExecutor</code> interface, and ships a <code>GkeCodeExecutor</code> that runs each snippet in a gVisor-sandboxed, ephemeral GKE workload. Every run should get a fresh, isolated container from a custom image with Python, Node, and Go toolchains installed. The tool should write the developer&#8217;s code into that sandbox, run it, and return the standard output, standard error, and exit code. Support at least Python, JavaScript, and Go, with the agent choosing the language from the challenge context or the developer&#8217;s stated preference.</p><p><strong>Testing:</strong> In the Dev UI, ask the agent to run a small snippet that prints something and exits cleanly, and confirm you get the output and an exit code of 0. Give it code that raises an error and confirm you see the error on standard error and a non-zero exit code. Run a &#8220;hello world&#8221; style snippet in each of Python, JavaScript, and Go and confirm each runs in the right language. Tell the agent you&#8217;re on a Go challenge, give it a Go snippet without naming the language, and confirm it picks Go.</p><h3>Step 4</h3><p>In this step your goal is to let your agent run tests and coach the developer through the results, while keeping execution under control.</p><p>Allow the agent to run a test suite against the user&#8217;s code, report which tests fail, and coach the developer through debugging, helping them understand and fix the problem themselves rather than rewriting the code for them. Add safety limits to every execution: a timeout, defaulting to 30 seconds, and a maximum output size, defaulting to 64 KB, both configurable, so a runaway process or a flood of output can&#8217;t disrupt the session. You&#8217;ll have to provide a way for the user&#8217;s code to reach the agent.</p><p><strong>Testing:</strong> Give the agent code with a known bug and a test that catches it. Confirm it runs the tests, reports the failure clearly, and responds with guiding questions rather than the corrected code, even when you ask it to &#8220;just fix it&#8221;. Then give it code with an infinite loop and confirm execution is cut off at the timeout with a clear message. Give it code that prints a huge amount of output and confirm the returned output is capped. Change the limits in your configuration and confirm the new values take effect.</p><h3>Step 5</h3><p><span>In this step your goal is to let a developer stop mid-challenge and pick up later, without the agent losing the thread or repeating itself.</span></p><p><span>Persist the conversation state using Session service and also enable the Memory Bank (VertexAiMemoryBankService) so a session can be paused and resumed. The Session service will capture which challenge the developer is working on, the code they&#8217;ve written, the coaching advice they&#8217;ve already been given, and which concepts have been covered. The Memory Bank will consolidate related memories across sessions so that user preferences are stored and as the notes accumulate the agent merges overlapping ones and avoids giving the developer the same advice twice.</span></p><p><strong><span>Testing:</span></strong><span> Have a session where you pick a challenge, write some code, and receive a couple of pieces of advice, then end it. Start a fresh session and confirm the agent remembers which challenge you&#8217;re on and refers back to your earlier code and advice rather than starting over. Across two or three sessions, steer it towards giving the same category of advice and confirm that by the later sessions it recognises it has already covered this and builds on it instead of repeating it.</span></p><h3>Step 6</h3><p>In this step your goal is to take your local agent and run it as a proper cloud service, still driven from your local repository, now easily running on the cloud.</p><p>Use <code>adk deploy</code> to deploy the agent from your local ADK project to the Google Cloud Agent Runtime, keeping your local repository as the single source of truth. Configure the deployment to use Gemini models served from Agent Runtime rather than a local or third-party API key. Expose the deployed agent through a public HTTPS endpoint, and set up continuous deployment so that pushing changes from your local repository automatically updates the running agent.</p><p><strong>Testing:</strong> Deploy with <code>adk deploy</code> and confirm it succeeds. Call the agent&#8217;s public HTTPS endpoint from your machine, for example with <code>curl</code>, and confirm you get a valid response over HTTPS using the Gemini models served from Agent Runtime, with no local or third-party model API key in the configuration. Make a small change locally, push it, and confirm the deployed agent updates without you running a manual deploy.</p><h3>Step 7</h3><p><span>In this step your goal is to give your deployed agent a real identity, so you can both restrict what it reaches and attribute everything it does, with no static credentials anywhere.</span></p><p><span>To do this, look into deploying to Agent Runtime with Agent Identity enabled.</span></p><p><strong><span>Testing:</span></strong><span> Confirm the deployed agent can still read specs, run searches, and execute code using its SPIFFE identity (A SPIFFE identity is a secure, cryptographically verifiable identifier assigned to specific software services (workloads) rather than machines - you can learn more on the </span><a href="https://spiffe.io/"><span>SPIFFE website</span></a><span>), and that there are no static API keys or long-lived credentials anywhere in the deployment.</span></p><p><span>Enable Data Access audit logs on the specs storage, run a full session, then filter Cloud Audit Logs by the agent&#8217;s principal and confirm the spec read, the search, and the sandbox execution all appear under the same SPIFFE ID.</span></p><p><span>Deploy a second agent and confirm its actions carry a different SPIFFE ID, so you can separate the two agents&#8217; activity from the logs alone.</span></p><p><span>Capture a valid token and try to use it from a different workload without the agent&#8217;s certificate, and confirm it is rejected while the agent&#8217;s own requests still succeed. Through the agent&#8217;s identity, try to reach a Google Cloud resource it shouldn&#8217;t need and confirm access is denied, and that the denial is recorded against the agent&#8217;s identity in the audit log.</span></p><h3>Step 8</h3><p>In this step your goal is to prove your hardening actually holds up, by passing an audit.</p><p>Build and run a mock third-party security audit against your deployment: a script that only ever reads, the same posture a real external auditor without console access would take. Check for static credentials confirming zero user-managed keys. Check for over-permissive IAM roles, flagging anything broader than the least-privilege set you defined in Step 7. Check for unencrypted state storage. Check for exposed debug endpoints by probing your deployed HTTPS endpoint for known debug paths, such as the ADK Dev UI routes, <code>/docs</code>, and <code>/openapi.json</code>, and confirming each returns 404 or 401 rather than 200. Your agent should pass all four checks.</p><p><strong>Testing:</strong> Run your audit and confirm it reports a clean pass on all four checks. Then deliberately introduce a problem, for example add a static credential or open up an IAM role, and confirm the audit catches it and fails. Fix it and confirm the audit passes again.</p><h3>Step 9</h3><p><span>In this step your goal is to make your agent pick the right model for the job and keep its spending under control.</span></p><p><span>Configure the agent to select models from </span><a href="https://fandf.co/3TKucUg"><span>Model Garden</span></a><span> based on task complexity, using a lightweight model for chat and challenge selection and a more capable model for code review and debugging. Because an ADK LlmAgent has a fixed model, structure this as a multi-agent setup: a lightweight coordinator agent that handles the conversation and delegates to a more capable sub-agent (via ADK sub-agents and auto-transfer, or by wrapping it as an AgentTool) whenever the developer needs code review or debugging. This makes the model boundary explicit and keeps each model&#8217;s usage cleanly attributable in the trace.</span></p><p><span>Track token usage per request and per session, and report the cost both in the Dev UI during development and in your production logs. Add a configurable per-session cost cap that gracefully ends a session when it&#8217;s reached, telling the developer clearly, rather than quietly running up charges.</span></p><p><strong><span>Testing:</span></strong><span> Have a simple challenge-selection conversation and confirm the lightweight model is used, then ask for a detailed code review and confirm the more capable model is used, checking the trace to see which model handled each request. Confirm you can see per-request and per-session cost in the Dev UI and in your production logs. Set a low cost cap, run a session until it&#8217;s reached, and confirm the session ends gracefully with a clear message and makes no further billable requests. Raise the cap and confirm the session can continue.</span></p><h3>Step 10</h3><p><span>In this step your goal is to make your agent&#8217;s behaviour visible and to keep its recommendations within policy.</span></p><p><span>Instrument your agent with </span><a href="https://opentelemetry.io/docs"><span>OpenTelemetry</span></a><span> and export its traces to </span><a href="https://fandf.co/4yFju1g"><span>Cloud Trace</span></a><span>, so that each agent interaction becomes a single trace whose child spans capture every model call and tool call, each span carrying the user query, the tool used, the model that served the request, and the token counts.</span></p><p><span>Finally, implement a governance control that restricts which challenges the agent can recommend based on a configurable policy, for example blocking challenges tagged as &#8220;enterprise&#8221; or &#8220;paid&#8221;, and record each policy block as a span event so enforcement is auditable in the trace.</span></p><p><strong><span>Testing:</span></strong><span> Run some interactions against your deployed agent, including at least one that triggers an error, then open Cloud Trace and confirm each interaction shows up as a single trace with child spans for its model and tool calls, that the model-call spans carry token-count attributes, and that the errored interaction has a span marked with an error status.</span></p><p><span>Configure the policy to block a particular challenge or tag, steer the conversation towards it, and confirm the agent won&#8217;t recommend it and offers an allowed alternative instead, then confirm the block is recorded as a span event on the trace. Then allow it and confirm the agent will recommend it.</span></p><h3>Going Further</h3><p>Once you&#8217;ve built the core coaching agent and hardened it for production, here are some ideas to take it further, ordered roughly from easiest to most ambitious:</p><ul><li><p>Add a richer set of challenge specs and let the coach recommend a learning path across several challenges rather than one at a time.</p></li><li><p>Add automatic grading that assesses how close a developer&#8217;s solution is to passing all the challenge&#8217;s testing steps.</p></li><li><p>Support voice or video coaching modes as well as text.</p></li><li><p>Add multi-user support and team coaching sessions, with per-user memory and shared team progress.</p></li><li><p>Let the agent use an alternative sandbox provider and compare its behaviour and cost against the GKE-based executor.</p></li><li><p>Build a proper front end beyond the Dev UI, tailored to the coaching experience.</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 #128 - Mock Interview Coach]]></title><description><![CDATA[This challenge is to build your own live AI-driven video based mock interview coach.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-128-mock-interview</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-128-mock-interview</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 25 Jul 2026 08:01:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!JN6I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.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 #128 - Mock Interview Coach</h2><p>This challenge is to build your own mock interview coach, a tool that lets you practise behavioural interviews face-to-face speaking to a photorealistic AI interviewer.</p><p>Behavioural interviews are the part of the hiring process that engineers practise least. Reading sample STAR answers is nothing like composing one out loud, under time pressure, while someone watches you. In this challenge you&#8217;ll build a coach that puts a live, talking interviewer on your screen using <a href="https://anam.ai/?utm_source=codingchallenges&amp;utm_medium=newsletter&amp;utm_campaign=cara4-build-challenge">Anam&#8217;s</a> real-time persona platform and their latest cara-4 model. It will ask you behavioural questions, probe your weak answers with follow-ups, and then score your performance and tell you how to improve. Along the way you&#8217;ll work with real-time avatar streaming, persona and prompt design, session-based authentication, and post-session transcript analysis.</p><p>This project is kindly sponsored by <a href="https://anam.ai/?utm_source=codingchallenges&amp;utm_medium=newsletter&amp;utm_campaign=cara4-build-challenge">Anam</a>.</p><h2>The Challenge - Building Building Your Own Mock Interview Coach</h2><p>You&#8217;re going to build a web application with two halves. The backend, in any language you like, will manage interview setup, issue <a href="https://anam.ai/?utm_source=codingchallenges&amp;utm_medium=newsletter&amp;utm_campaign=cara4-build-challenge">Anam</a> session tokens, and analyse transcripts to produce feedback reports. The frontend is a thin web page that streams the interviewer&#8217;s face and voice into a video element and lets you talk to them over your microphone.</p><p>The interesting parts are the persona design (writing a system prompt that makes an AI interview you rather than help you), the security model (keeping your API key server-side while the browser streams in real time), and the feedback loop (turning a raw conversation transcript into a structured STAR assessment).</p><p>This is an intermediate to advanced challenge. The backend can be written in any language that can serve HTTP and call a REST API, so pick whatever you&#8217;re most comfortable with. The frontend needs only a small amount of JavaScript, so don&#8217;t be put off if you&#8217;re not a frontend developer.</p><p>What you&#8217;ll build will look something like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JN6I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JN6I!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png 424w, https://substackcdn.com/image/fetch/$s_!JN6I!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png 848w, https://substackcdn.com/image/fetch/$s_!JN6I!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png 1272w, https://substackcdn.com/image/fetch/$s_!JN6I!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JN6I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png" width="1339" height="902" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:902,&quot;width&quot;:1339,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:319443,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/207350336?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JN6I!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png 424w, https://substackcdn.com/image/fetch/$s_!JN6I!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png 848w, https://substackcdn.com/image/fetch/$s_!JN6I!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png 1272w, https://substackcdn.com/image/fetch/$s_!JN6I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2446c47-9449-41f6-97a5-eae87e0ec423_1339x902.png 1456w" sizes="100vw" loading="lazy"></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 aria-hidden="true" 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><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><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>First, sign up for a free <a href="https://anam.ai/?utm_source=codingchallenges&amp;utm_medium=newsletter&amp;utm_campaign=cara4-build-challenge">Anam</a> account. The free tier gives you API access, 30 minutes of conversation per month, and a 3 minute limit per conversation. That&#8217;s enough to complete this challenge, but it does mean each test session is short, so plan to test with brief exchanges rather than full-length interviews.</p><p>From your Anam account, create an API key and make a note of it. We&#8217;ll be keeping it server-side, so think about how you&#8217;ll provide it to your backend, an environment variable named <code>ANAM_API_KEY</code> is the convention we&#8217;ll use in this challenge.</p><p>Next, pick your backend language and an HTTP server library you&#8217;re comfortable with. Then spend a little time with the <a href="https://anam.ai/docs/?utm_source=codingchallenges&amp;utm_medium=newsletter&amp;utm_campaign=cara4-build-challenge">Anam documentation</a>, particularly the JavaScript SDK quickstart, to understand the moving parts: personas, session tokens, and the streaming client.</p><p>Finally, make sure you have a browser with a working microphone and camera permissions you can grant to <code>localhost</code>.</p><h3>Step 1</h3><p>In this step your goal is to design your interviewer persona.</p><p>A persona on the Anam platform combines a face, a voice, an LLM, and a system prompt. The system prompt is where your coaching product lives, it&#8217;s what turns a general-purpose talking avatar into a behavioural interviewer.</p><p>Define a persona configuration (name, avatar, voice, LLM, and system prompt) for an interviewer that:</p><ul><li><p>Asks one behavioural question at a time and waits for the answer.</p></li><li><p>Probes with a follow-up when an answer lacks specifics, for example asking &#8220;what was the outcome?&#8221; when the candidate doesn&#8217;t say.</p></li><li><p>Never suggests answers or completes the candidate&#8217;s stories for them.</p></li><li><p>Stays in role, politely steering the conversation back to the interview if the candidate drifts off topic.</p></li></ul><p>Getting this behaviour reliable is a prompt engineering exercise, expect to iterate. If you want some great ideas for what to include in the system prompt, check out my interview with Austen McDonald about <a href="https://www.youtube.com/watch?v=BHfrXVMV-SE">mastering behavioural interviews</a>. Also check out Austen&#8217;s newsletter: <a href="https://thebehavioral.substack.com?r=gjveo">Mastering Behavioral Interviews</a>. He also has a <a href="https://thebehavioral.substack.com/p/behavioral-prep-on-a-budget-peer">guide to doing mock interviews</a>.</p><p><strong>Testing:</strong> Use the <a href="https://lab.anam.ai/?utm_source=codingchallenges&amp;utm_medium=newsletter&amp;utm_campaign=cara4-build-challenge">Anam Lab</a> to create the persona and talk to it. Try three conversations: give a vague answer (&#8221;we fixed it and it was fine&#8221;) and check you get a probing follow-up; ask the interviewer &#8220;what would you say in my situation?&#8221; and check it declines to answer for you; try changing the subject to the weather and check it brings you back to the interview. The lab is a great no-code option for refining the avatar before you build the full system.</p><p>Here&#8217;s what it looks like:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!h51z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!h51z!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png 424w, https://substackcdn.com/image/fetch/$s_!h51z!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png 848w, https://substackcdn.com/image/fetch/$s_!h51z!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png 1272w, https://substackcdn.com/image/fetch/$s_!h51z!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!h51z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png" width="1456" height="800" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:800,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:535729,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/207350336?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!h51z!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png 424w, https://substackcdn.com/image/fetch/$s_!h51z!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png 848w, https://substackcdn.com/image/fetch/$s_!h51z!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png 1272w, https://substackcdn.com/image/fetch/$s_!h51z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd778d50a-1752-430f-8a13-c0ec0a400f0f_1656x910.png 1456w" sizes="100vw" loading="lazy"></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 aria-hidden="true" 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><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>Use the prompt tab to enter your prompt, the avatar tab to upload / take a photo of you, the voice tab to clone your voice if you want to. Otherwise pick suitable ones from the existing options.</p><h3>Step 2</h3><p>In this step your goal is to build the session token service, the backend half of the security model.</p><p>Anam&#8217;s streaming sessions are authorised with short-lived session tokens, created by calling <code>POST &lt;https://api.anam.ai/v1/auth/session-token</code>&gt; with your API key and a persona configuration. The API key must never reach the browser, so your backend will act as the exchange point.</p><p>Build a backend with an endpoint that, when called by your frontend, requests a session token from the Anam API using the persona configuration from Step 1, and returns that token to the caller. Read the API key from the <code>ANAM_API_KEY</code> environment variable.</p><p>Your endpoint should also apply a time limit to each interview session. Make the limit configurable, and default it to fit inside the free tier&#8217;s 3 minute conversation limit.</p><p><strong>Testing:</strong> Start your backend and request a token from the command line:</p><pre><code><code>curl -X POST &lt;http://localhost:8080/api/session-token&gt;</code></code></pre><p>You should get back a JSON response containing a session token. Check your frontend code and browser network tab later in the challenge to confirm the Anam API key itself never appears anywhere client-side. Try setting the time limit configuration to a different value and confirm the session created with the token respects it.</p><h3>Step 3</h3><p>In this step your goal is to get your interviewer on screen, streaming in real time.</p><p>Build a minimal web page, served by your backend, that:</p><ul><li><p>Fetches a session token from the endpoint you built in Step 2.</p></li><li><p>Creates a streaming client using Anam&#8217;s <code>@anam-ai/js-sdk</code> package and attaches it to a video element with <code>streamToVideoElement</code>.</p></li><li><p>Has controls to start the interview and to end the session at any time.</p></li></ul><p>Once connected, the candidate speaks their answers over their microphone and the interviewer responds with voice and video. Have the interviewer open the session by greeting the candidate and explaining the format: how many questions to expect and how the interview will run. That behaviour belongs in your persona&#8217;s system prompt or greeting configuration, not in your JavaScript.</p><p><strong>Testing:</strong> Open the page in your browser, click start, and grant microphone access. You should see your interviewer appear in the video element, greet you, explain the format, and ask the first question within a few seconds. Answer out loud and confirm the interviewer hears you and responds. Click your end control mid-conversation and confirm the stream stops cleanly.</p><h3>Step 4</h3><p>In this step your goal is to make each interview a real, configurable interview rather than a single hard-coded conversation.</p><p>First, build a question bank of at least 20 behavioural questions, each tagged with a competency such as leadership, conflict, failure, teamwork, or delivery. Store it however you like, this is data your backend owns.</p><p>Then add an interview setup screen where, before the session starts, the candidate chooses:</p><ul><li><p>The target role they&#8217;re practising for.</p></li><li><p>The seniority level.</p></li><li><p>Which competencies they want to be assessed on.</p></li></ul><p>Your backend should generate each interview from those choices, drawing suitable questions from the bank, so that repeated sessions produce different interviews rather than the same script every time.</p><p><strong>Testing:</strong> Set up an interview targeting a senior role with the &#8220;conflict&#8221; and &#8220;failure&#8221; competencies. Confirm the questions asked relate to those competencies. Run a second interview with the same setup and confirm you get different questions from the bank. Change the competencies and confirm the questions change to match.</p><h3>Step 5</h3><p>In this step your goal is to capture and store the interview transcript.</p><p>The feedback report in the next step needs the full conversation, both the interviewer&#8217;s questions and the candidate&#8217;s answers. Anam gives you two routes to it: the SDK emits message history events while the session runs, and there is a session transcript API you can call after the session ends. Pick whichever suits your design.</p><p>Once you have the transcript, persist each completed interview with its date, the setup choices from Step 4, and the full transcript, so past sessions can be reviewed later.</p><p><strong>Testing:</strong> Complete a short interview, answering at least two questions. Then inspect your stored data and confirm it contains the date, the role, seniority and competencies you chose, and the complete conversation with both the interviewer&#8217;s and your own words. Complete a second interview and confirm both sessions are stored separately.</p><h3>Step 6</h3><p>In this step your goal is to turn the transcript into a feedback report, this is where your tool becomes a coach.</p><p>The standard framework for behavioural answers is the STAR method: Situation, Task, Action, Result. Strong answers cover all four; weak answers typically describe a situation and then trail off without actions or results.</p><p>Analyse each answer in the stored transcript and produce a written feedback report that includes:</p><ul><li><p>Which STAR elements were present and which were missing from each answer.</p></li><li><p>A score from 1 to 5 for each answer.</p></li><li><p>The candidate&#8217;s overall strengths.</p></li><li><p>One specific improvement suggestion per question.</p></li></ul><p>You&#8217;ll want an LLM to do the analysis, any provider works, this part is independent of the live Anam session.</p><p>Finally, track scores across sessions so the candidate can see whether they&#8217;re improving over time.</p><p><strong>Testing:</strong> Run an interview and deliberately give one incomplete answer, describe a situation but never say what the result was. The report should flag the missing Result for that answer and score it lower. Give one full STAR answer in the same interview and confirm it scores higher. Complete a few sessions and confirm you can see your scores across them.</p><h3>Step 7</h3><p>In this step your goal is to add interviewer styles and give the candidate control over the flow of the interview.</p><p>Different interviewers put different pressure on candidates, and practising with only one style leaves you unprepared for the others. Offer at least two selectable interviewer personas with distinct voices and personalities, for example a supportive coach who encourages you along, and a formal bar-raiser who is polite but gives you nothing back.</p><p>Then add flow control: the candidate should be able to skip a question they don&#8217;t want to answer, and end the interview early. When they do, the feedback report should cover only the questions actually answered, with no scores or feedback invented for questions that were skipped.</p><p><strong>Testing:</strong> Run an interview with each persona and confirm the voice and manner are noticeably different while the interview structure stays the same. In another session, skip the second question and end the interview after the third. Check the report covers exactly the questions you answered, with the skipped question absent.</p><h3>Going Further</h3><p>Once you have the core coach working, here are some ideas to take it further:</p><ul><li><p>Add a per-answer timer so candidates can practise keeping answers to two or three minutes.</p></li><li><p>Generate a printable or PDF version of the feedback report to share with a mentor.</p></li><li><p>Add technical and system design interview modes with their own question banks and scoring rubrics.</p></li><li><p>Bring your own LLM to the live conversation, so the same brain that scores the answers also drives the interviewer&#8217;s follow-up questions.</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 #127 - Tmux]]></title><description><![CDATA[This challenge is to build your own tmux.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-127-tmux</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-127-tmux</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 18 Jul 2026 08:00:26 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/cc71eb9c-f8fd-4c86-a5f3-feaaef482f1d_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 #127 - Tmux</h2><p>This challenge is to build your own version of tmux, the terminal multiplexer.</p><p>If you spend any time working in a terminal, sooner or later you&#8217;ll meet tmux. It lets you run multiple terminal sessions inside a single window, split that window into panes, and, best of all, detach from a session and come back to it later with everything still running. It&#8217;s the tool that saves your work when your SSH connection drops. Under the hood it brings together some fascinating systems programming: pseudo-terminals, client-server architecture over Unix domain sockets, raw terminal input handling, and screen rendering.</p><p>Building a simplified version yourself is one of the best ways I know to really understand how a terminal works.</p><h2>The Challenge - Building Your Own Tmux</h2><p>In this challenge you&#8217;re going to build a simplified terminal multiplexer. Your tool will let you create named sessions that keep running in the background, detach from them and reattach later, open multiple windows within a session, and split windows into panes, each running its own shell.</p><p>This is an advanced challenge. The individual steps are manageable, but you&#8217;ll be working with pseudo-terminals, raw terminal modes, Unix domain sockets, and screen rendering, so it helps to be comfortable with systems programming.</p><p>The concepts are the same in any language, so pick one you know well that gives you good access to operating system APIs. This challenge is best tackled on Linux, macOS, or another Unix-like platform, since that is where pseudo-terminals and Unix domain sockets live.</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. You&#8217;ll need access to pseudo-terminal (PTY) APIs, Unix domain sockets, and the ability to put the terminal into raw mode, so check your chosen language has libraries for these before you commit.</p><p>If you haven&#8217;t used tmux before, install it and spend some time with it. Create a session, detach with <code>Ctrl+b d</code>, reattach, split some panes, and switch between windows. Getting a feel for the real thing will make every step of this challenge clearer. The <a href="https://man.openbsd.org/tmux">tmux man page</a> and the <a href="https://github.com/tmux/tmux/wiki/Getting-Started">tmux getting started guide</a> are both excellent references.</p><p>It&#8217;s also worth reading up on how pseudo-terminals work (<code>man pty</code> on most systems) and how terminal escape sequences control the screen. These two topics are the foundation of everything you&#8217;ll build here.</p><h3>Step 1</h3><p>In this step your goal is to allocate a pseudo-terminal, spawn a shell inside it, and connect it to your own terminal.</p><p>This is the core building block of a terminal multiplexer. Your program should create a PTY, launch the user&#8217;s shell as a child process attached to it, put your own terminal into raw mode, and then shuttle data in both directions: keystrokes from your terminal go to the shell, and the shell&#8217;s output is displayed on your screen. When the shell exits, your program should restore the terminal to its original state and exit cleanly.</p><p><strong>Testing:</strong> Run your program:</p><pre><code><code>cctmux</code></code></pre><p>You should see a shell prompt. Try running some commands, including full-screen programs:</p><pre><code><code>ls -la
vim
top</code></code></pre><p>Everything should behave exactly as it would in a normal terminal. Type <code>exit</code> and verify your program shuts down and your terminal is restored to a usable state.</p><h3>Step 2</h3><p>In this step your goal is to split your program into a server and a client that communicate over a Unix domain socket.</p><p>Real <code>tmux</code> is built as a client-server system. The server owns the sessions and the shells running inside them; the client is a thin process that connects to the server, sends it your keystrokes, and displays whatever the server tells it to. This separation is what makes detaching possible later: the shells belong to the server, so they survive when a client goes away.</p><p>When your client starts, it should check whether a server is already running. If not, it should start one in the background. The two should communicate over a Unix domain socket, and you&#8217;ll need a simple protocol for passing keyboard input in one direction and screen output in the other.</p><p><strong>Testing:</strong> Run your client:</p><pre><code><code>cctmux</code></code></pre><p>You should get a working shell, just like in Step 1. In another terminal, verify the socket exists (for example with <code>ls -l</code> on wherever you chose to put it, such as <code>/tmp/cctmux-*</code>) and that the server appears in <code>ps</code> as a separate process from your client.</p><h3>Step 3</h3><p>In this step your goal is to support named sessions.</p><p>A session is a collection of shells managed by the server under a single name. Your tool should support creating a new named session:</p><pre><code><code>cctmux new-session -s mysession</code></code></pre><p>The server should be able to hold several sessions at once, each with its own shell. If a user tries to create a session with a name that already exists, they should get a clear error message.</p><p><strong>Testing:</strong> Create a session and verify you get a shell:</p><pre><code><code>cctmux new-session -s work</code></code></pre><p>Run a command like <code>echo hello</code> to confirm the shell works. Then try creating a duplicate:</p><pre><code><code>cctmux new-session -s work</code></code></pre><p>You should see an error telling you the session already exists.</p><h3>Step 4</h3><p>In this step your goal is to implement the prefix key and detaching from a session.</p><p>Tmux is driven by a prefix key, <code>Ctrl+b</code> by default. When the user presses the prefix, the next keystroke is a command for the multiplexer instead of input for the shell. Your first command is <code>d</code> for detach: the client should disconnect and exit, restoring the terminal, while the session and its shell keep running on the server.</p><p>Make the prefix key configurable so users can choose something other than <code>Ctrl+b</code>, for example via a command line flag or a simple configuration file. Any key that isn&#8217;t a recognised command after the prefix should be handled gracefully, and pressing the prefix twice should send the prefix key itself through to the shell.</p><p><strong>Testing:</strong> Create a session and start something long-running in it:</p><pre><code><code>cctmux new-session -s work</code></code></pre><p>Inside the session run:</p><pre><code><code>sleep 300 &amp;
echo "still here"</code></code></pre><p>Press <code>Ctrl+b</code> then <code>d</code>. You should be back at your normal terminal prompt. Verify with <code>ps</code> that the shell and the sleep process are still running. Then configure a different prefix key, start a new client, and verify detaching works with the new prefix and that <code>Ctrl+b</code> now passes through to the shell.</p><h3>Step 5</h3><p>In this step your goal is to support reattaching to a running session.</p><p>Detaching is only half the magic. Your tool should let the user reconnect to a session that&#8217;s running in the background:</p><pre><code><code>cctmux attach-session -t mysession</code></code></pre><p>On attaching, the user should see the session&#8217;s current screen content, including output that was produced while they were detached, and be able to carry on working as if they never left. Attaching to a session that doesn&#8217;t exist should produce a clear error.</p><p><strong>Testing:</strong> Create a session, run <code>echo before detach</code>, then detach with <code>Ctrl+b d</code>. Reattach:</p><pre><code><code>cctmux attach-session -t work</code></code></pre><p>You should see the screen as you left it, including <code>before detach</code>. Detach again, then try attaching to a session that doesn&#8217;t exist:</p><pre><code><code>cctmux attach-session -t nosuchsession</code></code></pre><p>You should get an error message, not a hang or a crash.</p><h3>Step 6</h3><p>In this step your goal is to support listing sessions, killing sessions, and cleaning up the server.</p><p>Your tool should report the sessions the server is currently managing:</p><pre><code><code>cctmux list-sessions</code></code></pre><p>The output should show each session&#8217;s name and some useful detail, such as how many windows it has and whether a client is attached, similar to the real tmux output.</p><p>It should also support killing a session:</p><pre><code><code>cctmux kill-session -t mysession</code></code></pre><p>Killing a session should terminate the shells running inside it. When the last session is closed, whether by <code>kill-session</code> or by all of its shells exiting, the server should shut itself down and remove its socket, leaving nothing behind.</p><p><strong>Testing:</strong> Create two sessions, <code>work</code> and <code>play</code>, detaching from each. Run:</p><pre><code><code>cctmux list-sessions</code></code></pre><p>You should see both sessions listed. Kill one:</p><pre><code><code>cctmux kill-session -t play
cctmux list-sessions</code></code></pre><p>Only <code>work</code> should remain. Kill <code>work</code> too, then verify with <code>ps</code> that the server process has exited and that the socket file has been removed.</p><h3>Step 7</h3><p>In this step your goal is to add a status bar.</p><p>Tmux reserves the bottom line of the terminal for a status bar. Yours should show the session name on the left, the list of windows in the middle (just one window for now), and the current time on the right. The status bar should update as things change, including ticking over as the time changes, and the shell should now render in the remaining lines above it without ever overwriting the bar.</p><p><strong>Testing:</strong> Attach to a session. You should see the status bar on the bottom line showing the session name and the time. Run a command that produces lots of output, such as:</p><pre><code><code>seq 1 100</code></code></pre><p>The output should scroll in the area above the status bar and the bar should stay intact. Watch for a minute and confirm the clock updates.</p><h3>Step 8</h3><p>In this step your goal is to support multiple windows within a session.</p><p>A window is like a tab: each window has its own shell, but only one window is visible at a time. Pressing the prefix key followed by <code>c</code> should create a new window with its own shell and switch to it. The status bar should list all the windows with their numbers, starting from <code>0</code>, and indicate which one is currently active.</p><p><strong>Testing:</strong> Attach to a session and run <code>echo window zero</code>. Press <code>Ctrl+b c</code>. You should get a fresh shell and the status bar should now show two windows, with the second one marked as active. Run <code>echo window one</code> in the new window. Create a third window with <code>Ctrl+b c</code> and confirm the status bar shows three windows.</p><h3>Step 9</h3><p>In this step your goal is to support switching between windows.</p><p>With multiple windows created, the user needs to move between them:</p><ul><li><p>The prefix key followed by a digit (<code>0</code> to <code>9</code>) should switch directly to that window number.</p></li><li><p>The prefix key followed by <code>n</code> should switch to the next window, wrapping around from the last back to the first.</p></li><li><p>The prefix key followed by <code>p</code> should switch to the previous window, wrapping the other way.</p></li></ul><p>When switching to a window, its screen content should be restored exactly as it was, and the status bar should update to show the newly active window.</p><p><strong>Testing:</strong> Using the session from Step 8, press <code>Ctrl+b 0</code>. You should see the first window with <code>window zero</code> still on screen. Press <code>Ctrl+b 1</code> and verify you see <code>window one</code>. Press <code>Ctrl+b n</code> twice and <code>Ctrl+b p</code> twice, checking the status bar each time to confirm you cycle through the windows in order and wrap around at each end. Try <code>Ctrl+b 9</code> and confirm switching to a window that doesn&#8217;t exist is handled gracefully.</p><h3>Step 10</h3><p>In this step your goal is to split a window into two panes, one above the other, with the prefix key followed by <code>"</code>.</p><p>This is where your multiplexer really starts to earn its name. Pressing <code>Ctrl+b "</code> should split the current pane horizontally, creating a new pane below it with its own PTY and shell. Both panes run at the same time: output arriving in either pane should render in that pane&#8217;s region of the screen, whichever pane is active.</p><p>Draw a border between the panes using box-drawing characters (such as <code>&#9472;</code> and <code>&#9474;</code>), and make sure each shell knows its actual pane size so that programs running inside it wrap and draw correctly. Keyboard input should go to the active pane.</p><p><strong>Testing:</strong> Attach to a session and press <code>Ctrl+b "</code>. The screen should split into a top and bottom pane with a horizontal border between them. In the active pane run:</p><pre><code><code>seq 1 5</code></code></pre><p>The output should appear only in that pane. Start a long-running command in one pane, for example <code>top</code>, and confirm it keeps updating its own region while you type in the other pane after switching (for now you can test switching in the next step, or verify the inactive pane keeps rendering). Run <code>ls</code> and confirm the columns fit the pane width, not the full terminal width.</p><h3>Step 11</h3><p>In this step your goal is to split a window into two panes side by side with the prefix key followed by <code>%</code>.</p><p>Pressing <code>Ctrl+b %</code> should split the current pane vertically, creating a new pane to the right with its own shell. Vertical borders should be drawn between side-by-side panes, and splits should compose: the user should be able to mix <code>"</code> and <code>%</code> splits to build up a grid of panes, each rendering independently in its own region.</p><p><strong>Testing:</strong> In a fresh window press <code>Ctrl+b %</code>. You should see two panes side by side with a vertical border. Run <code>seq 1 5</code> and confirm the output stays within the pane. Now press <code>Ctrl+b "</code> and confirm the current pane splits into top and bottom, giving you three panes. Run a different command in each and verify all three render correctly in their own regions.</p><h3>Step 12</h3><p>In this step your goal is to support navigating between panes with the prefix key followed by the arrow keys.</p><p>Pressing <code>Ctrl+b</code> followed by an arrow key should move the active pane in that direction: up, down, left, or right. Your tool should give a visual indication of which pane is active, for example by highlighting its border. Keyboard input should always go to the active pane.</p><p><strong>Testing:</strong> Build a layout with at least three panes. Use <code>Ctrl+b</code> followed by the arrow keys to move between them, confirming the active pane indicator follows you. In each pane type a different command, such as <code>echo top left</code>, and verify the input lands in the pane you expect. Try navigating in a direction where there is no pane and confirm nothing breaks.</p><h3>Step 13</h3><p>In this step your goal is to support resizing panes with the prefix key followed by <code>Ctrl</code> and the arrow keys.</p><p>Pressing the prefix followed by <code>Ctrl+Up</code>, <code>Ctrl+Down</code>, <code>Ctrl+Left</code>, or <code>Ctrl+Right</code> should move the border of the active pane in that direction, making it bigger or smaller. The neighbouring pane should shrink or grow to match, borders should be redrawn, and the shells in the affected panes should be told their new size so that running programs adapt.</p><p>Be warned if you&#8217;re working on a Mac Ctrl+Arrow gets captured by the OS, you&#8217;ll want to add support for the Alt key just like tmux itself does.</p><p><strong>Testing:</strong> Split a window with <code>Ctrl+b "</code>. Press <code>Ctrl+b Ctrl+Up</code> a few times and watch the border move up, making the bottom pane taller. Run <code>top</code> in one pane, resize it, and confirm <code>top</code> redraws itself to fit the new size. Repeat with a side-by-side split using <code>Ctrl+Left</code> and <code>Ctrl+Right</code>. Confirm you can&#8217;t resize a pane away to nothing.</p><h3>Step 14</h3><p>In this step your goal is to handle the terminal itself being resized.</p><p>When the user resizes their terminal emulator window, your client should notice, tell the server, and the server should redistribute the space: recalculating the pane layout, informing every shell of its new size, redrawing all borders, and keeping the status bar on the bottom line.</p><p><strong>Testing:</strong> Attach to a session with several panes and resize your terminal window by dragging its corner. The layout should adapt smoothly: panes share the new space, borders are redrawn correctly, and the status bar stays on the bottom row. Run <code>top</code> in a pane while resizing and confirm it adapts. Make the terminal very small and then large again, and confirm nothing is corrupted.</p><h3>Step 15</h3><p>In this step your goal is to add a command prompt.</p><p>Pressing the prefix key followed by <code>:</code> should open a prompt on the status bar line where the user can type a command, edit it, and press Enter to run it, or Escape to cancel. Support the commands your multiplexer already understands, for example <code>new-window</code>, <code>split-window</code>, <code>list-sessions</code>, and <code>kill-session</code>. Unknown commands should show an error message on the status line rather than doing anything surprising.</p><p><strong>Testing:</strong> Press <code>Ctrl+b :</code> and the status bar should turn into a prompt. Type <code>new-window</code> and press Enter; a new window should be created just as if you had pressed <code>Ctrl+b c</code>. Open the prompt again, type <code>nonsense</code>, and confirm you get an error message on the status line. Finally, press <code>Ctrl+b :</code> then Escape and confirm the prompt closes without running anything.</p><h3>Step 16</h3><p>In this step your goal is to add copy mode, so users can scroll back through a pane&#8217;s output history.</p><p>Each pane should keep a scrollback buffer of output that has moved off the top of the screen. Pressing the prefix key followed by <code>[</code> should enter copy mode for the active pane: the pane freezes, and the user can scroll back through the history using the arrow keys and page up/page down. Give a visual indication that copy mode is active, such as a position indicator showing where the user is in the history. Pressing <code>q</code> should leave copy mode and return the pane to live output.</p><p><strong>Testing:</strong> In a pane, generate more output than fits on the screen:</p><pre><code><code>seq 1 500</code></code></pre><p>Press <code>Ctrl+b [</code> and scroll up. You should be able to scroll back and find line <code>1</code>. Scroll down again, then press <code>q</code> and confirm the pane returns to the live view and typing works normally. Verify that output arriving in another pane doesn&#8217;t disturb your position while you&#8217;re scrolling.</p><h3>Going Further</h3><p>Once you have the core multiplexer working, here are some ideas to take it further:</p><ul><li><p>Add text selection and copying in copy mode, with a paste buffer and a paste key binding (prefix followed by <code>]</code>).</p></li><li><p>Support renaming sessions and windows, and show the custom names in the status bar.</p></li><li><p>Add a session chooser (like tmux&#8217;s prefix followed by <code>s</code>) for switching sessions interactively.</p></li><li><p>Support a configuration file that sets key bindings, colours, and status bar content, like <code>.tmux.conf</code>.</p></li><li><p>Add zooming a pane to full screen and back with prefix followed by <code>z</code>.</p></li><li><p>Implement mouse support: clicking to select a pane, dragging borders to resize, and scrolling with the mouse wheel.</p></li><li><p>Add synchronised panes, where input is sent to every pane in the window at once.</p></li><li><p>Support layout presets such as even-horizontal, even-vertical, and tiled.</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 #126 - Background Removal Tool]]></title><description><![CDATA[This challenge is to build your own background removal tool for images.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-126-background-removal</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-126-background-removal</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 11 Jul 2026 08:01:20 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/86efb9dd-84a5-4022-a8cf-d63ace19600b_2400x1514.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 #126 - Background Removal Tool</h2><p>This challenge is to build your own image background removal tool that takes a photograph of a person and removes the background, keeping the person and making everything else transparent.</p><p>You have probably seen this feature everywhere by now. You give it a photo of yourself, and a moment later you have a clean cut-out that you can drop onto a new background, turn into a profile picture, or use in a design. It feels like magic, but underneath it is a satisfying mix of image decoding, machine learning, and careful pixel work. Getting the body right is the easy part. Handling the fine wisps of hair less so!</p><p>In this challenge you are going to build a tool that does all of this on the user&#8217;s own device, with no hosted service doing the heavy lifting for you. By the end you will have a tool that can process a single image or a whole directory tree of them.</p><h2>The Challenge - Building a Background Removal Tool</h2><p>You are going to build a tool that accepts a photograph of a person, works out which pixels belong to the person and which belong to the background, and writes out a new image where the person is preserved and everything else is fully transparent.</p><p>The interesting parts of this challenge are the details. You have to decode common image formats, automatically find the person without any help from the user, produce edges that are smooth enough to look natural against a new background, and do all of the clever work on the user&#8217;s own device rather than shipping the image off to someone else&#8217;s server/API. Once the single-image case works, you will scale it up to handle whole directories at once while behaving gracefully when one image causes trouble.</p><p>This is an intermediate to advanced challenge. The core pipeline is the same whatever you build it with, so pick whatever you are most comfortable with for image processing and, if you go down the machine learning route, running a model without calling out to a hosted service.</p><h3>Step Zero</h3><p>In this introductory step you are going to set your environment up ready to begin developing and testing your solution.</p><p>Choose your target platform and programming language. I would encourage you to pick a language that has good libraries for decoding and encoding images, because you do not want to be writing a JPG decoder by hand this week. You will also want to think ahead to the person detection step. Many languages have bindings for running pre-trained segmentation models, so it is worth checking what is available before you commit.</p><p>It is entirely up to you how the user interacts with your tool. A command-line tool, a native desktop application, and a web app are all perfectly good choices, so pick whichever suits you and the way you like to work.</p><p>One constraint to keep in mind from the very start is that the background removal itself must not be handed off to a hosted third-party API. The important thing is that no one else&#8217;s server does the work of finding the person and removing the background. Where that computation runs is up to you, as long as it is under your control: running a model you have downloaded on the user&#8217;s machine is fine, and so is compiling it to WebAssembly and running it entirely in the browser so the image never leaves the user&#8217;s device. What is not in the spirit of things is uploading the image to a hosted background removal service and getting the answer back. Part of the fun here is understanding how the magic actually works, so you want to be the one doing it.</p><p>If you go down the machine learning route, be aware that a good segmentation model can be a sizeable download, often in the region of a couple of hundred megabytes. If you are on a slow or metered connection it is worth pulling the model down early so it is ready when you reach the detection step. This is worth bearing in mind for a browser-based build too, where that download becomes something the user&#8217;s browser has to fetch.</p><p>Gather a small set of test photographs to work with. You will want a few pictures of people, ideally with a mix of easy cases (a person against a plain wall) and harder ones (someone with loose or curly hair, or a busy background). Keep them somewhere handy, because you will run your tool against them again and again.</p><h3>Step 1</h3><p>In this step your goal is to load an image and write it back out as a PNG with the same dimensions and an alpha channel.</p><p>Before we go any further, it is worth being clear about what the alpha channel is, because it is central to the whole challenge. A colour image is made up of pixels, and each pixel usually has three values: how much red, green, and blue it contains. Each of those values normally runs from 0 to 255.</p><p>The alpha channel adds a fourth value to each pixel, on that same 0 to 255 scale, that records how opaque or transparent the pixel is. An alpha of 255 means the pixel is fully opaque and you see its colour completely. An alpha of 0 means it is fully transparent and you see straight through it to whatever is behind, as if that pixel were not there at all. Values in between give partial transparency, which is exactly what you will need later for soft edges.</p><p>So when this step asks you to make the whole image fully opaque, you are giving every pixel an alpha of 255. JPG images do not have an alpha channel, which is why they can never be transparent, but PNG images do. That is the reason the output of this challenge is always a PNG: it is the transparency, stored in the alpha channel, that lets the background disappear while the person stays.</p><p>Your tool should accept a single input image in either JPG or PNG format. Once it has decoded the image, it should write out a PNG that has an alpha channel and exactly the same pixel dimensions as the input. For now, do not try to remove anything. Just keep every pixel as it is and make the whole image fully opaque, giving every pixel the maximum alpha of 255. This gets your decode and encode pipeline working end to end before you add any of the clever detection logic.</p><p>Getting this foundation right matters. The rest of the challenge builds on top of a reliable &#8220;read an image, produce a same-sized PNG with alpha&#8221; pipeline, so it is worth making sure this is solid.</p><p><strong>Testing:</strong> Run your tool against a JPG and a PNG from your test set. Open the output files and confirm they look identical to the inputs. To check the dimensions and the alpha channel without having to eyeball them, reach for a command-line tool like <code>exiftool</code>:</p><pre><code><code>exiftool -ImageWidth -ImageHeight -ColorType output.png</code></code></pre><p>You should see the width and height matching your input exactly, and a colour type of <code>RGB with Alpha</code>, which confirms the alpha channel is present. If you prefer ImageMagick, <code>magick identify -format '%wx%h %[channels]\n' output.png</code> does the same job, printing something like <code>1200x800 srgba</code>, where the <code>a</code> on the end tells you there is an alpha channel.</p><h3>Step 2</h3><p>In this step your goal is to automatically detect the person in the image and produce a mask.</p><p>This is the heart of the challenge. Your tool needs to work out, for every pixel, whether it belongs to the person or to the background, and it must do this automatically. The user should not have to click, draw, or mark anything to point out where the person is. Given a photo, your tool figures it out on its own.</p><p>The output of this step is a mask, which is simply a second image the same size as the input where each pixel says how much that location belongs to the person. A common approach is to run a pre-trained person segmentation model yourself and use its output as your mask. Remember the no-hosted-service rule from Step Zero applies here.</p><p>You do not have to invent the detection method from scratch, and there is a lot of prior art to lean on. Here are some starting points worth a look, and it is up to you which route you take:</p><ul><li><p><strong>U&#178;-Net</strong> is a salient object detection network that powers a lot of open-source background removers. See the paper &#8220;U^2-Net: Going Deeper with Nested U-Structure for Salient Object Detection&#8221; (<a href="https://arxiv.org/abs/2005.09007">arxiv.org/abs/2005.09007</a>) and the code at <a href="https://github.com/xuebinqin/U-2-Net">github.com/xuebinqin/U-2-Net</a>. The popular <code>rembg</code> library (<a href="https://github.com/danielgatis/rembg">github.com/danielgatis/rembg</a>) is built on it and makes a good reference point for what &#8220;good enough&#8221; looks like.</p></li><li><p><strong>DeepLabv3 and DeepLabv3+</strong> are general semantic segmentation models that include a &#8220;person&#8221; class and ship ready to use in libraries such as torchvision. The paper is &#8220;Rethinking Atrous Convolution for Semantic Image Segmentation&#8221; (<a href="https://arxiv.org/abs/1706.05587">arxiv.org/abs/1706.05587</a>).</p></li><li><p><strong>MediaPipe Image Segmenter and Selfie Segmentation</strong> from Google (<a href="https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter">ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter</a>) are designed to run efficiently on-device, including in the browser through WebAssembly, which makes them a natural fit if you are building a web app.</p></li><li><p><strong>Segment Anything (SAM)</strong> from Meta (<a href="https://segment-anything.com/">segment-anything.com</a>, paper at <a href="https://arxiv.org/abs/2304.02643">arxiv.org/abs/2304.02643</a>) is a powerful, more recent option that can produce very clean masks.</p></li><li><p><strong>MODNet</strong> (<a href="https://arxiv.org/abs/2011.11961">arxiv.org/abs/2011.11961</a>, <a href="https://github.com/ZHKKKe/MODNet">github.com/ZHKKKe/MODNet</a>) and <strong>BiRefNet</strong> (<a href="https://github.com/ZhengPeng7/BiRefNet">github.com/ZhengPeng7/BiRefNet</a>) focus on high-quality matting and fine edges, which will be especially handy when you reach the hair in Step 4.</p></li></ul><p>Whichever you pick, you will typically load and run it with a runtime such as ONNX Runtime, PyTorch, or TensorFlow, all of which can run on the user&#8217;s own device and so keep you within the no-hosted-service rule.</p><p>If you would rather build the detector yourself from scratch instead of reaching for a trained model, that is a brilliant way to learn, and the trick is to start with the easy case and work up. A good first option is to lean on the plain background in your easy test photo: treat the pixels around the border of the image as background, then grow outwards to every connected pixel of a similar colour, and whatever is left in the middle is your person. That is really just two classic ideas stitched together, colour keying and a flood fill, and it will give you a usable mask on a simple photo with no machine learning at all. From there, a natural step up is GrabCut, a well-known graph-cut foreground extraction algorithm (the paper is &#8220;GrabCut: Interactive Foreground Extraction using Iterated Graph Cuts&#8221;, <a href="https://dl.acm.org/doi/10.1145/1015706.1015720">dl.acm.org/doi/10.1145/1015706.1015720</a>, and there is a clear walkthrough in the OpenCV docs at <a href="https://docs.opencv.org/4.x/d8/d83/tutorial_py_grabcut.html">docs.opencv.org/4.x/d8/d83/tutorial_py_grabcut.html</a>). GrabCut normally wants a rough box around the subject, which you can supply automatically by assuming the person is roughly central or by running a lightweight face detector such as an OpenCV Haar cascade to locate them. Other classic building blocks worth knowing are Otsu&#8217;s thresholding (<a href="https://en.wikipedia.org/wiki/Otsu%27s_method">en.wikipedia.org/wiki/Otsu%27s_method</a>) and the watershed algorithm. A from-scratch approach will not match a modern model on messy backgrounds or fine hair, but it will teach you a great deal about why those models exist.</p><p>To make this easy to inspect, have your tool save the mask out as its own greyscale image while you are developing. That way you can look at it directly and see how well the detection is working before you use it for anything.</p><p><strong>Testing:</strong> Run your detection against your test photos and save the masks. Open them and check that the person shows up as the bright, filled-in region and the background shows up as dark. Try your easy photo first (a person against a plain wall) and then a harder one. The mask does not have to be perfect yet, but the overall shape of the person should be clearly recognisable.</p><h3>Step 3</h3><p>In this step your goal is to use the mask to make the background transparent.</p><p>Now that you have a mask, combine it with the original image. Every pixel that the mask says belongs to the person should keep its original colour value untouched. Every pixel that belongs to the background should be made fully transparent by setting its alpha to zero. The result is a PNG, still the same dimensions as the input, where the person floats on a transparent background.</p><p><strong>Testing:</strong> Run the full pipeline on your test photos and open the output PNGs. The quickest visual check is to composite each cut-out onto a bright, solid colour that contrasts with the original photo, for example a strong red or green. With ImageMagick you can build that composite from the command line:</p><pre><code><code>magick output.png -background magenta -flatten check.png</code></code></pre><p>Open <code>check.png</code> and the person should appear against magenta with none of the original background showing through, and their colours should match the original photo.</p><p>You can also automate a few checks with ImageMagick so you do not have to rely on your eyes at all:</p><pre><code><code># There should now be some transparent pixels, so this prints False
magick identify -format '%[opaque]\n' output.png

# A corner pixel is almost always background, so its alpha should be 0
magick output.png -format '%[pixel:p{0,0}]\n' info:

# The average alpha across the image should sit between 0 and 1,
# roughly the fraction of the picture the person fills
magick output.png -alpha extract -format '%[fx:mean]\n' info:</code></code></pre><p>The first command should print <code>False</code>, confirming the image is no longer fully opaque. The corner pixel should come back fully transparent, something like <code>srgba(0,0,0,0)</code>. The mean alpha should be greater than 0 and less than 1, which tells you some of the picture has been kept and some made transparent, rather than the whole thing being wiped out or left untouched.</p><h3>Step 4</h3><p>In this step your goal is to produce smooth edges around the person, including the tricky areas like hair.</p><p>If you composited your Step 3 output onto a new background, you probably noticed a hard, jagged edge, and hair almost certainly looked like it had been cut out with blunt scissors. Real photographs do not have a crisp line where a person ends and the background begins. Strands of hair are semi-transparent, and edges blend gradually. To make a cut-out that composites cleanly onto a new background, the alpha channel around the person needs to transition smoothly rather than jumping straight from fully opaque to fully transparent.</p><p>Refine the edges of your cut-out so that boundary pixels take on partial transparency. Pay special attention to hair, which is the classic hard case and the thing that separates a convincing cut-out from an obvious one.</p><p><strong>Testing:</strong> Composite your refined output onto a new background again, ideally one that is quite different in colour from the original. The edges should look soft and natural, and wisps of hair should blend into the new background rather than showing a harsh outline or a fringe of the old background colour. Compare a before and after against your Step 3 result on a photo of someone with loose hair. The difference should be obvious. It is also worth peeking at the alpha channel on its own, for example with <code>magick output.png -alpha extract edge-check.png</code>, where you should see a soft grey halo around the person rather than a hard black-and-white boundary.</p><h3>Step 5</h3><p>In this step your goal is to give the user control over the output filename. We&#8217;re half way so this is an easy one to give you a quick rest.</p><p>Your tool should let the user specify where the output should be written. When they do not specify a name, your tool should choose a sensible default derived from the input filename. Crucially, the default must never overwrite the input file. Someone processing <code>photo.jpg</code> should get their cut-out saved somewhere else, not have their original silently replaced.</p><p><strong>Testing:</strong> Run your tool on an input without specifying an output name and confirm a sensibly named output file appears and that the original input is untouched. Then run it again specifying an explicit output name and confirm the file lands exactly where you asked. For good measure, try an input that is already a PNG and confirm the default output name does not collide with it.</p><h3>Step 6</h3><p>In this step your goal is to handle invalid input files cleanly.</p><p>Not every file with a <code>.jpg</code> or <code>.png</code> extension is actually a valid image, and users will inevitably point your tool at something that is not an image at all. When your tool is given a file that is not a valid JPG or PNG, it should report a clear error message explaining what went wrong and fail with a non-zero exit status (or the equivalent error signal for whatever interface you built). It should not crash with an unhelpful stack trace, and it should certainly not write out a broken or empty output file.</p><p>One thing that catches people out here is truncated JPEGs. Many mainstream JPEG decoders are deliberately tolerant and will happily decode a truncated file into a partial image without raising any error at all, so a test you expect to fail passes instead. If you hit this, you may need to validate the file yourself rather than relying on the decoder, for example by checking that a JPEG ends with its end-of-image marker. Truncated PNGs, by contrast, tend to fail cleanly almost everywhere, so they are a gentler test case.</p><p><strong>Testing:</strong> Start with the easy case. Create a text file, rename it to have a <code>.jpg</code> extension, and run your tool against it. You should see a clear error message and, if your tool is a command-line one, a non-zero exit status.</p><p>Next, make a truncated JPEG by keeping only the first slice of a valid one, which chops off the end of the image data and the end-of-image marker:</p><pre><code><code>head -c 2000 valid.jpg &gt; truncated.jpg</code></code></pre><p>Do the same for a PNG:</p><pre><code><code>head -c 500 valid.png &gt; truncated.png</code></code></pre><p>Run your tool against <code>truncated.jpg</code> and <code>truncated.png</code>. Both should be rejected with a clear error and a non-zero exit status, and no output file should be produced in any of these cases. The truncated JPEG is the one to watch, since a tolerant decoder may quietly accept it. If that happens, this is exactly the case the note above is warning you about, and it is a sign you need to validate the file yourself.</p><h3>Step 7</h3><p>In this step your goal is to process an entire directory of images.</p><p>Instead of just a single image, your tool should also accept a directory as input. When given a directory, it should find and process every JPG and PNG image within it, and it should do so recursively, descending into subdirectories to find images nested further down. Each image gets the same background removal treatment you built in the earlier steps.</p><p><strong>Testing:</strong> Create a directory with a few images in it, plus a subdirectory containing a couple more. Point your tool at the top-level directory and confirm that every image, including the ones in the subdirectory, gets processed. Drop a non-image file into the directory as well and confirm your tool sensibly ignores it rather than trying to process it.</p><h3>Step 8</h3><p>In this step your goal is to mirror the input directory structure in the output.</p><p>When processing a directory, the output needs to be organised, not dumped into a single flat folder where files from different subdirectories collide. Your tool should mirror the input directory structure under the output directory. In other words, each output image should be written to the same relative path as its source image. An image at <code>people/holiday/beach.jpg</code> in the input should produce a output file at the matching relative location under the output directory.</p><p><strong>Testing:</strong> Run your tool against the nested directory from Step 7. Inspect the output directory and confirm its structure mirrors the input. The output image for the image in the subdirectory should sit inside a matching subdirectory in the output folder, at the same relative path as its source. There is a subtle collision to watch for as well: because every output is a PNG, an input folder containing both <code>portrait.jpg</code> and <code>portrait.png</code> would map both output images to the same output name and one would clobber the other. Put a matching pair like this in a folder, run your tool, and decide how you want to handle it so that no image is silently lost.</p><h3>Step 9</h3><p>In this step your goal is to keep going when one image fails.</p><p>When you are batch processing a whole directory, it is frustrating if the entire run stops dead because one image out of a hundred was corrupt. Your tool should continue processing the remaining images when one of them fails, rather than aborting the whole run. Then, once it has finished working through everything, it should report the failures at the end so the user knows exactly which images did not get processed and why. Since a single bad file fails with a non-zero exit status (as you built in Step 6), a batch run that had any failures should also finish with a non-zero exit status once it has processed everything, so that scripts calling your tool can tell the run was not completely clean.</p><p><strong>Testing:</strong> Build a directory containing several valid images plus one invalid file (reuse the corrupt file from Step 6). Run your tool over the directory and confirm that all of the valid images are processed and written out correctly. At the end of the run, you should see a clear summary listing the file that failed, and, for a command-line tool, a non-zero exit status. Confirm that the one bad file did not prevent any of the good ones from being processed.</p><h3>Going Further</h3><p>Once you have the core tool working, here are some ideas to take it further:</p><ul><li><p>Add a preview or &#8220;trimap&#8221; mode that shows the mask and the refined edges side by side so you can debug tricky images.</p></li><li><p>Let the user replace the transparent background with a solid colour or another image, turning your tool into a full background replacement tool.</p></li><li><p>Support additional input formats such as WebP or HEIC.</p></li><li><p>Add a progress indicator when batch processing large directories, and parallelise the work across multiple CPU cores or a GPU.</p></li><li><p>Try more than one segmentation model and let the user choose, or compare their results to see which handles hair and fine detail best.</p></li><li><p>Handle photos with more than one person in them, and give the user a way to keep only a specific person.</p></li><li><p>Benchmark how long each image takes and see where the time goes, then look for ways to speed up the slowest stage.</p></li><li><p>If you built a native tool, try a browser-based version compiled to WebAssembly (or the other way round) so people can use it without installing anything.</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 #125 - Online Diff Viewer]]></title><description><![CDATA[This challenge is to build your own online diff viewer.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-125-online-diff</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-125-online-diff</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 04 Jul 2026 08:01:27 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7740c43e-ac36-4479-bd08-db7f7f555ade_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><h1>Thank You For All The Feedback!</h1><p>Before we kick of this week&#8217;s coding challenge, I&#8217;d like to thank everyone for all the feedback. I got a lot and am still working my way through it. I really appreciate everyone taking the time to get let me know your thoughts.</p><p>Thank you! &#128591;&#128525;</p><h2>Coding Challenge #125 - Online Diff Viewer</h2><p>This challenge is to build your own online diff viewer that compares two text inputs and displays the differences in a clear, visual format.</p><p>If you&#8217;ve ever used GitHub, GitLab, or any code review tool, you&#8217;ve seen a diff viewer. It&#8217;s the side-by-side or unified view that shows you exactly what changed between two versions of a file - green for additions, red for deletions. But how do these tools actually work? In this challenge you&#8217;ll find out by building one yourself. You&#8217;ll implement a diff algorithm, render the results in multiple visual formats, and add all the quality-of-life features that make a diff viewer truly useful.</p><h2>The Challenge - Building Your Own Online Diff Viewer</h2><p>You&#8217;re going to build an online diff viewer that runs entirely in the browser. Users will paste, upload, or drag-and-drop two pieces of text, and your tool will show them exactly what changed. Over several steps you&#8217;ll add side-by-side and unified views, syntax highlighting, navigation tools, export options, and accessibility features.</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>This is a web application, so you&#8217;ll need HTML, CSS, and JavaScript (or a framework you&#8217;re comfortable with). You&#8217;ll also need to understand how a diff algorithm works.</p><p>Take some time to study the <a href="http://www.xmailserver.org/diff2.pdf">Myers diff algorithm</a>. It&#8217;s the algorithm used by Git and many other tools. Your diff viewer will need to take two sequences of lines, compute the shortest edit script that transforms one into the other, and produce a set of additions, deletions, and unchanged lines.</p><h3>Step 1</h3><p>In this step your goal is to compute and display a basic line-level diff between two text inputs.</p><p>Create a simple interface with two text areas for input and a diff button. When the user clicks the button, your tool should compute the diff between the two inputs and display the result. Colour-code additions in green, deletions in red, and leave unchanged lines uncoloured or in a neutral colour.</p><p>At this stage a simple vertical, unified-style output is fine - we&#8217;ll add proper views in the next steps.</p><p><strong>Testing:</strong> Paste the following into your two inputs:</p><p>Left (original):</p><pre><code><code>function greet(name) {
  console.log("Hello, " + name);
}

function farewell(name) {
  console.log("Goodbye, " + name);
}</code></code></pre><p>Right (modified):</p><pre><code><code>function greet(name) {
  console.log("Hello, " + name + "!");
  console.log("Have a great day!");
}

function farewell(name) {
  console.log("Goodbye, " + name + "!");
}</code></code></pre><p>You should see the added lines in green and any lines that exist only in the original in red. Unchanged lines like <code>function greet(name) {</code> should appear without colour.</p><h3>Step 2</h3><p>In this step your goal is to add a side-by-side diff view with properly aligned rows.</p><p>Side-by-side views show both inputs next to each other, with matching lines on the same row and gaps where lines were added or removed. This is the most intuitive way to view diffs for many people.</p><p>Your side-by-side view should:</p><ul><li><p>Show the original on the left and the modified version on the right.</p></li><li><p>Align unchanged lines on the same row.</p></li><li><p>Leave blank gaps on one side when lines are added or removed.</p></li><li><p>Show line numbers for both sides.</p></li></ul><p><strong>Testing:</strong> Use the same test data from Step 1. In the side-by-side view, the two <code>console.log</code> lines in the modified version should appear opposite blank space on the left, and the original <code>console.log("Goodbye, " + name)</code> line should appear opposite blank space on the right.</p><h3>Step 3</h3><p>In this step your goal is to implement a unified diff view and let users toggle between the two views.</p><p>The unified diff view is the format used by Git and many command-line tools. It shows a single continuous output where additions are prefixed with <code>+</code>, deletions with <code>-</code>, and context lines with a space. Hunks are separated by <code>@@</code> headers showing the line ranges.</p><p>Your tool should allow the user to switch between side-by-side and unified views with a simple toggle button or tab.</p><p><strong>Testing:</strong> Switch between views with the same test data. In unified view, verify that added lines start with <code>+</code>, deleted lines with <code>-</code>, and context lines with a space. The hunk header format should show something like <code>@@ -2,5 +2,6 @@</code>.</p><h3>Step 4</h3><p>In this step your goal is to display diff summary statistics.</p><p>Users want to quickly understand the scope of changes. Your viewer should show a summary panel that displays:</p><ul><li><p>Lines added</p></li><li><p>Lines removed</p></li><li><p>Lines unchanged</p></li><li><p>Total number of changes</p></li></ul><p>Update this summary whenever the diff is recomputed.</p><p><strong>Testing:</strong> With the test data from Step 1, verify your summary shows 2 lines added, 1 line removed, and the correct number of unchanged lines. Change the inputs and confirm the statistics update accordingly.</p><h3>Step 5</h3><p>In this step your goal is to add syntax highlighting for at least five common programming languages.</p><p>Diff output is much easier to read when the code is syntax-highlighted. Your tool should highlight at least JavaScript, Python, HTML, CSS, and JSON.</p><p>The syntax highlighting should apply to both the original and modified sides. It should also work with the colour coding from the diff - so a highlighted Python keyword that appears in an added line would show with both the syntax colour and a green background.</p><p>Your viewer should attempt to auto-detect the language based on file extension or content, and also allow the user to manually override the language selection.</p><p><strong>Testing:</strong> Create test files in each supported language and verify the highlighting works correctly. Try mixing file types (JavaScript on the left, Python on the right) and verify the language override lets you pick the correct one. Test auto-detection by uploading a <code>.py</code> file and confirming Python highlighting is applied.</p><h3>Step 6</h3><p>In this step your goal is to support multiple input methods for loading text into the diff viewer.</p><p>Typing or pasting into text areas works, but users will also want to:</p><ul><li><p>Upload files from their computer using a file picker.</p></li><li><p>Drag and drop files directly onto the viewer.</p></li><li><p>Fetch content from a URL.</p></li></ul><p>When a file is loaded, display basic metadata such as the filename, file size, detected language, and line count.</p><p><strong>Testing:</strong> Create two text files on your computer and upload them using the file picker. Verify the content appears correctly in both sides and the metadata is displayed. Test drag and drop by dragging a file from your desktop onto the viewer. Test URL fetch by providing the URL of a publicly accessible text file.</p><h3>Step 7</h3><p>In this step your goal is to add diff precision modes: smart, word, and character-level highlighting.</p><p>So far your diff works at the line level. But within a changed line, only part of the line might have changed. Precision modes let users see exactly which words or characters changed:</p><ul><li><p><strong>Smart mode</strong>: Your best guess at what changed - typically word-level changes.</p></li><li><p><strong>Word mode</strong>: Highlight changed words within a line.</p></li><li><p><strong>Character mode</strong>: Highlight individual changed characters within a line.</p></li></ul><p><strong>Testing:</strong> Use a line like changing <code>Hello World</code> to <code>Hello Universe</code>. In word mode, only <code>World</code> should be highlighted on the left and <code>Universe</code> on the right. In character mode, individual characters like <code>W</code> vs <code>U</code> should be highlighted. Verify smart mode makes a sensible choice.</p><h3>Step 8</h3><p>In this step your goal is to add diff control options that let users customise what counts as a change.</p><p>Sometimes you want to focus on meaningful changes and ignore formatting differences. Add toggleable options for:</p><ul><li><p>Ignoring whitespace changes (e.g., extra spaces that don&#8217;t affect the code).</p></li><li><p>Ignoring case differences (e.g., <code>HELLO</code> vs <code>hello</code>).</p></li><li><p>Ignoring blank lines (empty lines added or removed).</p></li><li><p>Ignoring indentation-only changes (tabs vs spaces, changes in indent level).</p></li><li><p>Ignoring line ending differences (CRLF vs LF).</p></li></ul><p>These options should be available as checkboxes or a settings panel and take effect immediately when changed.</p><p><strong>Testing:</strong> Create two inputs that differ only in whitespace. With &#8220;ignore whitespace&#8221; enabled, the diff should show no changes. Disable it and the changes should reappear. Repeat for each option.</p><h3>Step 9</h3><p>In this step your goal is to improve large diff readability by letting users hide unchanged sections.</p><p>When there are hundreds of lines of unchanged code between changes, scrolling through all of them is tedious. Add the ability to collapse unchanged sections, showing a clickable indicator like &#8220;... 47 unchanged lines ...&#8221;. Users should be able to click the indicator to expand the section.</p><p><strong>Testing:</strong> Create two files with at least 50 unchanged lines between two change blocks. Verify the middle section is collapsed by default or can be collapsed, and that clicking it expands to show the full content.</p><h3>Step 10</h3><p>In this step your goal is to add a line wrap toggle and synchronised scrolling.</p><p>When lines are long, users need to be able to toggle line wrapping on and off. With wrapping off, a horizontal scrollbar lets them view long lines without breaking formatting.</p><p>Synchronised scrolling keeps both sides of the side-by-side view scrolling together. When the user scrolls one pane, the other follows. Users should also be able to disable this if they prefer independent scrolling.</p><p><strong>Testing:</strong> Create a file with very long lines (200+ characters) and verify toggling line wrap works correctly. In side-by-side view, scroll one pane and verify the other scrolls in sync. Disable sync scrolling and verify the panes scroll independently.</p><h3>Step 11</h3><p>In this step your goal is to add comprehensive navigation features.</p><p>Navigating a large diff efficiently is critical. Add the following:</p><ul><li><p><strong>Next change</strong> and <strong>previous change</strong> buttons that jump between diff hunks.</p></li><li><p><strong>First change</strong> and <strong>last change</strong> buttons that jump to the start and end of the diff.</p></li><li><p>A <strong>change navigator panel</strong> that lists all change blocks, showing the line range and a preview. Clicking a block in the panel should jump to that location.</p></li><li><p><strong>Gutter markers</strong> (coloured indicators in the margin) for additions, deletions, and modifications.</p></li><li><p><strong>Keyboard shortcuts</strong> for all navigation actions (e.g., <code>J</code> for next, <code>K</code> for previous).</p></li></ul><p><strong>Testing:</strong> Create a diff with at least 10 change blocks spread across the document. Verify each navigation control works correctly. Test all keyboard shortcuts and confirm the change navigator panel updates to highlight the current change as you navigate.</p><h3>Step 12</h3><p>In this step your goal is to handle large inputs efficiently.</p><p>Diffs of 10,000+ lines can freeze the browser if not handled carefully. To keep the UI responsive:</p><ul><li><p>Run the diff computation in a Web Worker so the main thread stays responsive.</p></li><li><p>Use virtualised rendering - only render the lines currently visible in the viewport instead of all 10,000 at once.</p></li><li><p>Show a progress indicator while the diff is being computed.</p></li></ul><p><strong>Testing:</strong> Generate two large text files (10,000+ lines) with differences scattered throughout. Verify the diff computation doesn&#8217;t freeze the browser - you should be able to click buttons and scroll while it&#8217;s processing. Verify scrolling through the output is smooth.</p><h3>Step 13</h3><p>In this step your goal is to add export and copy options.</p><p>Users need to get the diff output out of your tool and into other tools or documents. Add the ability to:</p><ul><li><p>Copy the full diff output to the clipboard.</p></li><li><p>Copy individual change blocks.</p></li><li><p>Export the diff as a unified patch file, HTML, Markdown, and JSON.</p></li><li><p>Generate Git-compatible patch output that can be applied with <code>git apply</code>.</p></li></ul><p><strong>Testing:</strong> Create a diff and test each export format. For unified patch, verify you can save it to a file and apply it with <code>git apply</code>. For HTML and Markdown, verify the output renders correctly with colour coding preserved. For JSON, verify the structure includes all change information.</p><h3>Step 14</h3><p>In this step your goal is to make your diff viewer a Progressive Web App (PWA) that works offline.</p><p>Turn your diff viewer into a PWA with a service worker so users can use it without an internet connection. Additionally:</p><ul><li><p>Persist the user&#8217;s recent sessions using localStorage or IndexedDB.</p></li><li><p>Show a history of previous diffs the user can revisit.</p></li><li><p>Let users save named diff sessions they can come back to later.</p></li></ul><p><strong>Testing:</strong> Load your diff viewer, then disconnect from the internet. Verify the page still loads and the diff viewer works. Run some diffs, close the browser, reopen it offline, and verify your history is still there.</p><h3>Step 15</h3><p>In this step your goal is to ensure your diff viewer is fully accessible and works well on all devices.</p><p>Your viewer should be usable by everyone, regardless of ability or device. This means:</p><ul><li><p>All functionality should be keyboard-accessible.</p></li><li><p>Proper ARIA labels and roles should be used throughout.</p></li><li><p>Screen readers should announce change summaries when the diff is computed.</p></li><li><p>Line numbers and file headers should remain visible (stick to the top) when scrolling.</p></li><li><p>The layout should be responsive - usable on desktop, tablet, and mobile.</p></li><li><p>Clear empty states and error messages should be shown when something goes wrong.</p></li></ul><p><strong>Testing:</strong> Navigate the entire viewer using only the keyboard. Run a screen reader and verify it announces the diff results. Resize the browser to mobile width and verify everything remains usable. Test with an empty input to verify the empty state message is shown.</p><h3>Step 16</h3><p>In this step your goal is to add theme support with light, dark, and high-contrast modes.</p><p>Your diff viewer should respect the user&#8217;s system theme preference by default. Add a theme selector that lets users choose between:</p><ul><li><p>Light mode</p></li><li><p>Dark mode</p></li><li><p>High-contrast mode (for users with visual impairments)</p></li></ul><p>The theme should apply to all UI elements including the diff output, syntax highlighting colours, and control panels.</p><p><strong>Testing:</strong> Change your system theme and verify the viewer matches. Manually switch themes and verify all colours update correctly. Test high-contrast mode with a screen reader or accessibility testing tool.</p><h3>Going Further</h3><p>Once you&#8217;ve built the core diff viewer, consider adding these advanced features:</p><ul><li><p><strong>AI-powered diff explanation</strong>: Use an LLM to explain what a change does in plain English.</p></li><li><p><strong>Code-review mode</strong>: Let users add comments and annotations to specific lines or change blocks.</p></li><li><p><strong>Semantic diffs</strong>: For JSON, compare the structure rather than the text. The same for HTML - compare the DOM tree.</p></li><li><p><strong>Syntax validation</strong>: Highlight JSON or HTML that is syntactically invalid in the input.</p></li><li><p><strong>Three-way merge view</strong>: Show a base version and two modified versions, useful for resolving merge conflicts.</p></li><li><p><strong>Image diff</strong>: Support uploading two images and showing a visual diff (pixel-by-pixel or structural comparison).</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[What should Coding Challenges become next?]]></title><description><![CDATA[Something needs to change, and I&#8217;d like you to help shape it.]]></description><link>https://codingchallenges.substack.com/p/what-should-coding-challenges-become</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/what-should-coding-challenges-become</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 20 Jun 2026 08:02:12 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2255881a-1835-4d4f-a686-4df7144264db_4032x3024.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey,</p><p>I&#8217;d love your help with something.</p><p>I&#8217;ve noticed that fewer people are opening and reading Coding Challenges each week. That tells me one of two things is probably true:</p><p>Either life is busy and inboxes are noisy, which I completely understand.</p><p>Or the newsletter isn&#8217;t giving you enough value right now.</p><p>I don&#8217;t want to keep sending you things that aren&#8217;t useful. I&#8217;d much rather shape this into something you genuinely look forward to opening.</p><p>So I have one question:</p><p>What would make Coding Challenges worth reading every week?</p><p>You can reply with a number if that&#8217;s easiest:</p><ol><li><p>More coding challenges and practical projects</p></li><li><p>More AI, LLMs, and generative AI content</p></li><li><p>More software engineering, architecture, testing, and system design</p></li></ol><p>Or reply with your own answer.</p><p>Some things I&#8217;d be happy to write about include:</p><ul><li><p>Learning to code</p></li><li><p>Learning AI</p></li><li><p>Building AI products</p></li><li><p>LLMs and generative AI</p></li><li><p>Deploying AI systems.</p></li><li><p>System design and architecture</p></li><li><p>Testing and maintenance</p></li><li><p>Engineering excellence</p></li><li><p>Programming languages</p></li><li><p>Databases</p></li><li><p>Cloud platforms</p></li><li><p>AI tooling</p></li><li><p>Developer productivity</p></li></ul><p>I know that&#8217;s a broad list, so don&#8217;t feel limited by it. The main thing I want to know is what would actually help you.</p><p>If you&#8217;re happy to share publicly, please leave a comment. It would help build a better conversation around where Coding Challenges should go next.</p><p>If you&#8217;d rather keep it private, just hit reply and email me directly.</p><p>And if this newsletter isn&#8217;t for you anymore, no hard feelings at all. Please unsubscribe. I&#8217;d rather not keep bothering people who don&#8217;t want these emails.</p><p>Thanks for reading, and thanks in advance if you reply.</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #124 - Du]]></title><description><![CDATA[This challenge is to build your own du.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-124-du</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-124-du</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 13 Jun 2026 08:01:14 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ef145ce3-ea31-429b-b2e3-0966c198fb71_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 #124 - Du</h2><p>This challenge is to build your own version of <code>du</code>, the classic POSIX disk usage estimation utility.</p><p><code>du</code> is one of those utilities you reach for when your disk is filling up and you need to know where all the space has gone. It walks a directory tree, adds up how much disk space each directory consumes, and prints a summary. It handles symbolic links, hard links, glob exclusions, human-readable units, depth limits, and more. Building your own version is a fantastic exercise in filesystem traversal, careful bookkeeping of inodes, and writing a tool that plays nicely with the rest of the Unix ecosystem.</p><h2>The Challenge - Building Du</h2><p>In this challenge you&#8217;re going to build your own version of <code>du</code>, a disk usage estimation tool. Your tool will recursively walk directory trees, calculate disk usage for files and directories, and display the results in a variety of formats. It will be compatible with the POSIX <code>du</code> utility, which means you&#8217;ll be able to test your work directly against the system <code>du</code>.</p><p>The interesting parts of <code>du</code> are the details: counting disk blocks rather than byte sizes, avoiding double-counting hard-linked files by tracking inodes, following or skipping symbolic links, and presenting the data in ways that are actually useful when you&#8217;re trying to figure out why your disk is full.</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 good access to filesystem metadata, things like file sizes, inode numbers, and modification times. Most general-purpose languages have libraries for this, so you should be well covered.</p><p>Before you start coding, spend some time with the system <code>du</code>. Run it on a few directories, try the various flags, and get a feel for the output format. Have a read through the <a href="https://pubs.opengroup.org/onlinepubs/9699919799/utilities/du.html">POSIX du specification</a> and the man page on your own machine (<code>man du</code>).</p><p>Create a small test directory structure to use throughout the challenge:</p><pre><code><code>mkdir -p testdir/subdir1/deep
mkdir -p testdir/subdir2
echo "hello world" &gt; testdir/file1.txt
echo "this is a slightly longer file with more content in it" &gt; testdir/subdir1/file2.txt
echo "deep file" &gt; testdir/subdir1/deep/file3.txt
echo "another file" &gt; testdir/subdir2/file4.txt</code></code></pre><p>We&#8217;ll use <code>testdir</code> as our playground throughout the challenge.</p><h3>Step 1</h3><p>In this step your goal is to recursively calculate and display the disk usage of directories.</p><p>Your tool should walk a directory tree starting from a given path, sum up the disk usage of all files it finds, and print the total for each directory. The output should be in 512-byte blocks (the POSIX standard default), with the size followed by the directory path. Directories should be printed in traversal order, deepest entries first, with parent directories printed after their children.</p><p>When no path is specified, your tool should default to the current directory.</p><p>A 512-byte block is the traditional Unix unit for disk usage. On most systems you can calculate this by taking the file size in bytes, dividing by 512, and rounding up. A zero-byte file still occupies one block on most filesystems, but for this step you can treat empty files as zero blocks to keep things simple.</p><p><strong>Testing:</strong> Run your tool against the test directory:</p><pre><code><code>ccdu testdir</code></code></pre><p>You should see output similar to:</p><pre><code><code>8&#9;testdir/subdir1/deep
16&#9;testdir/subdir1
8&#9;testdir/subdir2
32&#9;testdir</code></code></pre><p>The exact numbers will vary depending on your filesystem and file contents. The key thing is that each directory shows a total, deeper directories appear before their parents, and the final line shows the total for the root of the tree.</p><h3>Step 2</h3><p>In this step your goal is to add the <code>-h</code>, <code>-k</code>, and <code>-m</code> flags for different size units.</p><p>By default your tool outputs in 512-byte blocks. The <code>-h</code> flag should switch to human-readable format, showing sizes with suffixes like K, M, G, and T (so <code>1.5M</code> for 1.5 megabytes, for example). The <code>-k</code> flag should show sizes in kilobytes (1024-byte units), and the <code>-m</code> flag should show sizes in megabytes.</p><p>Have a play with the system <code>du -h</code> to see how it formats sizes, it rounds to a sensible number of decimal places and picks the largest unit that keeps the number readable.</p><p><strong>Testing:</strong></p><pre><code><code>ccdu -h testdir
ccdu -k testdir
ccdu -m testdir</code></code></pre><p>The <code>-h</code> output should show sizes like <code>4.0K</code> or <code>1.2M</code>. The <code>-k</code> output should show whole numbers in kilobytes. The <code>-m</code> output should show sizes in megabytes.</p><h3>Step 3</h3><p>In this step your goal is to add the <code>-s</code> (summary) flag.</p><p>When <code>-s</code> is passed, your tool should display only a total for each argument path, skipping the per-directory breakdown entirely. This is the flag people reach for when they just want to know how big a directory is without seeing the whole tree.</p><p><strong>Testing:</strong></p><pre><code><code>ccdu -s testdir</code></code></pre><p>This should produce a single line with the total size of <code>testdir</code>. Try it with multiple paths:</p><pre><code><code>ccdu -s testdir testdir/subdir1</code></code></pre><p>This should produce one summary line per path.</p><h3>Step 4</h3><p>In this step your goal is to add the <code>-c</code> flag for a grand total.</p><p>When <code>-c</code> is passed alongside multiple path arguments, your tool should print a grand total line at the end of the output, labelled <code>total</code>. This is useful when you want to know the combined size of several directories at once.</p><p><strong>Testing:</strong></p><pre><code><code>ccdu -s -c testdir testdir/subdir1 testdir/subdir2</code></code></pre><p>You should see a summary line for each path, followed by a <code>total</code> line that is the sum of all three.</p><h3>Step 5</h3><p>In this step your goal is to add the <code>-a</code> flag to show individual file sizes.</p><p>By default <code>du</code> only shows sizes for directories. With <code>-a</code>, it should also print a line for every file it encounters, showing that file&#8217;s disk usage alongside its path. Files appear in traversal order, interleaved with the directory totals.</p><p><strong>Testing:</strong></p><pre><code><code>ccdu -a testdir</code></code></pre><p>You should see lines for each file (<code>file1.txt</code>, <code>file2.txt</code>, etc.) as well as for each directory.</p><h3>Step 6</h3><p>In this step your goal is to add the <code>-d</code> / <code>--max-depth</code> flag to limit output depth.</p><p>The <code>-d</code> flag takes a number and restricts output to entries at or below that depth relative to the starting path. <code>-d 0</code> is equivalent to <code>-s</code> (summary only). <code>-d 1</code> shows the root directory and its immediate children, but nothing deeper. This is incredibly useful when you have a deep tree and only care about the top-level breakdown.</p><p><strong>Testing:</strong></p><pre><code><code>ccdu -d 0 testdir
ccdu -d 1 testdir</code></code></pre><p>The first should show only the total for <code>testdir</code>. The second should show <code>testdir</code> plus <code>subdir1</code> and <code>subdir2</code>, but not <code>subdir1/deep</code> or any files.</p><h3>Step 7</h3><p>In this step your goal is to add the <code>--exclude</code> flag to skip files matching a glob pattern.</p><p>The <code>--exclude</code> flag takes a glob pattern (like <code>*.log</code> or <code>tmp*</code>) and any file or directory whose name matches the pattern should be skipped entirely, it should not be counted in any totals and should not appear in the output. Multiple <code>--exclude</code> flags can be specified.</p><p><strong>Testing:</strong></p><pre><code><code>ccdu -a exclude "*.txt" testdir</code></code></pre><p>This should produce no file lines (since all our test files are <code>.txt</code>) and all directory totals should be zero or minimal.</p><h3>Step 8</h3><p>In this step your goal is to add the <code>-L</code> flag to follow symbolic links.</p><p>By default <code>du</code> does not follow symbolic links, it reports the size of the link itself, not the target. With <code>-L</code>, symbolic links should be followed and the size of the target file or directory should be counted instead.</p><p>Create a symlink to test with:</p><pre><code><code>ln -s testdir/subdir1 testdir/link-to-subdir1</code></code></pre><p><strong>Testing:</strong> Without <code>-L</code>, the symlink should contribute very little to the total. With <code>-L</code>, the contents of <code>subdir1</code> should be counted again through the link path.</p><pre><code><code>ccdu -s testdir
ccdu -L -s testdir</code></code></pre><p>The second total should be larger because it includes the contents reachable through the symlink.</p><h3>Step 9</h3><p>In this step your goal is to avoid double-counting hard-linked files.</p><p>When two or more hard links point to the same inode, <code>du</code> should only count that file&#8217;s disk usage once. This means you need to track which inodes you have already counted and skip any file whose inode you have seen before.</p><p>Create a hard link to test with:</p><pre><code><code>ln testdir/file1.txt testdir/subdir2/hardlink-to-file1.txt</code></code></pre><p><strong>Testing:</strong></p><pre><code><code>ccdu -a testdir</code></code></pre><p>The total for <code>testdir</code> should not include <code>file1.txt</code> twice. The hard link should not appear in the output (even with <code>-a</code>) because its inode was already counted for the first link encountered, and <code>du</code> skips all subsequent links to the same inode entirely.</p><h3>Step 10</h3><p>In this step your goal is to add the <code>-t</code> / <code>--threshold</code> flag.</p><p>The <code>-t</code> flag takes a size value (with optional unit suffix like <code>K</code>, <code>M</code>, <code>G</code>) and filters the output so that only entries at or above that size are shown. This is useful for finding the big consumers of disk space in a large tree without wading through hundreds of tiny directories.</p><p><strong>Testing:</strong></p><pre><code><code>ccdu -t 1K testdir
ccdu -t 10K -h testdir</code></code></pre><p>The first should show only directories using at least 1 kilobyte. The second should show only directories using at least 10 kilobytes.</p><h3>Going Further</h3><p>Here are some ideas to take your <code>du</code> implementation further:</p><ul><li><p>Add the <code>-inodes</code> flag to count the number of inodes (files and directories) rather than disk usage</p></li><li><p>Add the <code>x</code> / <code>-one-file-system</code> flag to skip directories that are on a different filesystem or mount point</p></li><li><p>Add the <code>-dereference-args</code> flag to follow symlinks only for the command-line arguments, not for symlinks found during traversal</p></li><li><p>Support the <code>-block-size=SIZE</code> flag to let the user specify an arbitrary block size</p></li><li><p>Add coloured output when writing to a terminal, highlighting the largest directories</p></li><li><p>Support JSON or CSV output format for piping into other tools or visualisation</p></li><li><p>Add a <code>-sort</code> flag to order output by size, name, or time</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 #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></channel></rss>