<?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>Thu, 05 Nov 2026 00:45:09 GMT</lastBuildDate><atom:link href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2Ffeed" 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 #144 - Matrix Falling Text Generator]]></title><description><![CDATA[This challenge is to build your own version of the Matrix&#8217;s falling text.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-144-matrix-falling</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-144-matrix-falling</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 31 Oct 2026 09:01:17 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%218bfM%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.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 #144 - Matrix Falling Text Generator</h2><p>This challenge is to build your own falling text generator that simulates the falling green text from The Matrix. We&#8217;re calling it <code>ccmatrix</code>.</p><p>If you have seen the film, you will remember the moment the screen fills with streams of glowing characters raining down in the dark. <code>ccmatrix</code> brings that to your terminal. It is a small program, but it is a lot of fun to build, and it is a great way to learn how a terminal really works: how to draw anywhere on the screen, how colours are produced, how to read key presses without waiting for the Enter key, and how to react when the window changes size.</p><p>The best part is that you get to watch your work come alive as soon as it runs. It&#8217;ll look like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%218bfM%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8bfM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png 424w, https://substackcdn.com/image/fetch/$s_!8bfM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png 848w, https://substackcdn.com/image/fetch/$s_!8bfM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png 1272w, https://substackcdn.com/image/fetch/$s_!8bfM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png 1456w" sizes="100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%218bfM%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png" width="1039" height="592" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:592,&quot;width&quot;:1039,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:210692,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://codingchallenges.substack.com/i/222800696?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.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_!8bfM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png 424w, https://substackcdn.com/image/fetch/$s_!8bfM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png 848w, https://substackcdn.com/image/fetch/$s_!8bfM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png 1272w, https://substackcdn.com/image/fetch/$s_!8bfM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8d613076-531d-499c-acf0-eaf95e154a95_1039x592.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><h2>The Challenge - Building The Matrix Falling Text Generator</h2><p>In this challenge you&#8217;re going to build a program that will take over the terminal, fill it with columns of random characters that fall at different speeds, and give the whole thing the glow of the film. It will also accept a handful of command line options, so you can change the speed, the colours and the characters it uses.</p><p>The core of this challenge is the same in any language, so pick whatever you are most interested in having fun with / learning.</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 programming language. Most languages have libraries for drawing on the terminal and reading keys, or you can talk to the terminal directly using escape codes. Either approach is fine, though you will learn more by trying the second.</p><h3>Step 1</h3><p>In this step your goal is to switch the terminal into raw mode and draw a single falling character.</p><p>Put the terminal into raw mode, which lets you read key presses the moment they happen rather than waiting for the user to press Enter, and gives you full control over where things are drawn on the screen. Then draw a single character at the top of one column and move it down, one row at a time, with a short delay between each move.</p><p>The program should keep running until the user presses <code>q</code>. When it exits, it must restore the terminal to the state it was in before your program started, including the cursor, the colours and the contents of the screen as far as is practical.</p><p><strong>Testing:</strong> Run your program with no options:</p><pre><code><code>ccmatrix</code></code></pre><p>You should see a single character appear at the top of the screen and fall, one row at a time, until it reaches the bottom. Press <code>q</code> and check that you are returned to a normal, usable prompt, with the cursor visible and typed characters echoed back. Try pressing other keys such as <code>a</code> or <code>x</code> and confirm that nothing happens and your program keeps running.</p><h3>Step 2</h3><p>In this step your goal is to draw the trail behind the falling character.</p><p>Rather than a single character moving down the screen, draw a trail behind it: the leading character bright white, followed by a green trail a few rows long, and behind the trail the column left blank so the characters appear to fall through the dark. As the head moves down, the trail should move with it.</p><p><strong>Testing:</strong></p><pre><code><code>ccmatrix</code></code></pre><p>You should see a bright white character with a green trail falling away behind it, down a single column. Compare the length of the trail and how it fades with the real <code>cmatrix</code> running in another window. Press <code>q</code> and confirm the terminal is still restored correctly.</p><h3>Step 3</h3><p>In this step your goal is to fill the screen with many falling trails.</p><p>Extend your single column so that every column on the screen has its own falling trail. Each should start at a random time and fall at its own speed, so the screen looks interesting and reflects the screenshot above, rather than uniform.</p><p><strong>Testing:</strong></p><pre><code><code>ccmatrix</code></code></pre><p>You should see the screen fill with falling green characters, each column with a bright white head. Press <code>q</code> and check that you are returned to a normal, usable prompt, with the cursor visible and typed characters echoed back. Try pressing other keys such as <code>a</code> or <code>x</code> and confirm that nothing happens and your program keeps running.</p><h3>Step 4</h3><p>In this step your goal is to add screensaver mode with the <code>-s</code> flag.</p><p>When <code>-s</code> is passed, your program should exit as soon as the user presses any key at all, not just <code>q</code>. If your system has a mouse include that too. As before, the terminal must be fully restored on exit.</p><p><strong>Testing:</strong></p><pre><code><code>ccmatrix -s</code></code></pre><p>Let the screen fill, then press the space bar. The program should exit straight away and leave you with a working prompt. Repeat the test with a few other keys, such as a letter, a digit, the Enter key and finally the mouse. Then run without <code>-s</code> and check that those same keys are ignored, so the difference between the two modes is clear.</p><h3>Step 5</h3><p>In this step your goal is to add control over the speed of the animation.</p><p>Support the <code>-u</code> flag, which sets the delay between updates as a number from 0 to 10. The default is 4, and 0 is the fastest. Your program should also let the user change the speed while it is running by pressing the keys <code>0</code> to <code>9</code>, taking effect straight away without restarting the animation.</p><p>Think about what should happen when someone passes a value outside the range, or something that is not a number. A clear error message and a non-zero exit code is the right way to handle it.</p><p><strong>Testing:</strong></p><pre><code><code>ccmatrix -u 0
ccmatrix -u 10
ccmatrix</code></code></pre><p>The first run should be noticeably faster than the third (which uses the default of 4), and the second should be noticeably slower. While one of them is running, press <code>0</code>, then <code>9</code>, and watch the animation speed up and slow down. Finally check the error handling:</p><pre><code><code>ccmatrix -u 11
ccmatrix -u fast</code></code></pre><p>Both should report a problem and exit without drawing anything.</p><h3>Step 6</h3><p>In this step your goal is to add colour options.</p><p>Support the <code>-C</code> flag, which sets the colour of the trails. It should accept one of <code>green</code> (the default), <code>red</code>, <code>blue</code>, <code>white</code>, <code>yellow</code>, <code>cyan</code>, <code>magenta</code> or <code>black</code>. The bright leading character should stay white. Also support the <code>-r</code> flag, which displays the trails in rainbow colours instead.</p><p><strong>Testing:</strong></p><pre><code><code>ccmatrix -C red
ccmatrix -C cyan
ccmatrix -r</code></code></pre><p>The first two runs should show trails in the requested colours, with white heads. The third should show trails made up of several different colours. Also try an unknown colour:</p><pre><code><code>ccmatrix -C purple</code></code></pre><p>This should report the list of colours that are supported and exit.</p><h3>Step 7</h3><p>In this final step your goal is to add Japanese characters and handle resizing.</p><p>Support the <code>-c</code> flag, which draws half-width katakana characters instead of the default letters and numbers. This is the closest you will get to the look of the film, so it is a very satisfying one to see working.</p><p>Your program should also react when the terminal window is resized, either by making it larger or smaller. After a resize the display should redraw correctly to fill the new size, with no leftover fragments, no crashes and no columns drawn outside the window.</p><p><strong>Testing:</strong></p><pre><code><code>ccmatrix -c</code></code></pre><p>You should see katakana characters falling down the screen. If they appear as boxes or question marks, check that your terminal uses a font that supports them and that it is set to UTF-8. Combine it with other options to check they work together:</p><pre><code><code>ccmatrix -c -C cyan -u 2</code></code></pre><p>Then, while any of the above is running, drag the window larger and smaller. The animation should carry on and fill the new space each time. Do the same by changing the font size, or by using your window manager to maximise and restore the window.</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 the <code>b</code> and <code>B</code> flags, which make some or all of the characters bold</p></li><li><p>Add the <code>a</code> flag for asynchronous scrolling, where columns move at different rates</p></li><li><p>Add a message mode, where the falling characters briefly resolve into a line of text of your choosing</p></li><li><p>Support a configuration file so a user can store their favourite options</p></li><li><p>Make the characters in the trail flicker and change as they fall, rather than staying fixed</p></li><li><p>Add support for true colour terminals, with smooth fades along each trail</p></li><li><p>Measure how much CPU your program uses, and see if you can make it lighter without changing how it looks</p></li><li><p>Make it into a full blown screensaver</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #142 - Software KVM]]></title><description><![CDATA[This challenge is to build your own version of Barrier.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-142-software-kvm</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-142-software-kvm</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 24 Oct 2026 08:00:50 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0eaaf7d5-a75b-4209-aac4-a132866368c1_2400x1500.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 #142 - Software KVM</h2><p>This challenge is to build your own software KVM switch, a simplified version of <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fdebauchee%2Fbarrier">Barrier</a>, the open source tool that lets you control several computers with a single keyboard and mouse.</p><p>If you&#8217;ve ever had two or three computers side by side on your desk, you&#8217;ll know the pain of swapping keyboards and mice between them, swapping you between different keyboards and mice, or reaching for a physical KVM switch.</p><p>Barrier solves this in software: move your cursor off the edge of one screen and it reappears on the next computer, carrying your keyboard and clipboard with it. No extra cables, no hardware box, just a small program running on each machine.</p><p>In this challenge you&#8217;ll build both halves of that system from scratch, speaking Barrier&#8217;s own open wire protocol, so that a real, unmodified Barrier client or server can interoperate with yours, and two instances of your own build can run the whole show on their own.</p><h2><strong>Fancy Winning A MacBook Neo or some Merch? Then Join Kesta&#8217;s Hacktober Event.</strong></h2><p>This October I&#8217;m pleased to be part of the <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4cZ83bN">Kestra Hacktober event</a>. It&#8217;s a great way to contribute to open source this October and win some great prizes!</p><p>There are two ways to enter:</p><ol><li><p>Fix an issue in their open-source codebase.</p></li></ol><p>They&#8217;ve identified and scoped a number of issues across their core and plugin repositories. They&#8217;re tagged good-first-issue, so you can find one that interests you.</p><ol><li><p>Build a blueprint, a ready-to-use flow template that other developers can pick up and run.</p></li></ol><p>Whichever way you take part, there are prizes to be won!</p><p>&#129351; 1st place: 13&#8221; MacBook Neo</p><p>&#129352; 2nd place: Apple iPad</p><p>&#129353; 3rd place: $150 Amazon gift card</p><p>You need to <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4jk0now">register here</a> to win prizes.</p><p>And you can find the full details on <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4cZ83bN">Kestra&#8217;s Hacktober page</a>.</p><p>OK, on with the coding challenge!</p><h2>The Challenge - Building Your Own Software KVM</h2><p>You&#8217;re going to build a software KVM. It&#8217;s a client-server systems.</p><p>The server captures keyboard and mouse activity on your main machine, works out which connected screen should be in control as the cursor crosses screen edges, and forwards input, clipboard and status updates to the right client over the network.</p><p>The client connects to a server, and acts on everything it&#8217;s sent, moving the cursor, typing keystrokes, updating the clipboard, on the machine it&#8217;s running on. Because you&#8217;re implementing Barrier&#8217;s real protocol rather than inventing your own, you&#8217;ll be able to test your work against the genuine Barrier client and server at every stage, right up until the point where you don&#8217;t need them any more.</p><p>This is challenge combines binary network protocol work with low level, platform specific input handling, so pick a language with solid support for both TCP sockets and OS level mouse and keyboard capture.</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, bearing in mind you&#8217;ll need a library for network programming, one for capturing global mouse and keyboard events on your own machine (not just events inside your own windows), and one for injecting simulated mouse and keyboard input, since you&#8217;ll eventually be building the client side too. Most mainstream languages have options for all three.</p><p>Download and install the real <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fdebauchee%2Fbarrier%2Freleases">Barrier client</a> on a second machine, you&#8217;ll use this throughout the challenge as the thing your server controls. Also consider installing a packet capture tool such as Wireshark or <code>tcpdump</code>, and spend some time capturing traffic between two genuine Barrier instances so you have a reference for what correctly formed messages look like.</p><p>Finally, read through Barrier&#8217;s own protocol header in its source code (<code>src/lib/barrier/protocol_types.h</code> in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fdebauchee%2Fbarrier">debauchee/barrier</a> repository) to familiarise yourself with the message codes you&#8217;ll be implementing.</p><h3>Step 1</h3><p>In this step your goal is to accept connections from Barrier clients and complete the initial handshake.</p><p>Your server should listen for TCP connections on port 24800, the default Barrier port. When a client connects, it should send a <code>Hello</code> message carrying the protocol&#8217;s major and minor version, and wait for the client to reply with a <code>HelloBack</code> message containing the client&#8217;s own version and the name it was configured with.</p><p><strong>Testing:</strong> Point the Barrier client at your server&#8217;s address and port. Watch your server log the client&#8217;s reported name and protocol version once the handshake completes. If you installed Wireshark, capture the exchange in Wireshark and compare it against the reference capture you took in Step Zero.</p><h3>Step 2</h3><p>In this step your goal is to reject clients speaking an incompatible version of the protocol.</p><p>If the version a client reports in its <code>HelloBack</code> is one your server doesn&#8217;t support, your server should respond with an <code>EICV</code> (incompatible version) message and close the connection rather than proceeding.</p><p><strong>Testing:</strong> Write a small script that opens a raw TCP connection to your server and sends a <code>HelloBack</code> with a deliberately wrong version number. Confirm your server sends <code>EICV</code> and disconnects. Then reconnect with the real Barrier client and confirm the normal handshake still succeeds.</p><h3>Step 3</h3><p>In this step your goal is to load a screen configuration describing the layout of connected screens.</p><p>Your server should read a configuration that names each client screen and defines its directional neighbours, left, right, top and bottom, relative to the other configured screens. This is the layout Barrier itself asks you to draw when you arrange screens in its own configuration tool.</p><p><strong>Testing:</strong> Write a configuration with at least two screens, for example <code>office-mac</code> to the left of <code>office-pc</code>, and confirm your server parses it into a lookup you can query, such as &#8220;what is to the right of <code>office-mac</code>?&#8221;. Try a configuration that references a screen that doesn&#8217;t exist and confirm your server reports a clear error instead of crashing.</p><h3>Step 4</h3><p>In this step your goal is to exchange screen information with a connected client.</p><p>After the handshake, your server should receive a <code>DINF</code> message from the client reporting its screen size and current cursor position, and acknowledge it with a <code>CIAK</code> message.</p><p><strong>Testing:</strong> Connect the real Barrier client and confirm your server logs the screen resolution it reports in its <code>DINF</code> message. Compare the <code>CIAK</code> your server sends against the one sent by a genuine Barrier server in your Wireshark reference capture if you did it.</p><h3>Step 5</h3><p>In this step your goal is to track which screen currently has control and where the cursor is on it.</p><p>Your server captures mouse movement on your own machine (the one with the physical keyboard and mouse attached), so it needs to keep its own model of which screen is &#8220;active&#8221; and the cursor&#8217;s current coordinates on that screen, updating it as the mouse moves.</p><p><strong>Testing:</strong> Move your mouse around your screen and log the coordinates your server is tracking. Confirm the logged position matches where your cursor actually is, and that it stays within your screen&#8217;s bounds.</p><h3>Step 6</h3><p>In this step your goal is to detect when the cursor should hand off to another screen.</p><p>When the cursor reaches the edge of the active screen, your server should check the screen configuration from Step 3 for a neighbour in that direction. If one is configured, that&#8217;s a handoff; if there&#8217;s no neighbour configured on that edge, the cursor should simply stop at the edge as normal.</p><p><strong>Testing:</strong> With a configuration that has a neighbour to the right but none above, move your cursor to the right edge and confirm your server detects a handoff. Move it to the top edge and confirm it doesn&#8217;t.</p><h3>Step 7</h3><p>In this step your goal is to actually switch control to the neighbouring screen.</p><p>When your server detects a handoff, it should send a <code>CINN</code> command to the client gaining control, including the coordinates where the cursor enters and a sequence number, and send a <code>COUT</code> command to the client that just lost control.</p><p><strong>Testing:</strong> With the real Barrier client connected and configured as a neighbour, move your cursor to the matching screen edge and watch the cursor appear on the client machine. Move it back the other way and confirm control returns to your own screen.</p><h3>Step 8</h3><p>In this step your goal is to forward keyboard activity to the client currently in control.</p><p>Your server should forward key press and release events using <code>DKDN</code> and <code>DKUP</code> messages, carrying the key code and modifier mask, and forward repeated keystrokes from a held key using <code>DKRP</code>.</p><p><strong>Testing:</strong> With the Barrier client in control, open a text editor on the client machine and type on your physical keyboard. Confirm the correct characters appear, including held keys like a repeated letter when you hold a key down, and that modifier combinations like Shift+A produce a capital letter.</p><h3>Step 9</h3><p>In this step your goal is to forward mouse button and wheel activity.</p><p>Your server should forward mouse button press and release events using <code>DMDN</code> and <code>DMUP</code> messages carrying the button id, and forward scroll wheel movement using <code>DMWM</code>.</p><p><strong>Testing:</strong> With the Barrier client in control, click and scroll with your physical mouse. Confirm clicks register correctly on the client machine (for example, clicking a button in an open application) and that scrolling moves the content on the client&#8217;s screen.</p><h3>Step 10</h3><p>In this step your goal is to switch to relative mouse movement once control has entered a client screen.</p><p>Rather than repeatedly resending absolute coordinates, your server should send relative movement deltas using <code>DMRM</code> once the cursor has entered a client&#8217;s screen.</p><p><strong>Testing:</strong> Capture the traffic between your server and the Barrier client while moving the mouse continuously across the client&#8217;s screen. Confirm the messages are <code>DMRM</code>, not repeated <code>DMMV</code>, and that cursor movement on the client stays smooth with no jumps or drift.</p><h3>Step 11</h3><p>In this step your goal is to keep connections alive and close them cleanly.</p><p>Your server should send a <code>CALV</code> keep-alive message to each connected client every 3 seconds, and disconnect any client that fails to respond within 9 seconds. It should also support a <code>CNOP</code> no-op message for simple connectivity checks, and send a <code>CBYE</code> message when deliberately closing a connection.</p><p><strong>Testing:</strong> Leave a client connected and idle, and confirm in Wireshark that <code>CALV</code> messages go out roughly every 3 seconds. Simulate an unresponsive client (for example, by blocking its replies with a firewall rule) and confirm your server disconnects it after about 9 seconds. Stop your server normally and confirm the client receives a <code>CBYE</code> before the connection closes.</p><h3>Step 12</h3><p>In this step your goal is to synchronise the clipboard between screens.</p><p>When the clipboard changes on the screen currently in control, your server should send a <code>CCLP</code> ownership notification followed by a <code>DCLP</code> message carrying the clipboard contents to the other connected clients, and it should accept the same messages coming the other way when the clipboard changes on a client.</p><p><strong>Testing:</strong> Copy some text on your own machine while a client is connected, then paste on the client machine and confirm the same text appears. Then copy something on the client machine and confirm it comes back to your own clipboard.</p><h3>Step 13</h3><p>In this step your goal is to notify clients when your screensaver starts or stops.</p><p>Your server should send a <code>CSEC</code> message to connected clients when the primary screen&#8217;s screensaver activates, and another when it deactivates.</p><p><strong>Testing:</strong> Trigger your machine&#8217;s screensaver and confirm connected clients receive a <code>CSEC</code> message indicating the started state, followed by one indicating the stopped state when you dismiss it.</p><h3>Step 14</h3><p>In this step your goal is to support more than two screens at once.</p><p>Your screen configuration and switching logic from earlier steps should work for any number of connected screens arranged in a configurable grid, not just a single pair.</p><p><strong>Testing:</strong> Configure three screens in an L shape, for example one to the right of your primary, and one below that one, connect three client instances, and confirm the cursor moves correctly between all of them as you&#8217;d expect from the layout, including that moving diagonally across two edges in sequence puts you on the right screen each time.</p><h3>Step 15</h3><p>In this step your goal is to confirm full interoperability with a real Barrier client.</p><p>Run an extended session using only the genuine, unmodified Barrier client as your test client; type, click, scroll, switch screens back and forth, and sync the clipboard, all without the client needing any changes or special configuration to work with your server.</p><p><strong>Testing:</strong> Use your server as a daily driver for a short working session, controlling the Barrier client machine for ordinary tasks like browsing or editing a document. If everything behaves as it would with a genuine Barrier server, nice work!</p><h3>Step 16</h3><p>In this step your goal is to implement the client side of the protocol, so your own build can act as the screen being controlled, not just the one doing the controlling.</p><p>Your client should connect to a server, complete the handshake and the <code>DINF</code>/<code>CIAK</code> exchange, and then act on everything the server sends it: moving the cursor and injecting keystrokes on <code>CINN</code>/<code>COUT</code> and the <code>DKDN</code>/<code>DKUP</code>/<code>DKRP</code>/<code>DMDN</code>/<code>DMUP</code>/<code>DMWM</code>/<code>DMRM</code> messages, updating the local clipboard on <code>CCLP</code>/<code>DCLP</code>, responding to <code>CALV</code> and <code>CNOP</code>, and disconnecting cleanly on <code>CBYE</code>.</p><p><strong>Testing:</strong> First connect your client to a Barrier server and confirm it behaves just like a genuine Barrier client, moving the cursor and typing correctly on your machine as the Barrier server hands it control.</p><p>Then run two instances of your own build, one as server and one as client, on two machines (or two user sessions on one machine), with no real Barrier software involved at all, and repeat the full set of tests from the earlier steps, screen switching, typing, clicking, scrolling, and clipboard sync, end to end between your own software only.</p><h3>Going Further</h3><p>Once you have the core software KVM working, here are some ideas to take it further:</p><ul><li><p>Add TLS encryption to the connection, matching the SSL support in newer versions of the real protocol.</p></li><li><p>Support file drag and drop between screens using the <code>DFTR</code> message.</p></li><li><p>Add hotkey-triggered screen switching as an alternative to edge detection.</p></li><li><p>Handle screens with different resolutions or DPI scaling correctly when translating cursor coordinates.</p></li><li><p>Build a small configuration UI for drawing the screen layout, rather than hand editing a config file.</p></li><li><p>Add automatic reconnection when a client drops and comes back.</p></li><li><p>Package your server and client as a background service or system tray application.</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #141 - Tail]]></title><description><![CDATA[This challenge is to build your own version of tail.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-141-tail</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-141-tail</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 17 Oct 2026 08:00:56 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40bc381a-7db1-44e7-8652-aa919ef8f48e_2400x1602.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 #141 - Tail</h2><p>This challenge is to build your own version of <code>tail</code>, the classic Unix utility for looking at the end of a file and following a file as it grows.</p><p>If you&#8217;ve ever debugged a live system, you&#8217;ve almost certainly run <code>tail -f</code> on a log file to watch new entries appear in real time. It looks simple, but there&#8217;s some interesting systems programming going on: efficiently seeking to the end of huge files, detecting when a file has been truncated or rotated out from under you, and using the operating system&#8217;s own file notification mechanisms instead of just polling in a loop.</p><p>Building your own version is a great way to get comfortable with low-level file I/O and the quirks of how files actually behave on a live system.</p><h2><strong>Fancy Winning A MacBook Neo or some Merch?Then Join Kesta&#8217;s Hacktober Event.</strong></h2><p>This October I&#8217;m pleased to be part of the <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4cZ83bN">Kestra Hacktober event</a>. It&#8217;s a great way to contribute to open source this October and win some great prizes!</p><p>There are two ways to enter:</p><ol><li><p>Fix an issue in their open-source codebase.</p></li></ol><p>They&#8217;ve identified and scoped a number of issues across their core and plugin repositories. They&#8217;re tagged good-first-issue, so you can find one that interests you.</p><ol><li><p>Build a blueprint, a ready-to-use flow template that other developers can pick up and run.</p></li></ol><p>Whichever way you take part, there are prizes to be won!</p><p>&#129351; 1st place: 13&#8221; MacBook Neo</p><p>&#129352; 2nd place: Apple iPad</p><p>&#129353; 3rd place: $150 Amazon gift card</p><p>You need to <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4jk0now">register here</a> to win prizes.</p><p>And you can find the full details on <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4cZ83bN">Kestra&#8217;s Hacktober page</a>.</p><p>OK, on with the coding challenge!</p><h2>The Challenge - Building Your Own Tail</h2><p>In this challenge you&#8217;re going to build your own version of <code>tail</code>. You&#8217;ll start with the basics, printing the last few lines or bytes of a file, then work your way up to a proper <code>-f</code> follow mode that can track a file being written to by another process, survive that file being truncated or rotated, and watch several files at once.</p><p>The core logic is the same in any language, so pick whatever you&#8217;re most comfortable with, though a language that gives you easy access to low-level file I/O and system calls will make the later steps noticeably smoother.</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. Because this challenge leans on operating system specific features later on, kqueue on macOS and BSD, inotify on Linux, it&#8217;s worth checking early on what your language offers for file system event notifications, or whether you&#8217;ll need to reach for the underlying system calls directly.</p><p>Spend a little time with the system <code>tail</code> before you start. Run it on a few files, try <code>-f</code> on a log file while appending to it from another terminal, and read through <code>man tail</code>.</p><p>Create a small test file to use throughout the challenge:</p><pre><code><code>seq 1 15 &gt; test.log</code></code></pre><p>This gives you a file with 15 lines, numbered 1 to 15, which makes it easy to check your output against what you&#8217;d expect.</p><h3>Step 1</h3><p>In this step your goal is to print the last 10 lines of a file, matching the default behaviour of <code>tail</code>.</p><p>Your tool should accept a single filename as an argument, read the file, and print only its final 10 lines. If the file has fewer than 10 lines, print all of them.</p><p><strong>Testing:</strong></p><pre><code><code>cctail test.log</code></code></pre><p>You should see lines 6 to 15. Try it against a file with fewer than 10 lines and confirm the whole file is printed.</p><h3>Step 2</h3><p>In this step your goal is to add the <code>-n</code> flag so the user can choose how many lines to display.</p><p><code>-n</code> takes a number and your tool should print that many lines from the end of the file instead of the default 10.</p><p><strong>Testing:</strong></p><pre><code><code>cctail -n 5 test.log</code></code></pre><p>You should see lines 11 to 15. Try <code>cctail -n 20 test.log</code>, since there are only 15 lines in the file, the whole file should be printed.</p><h3>Step 3</h3><p>In this step your goal is to support the <code>-n +NUM</code> syntax.</p><p>Instead of counting backwards from the end of the file, <code>-n +NUM</code> counts forwards from the start, starting output at line <code>NUM</code>. So <code>-n +5</code> means &#8220;start at line 5 and print to the end of the file&#8221;, rather than &#8220;print the last 5 lines&#8221;.</p><p><strong>Testing:</strong></p><pre><code><code>cctail -n +5 test.log</code></code></pre><p>You should see lines 5 to 15. Compare it with <code>cctail -n 5 test.log</code> from the previous step to make sure you haven&#8217;t mixed the two up.</p><h3>Step 4</h3><p>In this step your goal is to add the <code>-c</code> flag to output the last NUM bytes of a file instead of lines.</p><p><code>-c</code> should work independently of <code>n</code>: when it&#8217;s given, your tool should seek to NUM bytes before the end of the file and print exactly that many bytes, with no regard for line boundaries.</p><p><strong>Testing:</strong></p><pre><code><code>printf 'abcdefghij' &gt; bytes.log
cctail -c 4 bytes.log</code></code></pre><p>You should see <code>ghij</code> printed with no trailing newline. Compare your output against the system <code>tail -c 4 bytes.log</code> to confirm it matches exactly.</p><h3>Step 5</h3><p>In this step your goal is to make reading the last N lines or bytes efficient on large files.</p><p>If your current implementation reads the whole file into memory before working out what to print, it&#8217;ll fall over on large files. Rework it to seek from the end of the file and read backwards in chunks, stopping as soon as you&#8217;ve found enough lines (or bytes), rather than reading the entire file from the start.</p><p><strong>Testing:</strong> Generate a large test file and confirm your tool still returns quickly and correctly:</p><pre><code><code>seq 1 5000000 &gt; bigfile.log
time cctail -n 10 bigfile.log</code></code></pre><p>The command should return almost instantly and show lines 4999991 to 5000000, rather than taking noticeably longer the way a &#8220;read the whole file&#8221; approach would.</p><h3>Step 6</h3><p>In this step your goal is to support reading from standard input when no file is specified.</p><p>When your tool is run without a filename, it should read from stdin instead and apply the same last-N-lines or last-N-bytes logic to whatever it receives. Bear in mind that you can&#8217;t seek on a stream the way you can on a regular file, so this will need a different approach from the one you used in Step 5. Hint: think about ways of buffering data.</p><p><strong>Testing:</strong></p><pre><code><code>cat test.log | cctail -n 5
seq 1 100 | cctail -n 3</code></code></pre><p>Both should behave the same as if you&#8217;d passed the equivalent file as an argument.</p><h3>Step 7</h3><p>In this step your goal is to support multiple files as arguments, each displayed under its own header.</p><p>When more than one file is given, your tool should print a header line in the form <code>==&gt; filename &lt;==</code> before each file&#8217;s output, in the order the files were given on the command line.</p><p><strong>Testing:</strong></p><pre><code><code>seq 100 115 &gt; test2.log
cctail test.log test2.log</code></code></pre><p>You should see a <code>==&gt; test.log &lt;==</code> header followed by its last 10 lines, then a <code>==&gt; test2.log &lt;==</code> header followed by its last 10 lines.</p><h3>Step 8</h3><p>In this step your goal is to implement the <code>-f</code> (follow) flag.</p><p>After printing the initial lines, your tool should keep running and watch the file for new data being appended, printing each new line as it arrives, exactly as if another process were writing to that file. A straightforward way to start is to poll the file periodically and check whether it has grown.</p><p><strong>Testing:</strong> In one terminal:</p><pre><code><code>cctail -f test.log</code></code></pre><p>In a second terminal:</p><pre><code><code>echo "a new line" &gt;&gt; test.log</code></code></pre><p>You should see <code>a new line</code> appear in the first terminal shortly after you run the second command. Stop your tool with <code>Ctrl+C</code> for now, we&#8217;ll make that exit cleanly in a later step.</p><h3>Step 9</h3><p>In this step your goal is to replace simple polling with OS-level file watching where it&#8217;s available.</p><p>Rather than waking up on a timer to check if the file has changed, use the operating system&#8217;s native file notification mechanism, inotify on Linux, kqueue on macOS and BSD, to be told immediately when the file is written to. Fall back to the polling approach from Step 8 on platforms or file systems where this isn&#8217;t available.</p><p><strong>Testing:</strong></p><pre><code><code>cctail -f test.log</code></code></pre><p>In another terminal, append a line and confirm it appears essentially instantly, noticeably faster than a one second polling interval would allow. If you can, add some temporary logging to confirm which code path (watch-based or polling) is actually being used.</p><h3>Step 10</h3><p>In this step your goal is to detect file truncation and recover from it.</p><p>If the file you&#8217;re following suddenly becomes shorter than it was, it has been truncated, likely by something like <code>log rotation with copy-and-truncate</code>, or a tool clearing a log file. When this happens, your tool should notice and restart reading from the beginning of the file, rather than trying to carry on from an offset that no longer makes sense (or erroring out).</p><p><strong>Testing:</strong></p><pre><code><code>cctail -f test.log</code></code></pre><p>In another terminal, truncate and refill the file:</p><pre><code><code>&gt; test.log
echo "fresh start" &gt;&gt; test.log</code></code></pre><p>Your tool should detect the truncation and print <code>fresh start</code> rather than hanging or throwing an error.</p><h3>Step 11</h3><p>In this step your goal is to add the <code>-F</code> flag, which follows a file by name rather than by file handle, so it survives the file being rotated or replaced.</p><p>With plain <code>-f</code>, if a file is renamed and a new file is created in its place (the way <code>logrotate</code> typically works), your tool keeps following the old, now-renamed file and never sees the new one. With <code>-F</code>, it should detect that the original filename has been replaced by a new file and switch over to following that new file instead.</p><p><strong>Testing:</strong> Run with the new flag:</p><pre><code><code>cctail -F test.log</code></code></pre><p>In another terminal, simulate rotation:</p><pre><code><code>mv test.log test.log.old
echo "post-rotation line" &gt;&gt; test.log</code></code></pre><p>Your tool should pick up <code>post-rotation line</code> from the newly created <code>test.log</code>. Repeat the same steps using <code>-f</code> instead of <code>-F</code> and confirm it does <em>not</em> pick up the new file, demonstrating the difference between the two flags.</p><h3>Step 12</h3><p>In this step your goal is to extend follow mode to support watching multiple files at once.</p><p>Your tool should be able to follow several files simultaneously, printing the <code>==&gt; filename &lt;==</code> header (from Step 7) whenever output switches from one file to another, so it&#8217;s always clear which file a given line came from.</p><p><strong>Testing:</strong></p><pre><code><code>cctail -f test.log test2.log</code></code></pre><p>In another terminal, append to each file in turn:</p><pre><code><code>echo "line for test.log" &gt;&gt; test.log
echo "line for test2.log" &gt;&gt; test2.log
echo "another line for test.log" &gt;&gt; test.log</code></code></pre><p>Each append should appear under the correct file&#8217;s header, with a fresh header printed whenever the active file changes.</p><h3>Step 13</h3><p>In this step your goal is to add the <code>-q</code> flag to suppress filename headers.</p><p>When <code>-q</code> is passed, your tool should never print the <code>==&gt; filename &lt;==</code> headers, even when following multiple files. This is handy for scripting, where you just want the raw output without knowing (or caring) which file each line came from.</p><p><strong>Testing:</strong></p><pre><code><code>cctail -q -f test.log test2.log</code></code></pre><p>Append to both files as in Step 12&#8217;s test and confirm no headers appear at all, just the interleaved output.</p><h3>Step 14</h3><p>In this step your goal is to make sure your tool exits cleanly and leaves the terminal in a sane state when interrupted.</p><p>Pressing <code>Ctrl+C</code> while following a file should cause your tool to shut down promptly: close any open file handles or watchers, and make sure the terminal behaves normally afterwards, with no leftover state from anything you changed while running.</p><p><strong>Testing:</strong></p><pre><code><code>cctail -f test.log</code></code></pre><p>Press <code>Ctrl+C</code>. Your tool should exit immediately with no hanging or stray output, and the shell prompt that follows should behave exactly as it did before you ran the command.</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 colour to the filename headers or to lines matching particular patterns, to make it easier to scan output visually.</p></li><li><p>Add a <code>-grep</code> style flag that only shows lines matching a pattern, useful for following a busy log but only caring about certain messages.</p></li><li><p>Support following gzip-compressed log files, decompressing on the fly.</p></li><li><p>Add support for following remote files over SSH, similar to tools like <code>mtail</code> or <code>multitail</code>.</p></li><li><p>Add a <code>-pid</code> flag that stops following once a given process has exited, useful for following a log for exactly as long as the program writing it is running.</p></li><li><p>Build a split-pane terminal UI that follows several files side by side at once, rather than interleaving output into a single stream.</p></li><li><p>Benchmark your implementation against the system <code>tail -f</code> under heavy write load and see how the latency compares.</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #140 - Bluetooth Proximity Finder]]></title><description><![CDATA[This challenge is to build your own Bluetooth Proximity Monitoring tool.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-140-bluetooth-proximity</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-140-bluetooth-proximity</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 10 Oct 2026 08:02:06 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc93ebc90-4686-479b-b027-ef27a9268d31_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 #140 - Bluetooth Proximity Finder</h2><p>This challenge is to build your own Bluetooth proximity locator, a command line tool that helps you find a nearby device by watching its signal strength climb as you get closer.</p><p>The idea comes from a true story a developer posted on X. They lost their phone at the office and spent half an hour turning the place over, only to remember that Find My had been switched off by the company&#8217;s device management software. Out of ideas, they asked Claude for help. Claude suggested tracking the phone&#8217;s Bluetooth signal strength instead, and wrote a simple signal meter for them. Then they walked around the office watching the number climb, and found the phone.</p><p>That is exactly what you are going to build in this challenge. Along the way you will learn how to scan for Bluetooth Low Energy devices. It is a great excuse to step outside the terminal and interact with real hardware.</p><p>I accidentally deleted the URL for the X post, if you know it please add it to the comments on SubStack. Thanks.</p><h2><strong>Fancy Winning A MacBook Neo or some Merch?Then Join Kesta&#8217;s Hacktober Event.</strong></h2><p>This October I&#8217;m pleased to be part of the <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4cZ83bN">Kestra Hacktober event</a>. It&#8217;s a great way to contribute to open source this October and win some great prizes!</p><p>There are two ways to enter:</p><ol><li><p>Fix an issue in their open-source codebase.</p></li></ol><p>They&#8217;ve identified and scoped a number of issues across their core and plugin repositories. They&#8217;re tagged good-first-issue, so you can find one that interests you.</p><ol><li><p>Build a blueprint, a ready-to-use flow template that other developers can pick up and run.</p></li></ol><p>Whichever way you take part, there are prizes to be won!</p><p>&#129351; 1st place: 13&#8221; MacBook Neo</p><p>&#129352; 2nd place: Apple iPad</p><p>&#129353; 3rd place: $150 Amazon gift card</p><p>You can find out more and sign up on <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4cZ83bN">Kestra&#8217;s Hacktober page</a>.</p><p>OK, on with the coding challenge!</p><h2>The Challenge - Building Your Own Bluetooth Proximity Finder</h2><p>You&#8217;re going to build a command line tool that scans for nearby Bluetooth Low Energy (BLE) devices, ranks them by signal strength, and helps you track a specific device down by turning its raw signal into &#8220;getting warmer, getting colder&#8221; style feedback.</p><p>The core ideas, scanning, smoothing noisy data, and turning a number into useful feedback, are the same in any language, so pick a language and BLE library you&#8217;re comfortable with. If you&#8217;ve mostly worked on the web or in application code, this is also a nice opportunity to get a taste of hardware-adjacent programming.</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 language with a BLE scanning library available for your operating system, for example Python&#8217;s <code>bleak</code>, Node&#8217;s <code>@abandonware/noble</code>, Go&#8217;s <code>tinygo-org/bluetooth</code>, or Swift&#8217;s <code>CoreBluetooth</code> on macOS. You&#8217;ll need a machine with a Bluetooth adapter, and at least one other BLE-advertising device nearby to scan for, your phone, a pair of wireless headphones, or a fitness tracker will all work.</p><p>Before writing any code, use your operating system&#8217;s built in Bluetooth scanning to confirm your adapter can see nearby devices and to get a feel for what a real scan result looks like, including the Received Signal Strength Indicator (RSSI) value reported in dBm. As a rough guide, a device held at arm&#8217;s length is often somewhere around -45 dBm, while a device in the same room but further away might read closer to -72 dBm. These numbers vary a lot by device and environment, so treat them as a rough guide rather than a rule.</p><h3>Step 1</h3><p>In this step your goal is to build a survey mode that continuously lists nearby BLE devices ranked by signal strength.</p><p>Your tool should scan for nearby BLE devices and print each one&#8217;s advertised name (or its address, if no name is broadcast), its address or identifier, and its RSSI in dBm. Rather than scanning once and exiting, refresh the displayed list on a fixed interval, a default of once a second is a sensible starting point.</p><p>Sort the list so the strongest signal, the closest device, always appears at the top. Remove a device from the list once it hasn&#8217;t been seen for a while, say 10 seconds, so devices that move out of range or power off disappear rather than lingering forever.</p><p><strong>Testing:</strong> Run your tool in survey mode with a couple of BLE devices nearby (your phone, headphones, and so on). You should see a live-updating list with the strongest signal at the top. Walk a device further away and confirm it drops down the list, then out of range entirely and confirm it disappears after your timeout has passed.</p><h3>Step 2</h3><p>In this step your goal is to add a calibration mode that converts raw RSSI into human-readable proximity bands.</p><p>A raw dBm reading isn&#8217;t meaningful to most people at a glance. Add a calibration mode that prompts the user to hold a target device at arm&#8217;s length, and records the RSSI it sees as the &#8220;near&#8221; reference point. Use that reference, plus a documented default &#8220;far&#8221; reference (-72 dBm is a reasonable default), to map incoming RSSI readings to at least three bands, for example &#8220;very close&#8221;, &#8220;same room&#8221;, and &#8220;far&#8221;. Let the user override both reference points with command line flags, since the right values will vary by device and environment.</p><p><strong>Testing:</strong> Run calibration while holding your test device at arm&#8217;s length, then run survey mode and confirm the device shows as &#8220;very close&#8221;. Walk it across the room and confirm the band changes to &#8220;same room&#8221; or &#8220;far&#8221; as appropriate. Try overriding the reference points with your flags and confirm the bands shift accordingly.</p><h3>Step 3</h3><p>In this step your goal is to add a hunt mode that tracks a single named device.</p><p>Add a hunt mode, invoked with a device name or address, for example <code>--hunt "John's iPhone"</code>, that filters the live display down to just that one target instead of showing the full survey.</p><p>It should show the target&#8217;s current proximity band and raw RSSI, updating on every new reading, and it should clearly indicate when the target hasn&#8217;t been seen within your timeout window rather than silently freezing on stale data. If the named device is never seen at all within a configurable startup timeout (30 seconds is a sensible default), exit with a non-zero status and a clear error message rather than waiting forever.</p><p><strong>Testing:</strong> Run hunt mode against a device you know is nearby and confirm it tracks that device specifically, ignoring any others in range. Turn the target device&#8217;s Bluetooth off (or walk it out of range) and confirm your tool reports that it has lost the target rather than showing frozen data. Run hunt mode against a name that doesn&#8217;t exist and confirm it exits with an error after your startup timeout.</p><h3>Step 4</h3><p>In this step your goal is to smooth out noisy readings so the displayed signal doesn&#8217;t jump around.</p><p>Raw RSSI readings are noisy, and some Bluetooth stacks will hand you back a cached value rather than a fresh measurement. Maintain a rolling window of recent readings per device, the last five is a reasonable starting point, and compute a smoothed RSSI using a simple moving average or exponential moving average rather than displaying the raw last-seen value directly.</p><p>Treat a new reading as a duplicate, and discard it, when its value exactly matches the previous reading for that device and arrives within a short debounce window (200ms is a reasonable default), since this usually means you&#8217;ve been handed a cached value rather than a genuinely new measurement.</p><p>Add a verbose mode that shows how many raw readings came in versus how many were counted as genuine changes, so you can actually see the debounce doing its job.</p><p><strong>Testing:</strong> Run hunt mode in verbose mode and watch the raw versus counted reading totals. Confirm the displayed RSSI is noticeably smoother than the raw values you saw in Step 1. Leave a device stationary for a while and confirm the &#8220;counted&#8221; total grows much more slowly than the &#8220;raw&#8221; total, since a stationary device should be producing mostly repeated cached readings.</p><h3>Step 5</h3><p>In this step your goal is to add optional audio feedback that speeds up as you get closer to the target.</p><p>Add a <code>--sound</code> flag that plays a short click or beep each time a new reading is processed for the hunted device. Scale the interval between clicks to the smoothed RSSI, so clicks arrive slowly when the target is far away and rapidly, parking-sensor style, as you close in. Make sure the clicking stops automatically when hunt mode loses the target, rather than continuing to click away at the last known rate once the device is gone.</p><p>Bonus points if you use the motion detection sound from Aliens! &#128526;</p><p><strong>Testing:</strong> Run hunt mode with <code>--sound</code> enabled and walk towards your target device. Confirm the clicks speed up as you get closer and slow down as you move away. Move the target out of range and confirm the sound stops rather than continuing at its last rate.</p><h3>Step 6</h3><p>In this step your goal is to estimate a rough distance in metres, and to be upfront about the limits of that estimate.</p><p>Using the log-distance path loss model, estimate an approximate distance in metres from the smoothed RSSI, taking your calibrated &#8220;near&#8221; reference and a configurable path-loss exponent as inputs. Display this estimated distance alongside a clear disclaimer that BLE RSSI-based distance is noisy and approximate, not a precise measurement, since walls, your own body, and interference all affect the reading. Also document, in your tool&#8217;s help output, that a single radio only gives you distance, not bearing, so your tool can tell you &#8220;warmer&#8221; but never &#8220;that way&#8221;.</p><p><strong>Testing:</strong> Run hunt mode and compare the estimated distance against the actual physical distance to your target device at a few different points (right next to it, across the room, in another room). Confirm the numbers are in a sensible ballpark rather than being wildly off, and confirm the disclaimer and help text are visible.</p><h3>Step 7</h3><p>In this step your goal is to handle a missing or unauthorised Bluetooth adapter gracefully.</p><p>Detect when the host&#8217;s Bluetooth adapter is powered off, or when your process hasn&#8217;t been granted permission to use it, and print a clear, actionable error message rather than a stack trace or a silent hang. Also handle the case where there&#8217;s no BLE-capable adapter on the host at all, exiting cleanly with a descriptive error rather than crashing.</p><p><strong>Testing:</strong> Turn off Bluetooth on your machine (or revoke your tool&#8217;s Bluetooth permission, if your OS supports that) and run your tool. Confirm you get a clear, readable error message rather than a crash or a hang. Turn Bluetooth back on and confirm your tool works normally again.</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 classic Bluetooth (BR/EDR) scanning as a second signal source alongside your BLE scan.</p></li><li><p>Build a simple signal-strength bar chart or &#8220;big text&#8221; display for the terminal, so the proximity band is readable from across the room.</p></li><li><p>Log a session&#8217;s readings to a file and plot the signal strength over time afterwards.</p></li><li><p>Experiment with combining readings from two scanning stations (two laptops, say) to see how close you can get to estimating a rough direction, even though a single radio alone cannot.</p></li><li><p>Package your calibration reference points per device, so you don&#8217;t have to recalibrate every time you hunt for the same phone or headphones.</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #139 - Code Context MCP Server]]></title><description><![CDATA[This challenge is to build your own Code Context MCP server with memory.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-139-code-context</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-139-code-context</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 03 Oct 2026 08:01:19 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe024a243-e9ca-42f9-92c9-4f7f81f22ec8_9504x6336.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><strong>Coding Challenge #X139 - Code Context MCP Server</strong></h2><p>This challenge is to build your own code context server, a searchable, self-improving brain for your codebases that any AI coding tool can plug into.</p><p>If you use an AI coding assistant, you have probably noticed that it starts every session knowing nothing. It greps around, opens a few files, makes some guesses at how the project fits together, and then forgets all of it the moment you close or clear the session. Next task, it does the same work again to get the same context.</p><p>In this challenge you are going to build the missing layer. You will ingest a codebase, or a whole collection of them, into a database, chop it up at function and class boundaries, and turn it into something you can ask questions of in plain English. Then you will serve it over the Model Context Protocol, so Claude Code, Codex, Cursor, or any other MCP client is talking to the same brain.</p><p>Finally you will make it improve over time: it will write its own notes, remember what it has already worked out about your code, and learn from which answers actually got used.</p><p>This challenge was created in collaboration with <strong><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4hIs1ZP">Oracle</a></strong>, inspired by their blog post <strong><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4hIs1ZP">Build a Self-Improving Second Brain on Oracle AI Database</a></strong>.</p><h2><strong>The Challenge - Building Your Own Code Context MCP Server</strong></h2><p>You are going to build a tool that indexes codebases and serves them to AI clients over MCP.</p><p>Along the way you will get to play with semantic search, hybrid ranking, graph queries, and a few ideas from the world of AI agents that are genuinely useful and not just hype.</p><p>This Coding Challenge leverages Oracle AI Database 26ai because it supports generating embeddings inside the database, hybrid search that fuses keyword and semantic results, and graph queries over your code, all in the same database engine. That means your source code never leaves the database to be indexed, and you do not need an embedding API key. If you would rather use a different stack, it still works, but you will be assembling three or four separate pieces of infrastructure instead of one.</p><h3><strong>Step Zero</strong></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 programming language. You will need a decent database driver, a code parser, and an MCP server library, so pick something well served on all three counts. Python, TypeScript, Go, Java, and Rust are all great choices.</p><p>You will need a few things in place before you start:</p><ul><li><p>A container runtime, so you can run the database locally. I use Docker.</p></li><li><p>A code parser.<a href="iframe.php?url=https%3A%2F%2Ftree-sitter.github.io%2Ftree-sitter%2F"> Tree-sitter</a> has bindings for most languages and grammars for almost every language you would want to index, but anything that can tell you where functions and classes start and end will do.</p></li><li><p>An MCP client to test against. The<a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fmodelcontextprotocol%2Finspector"> MCP Inspector</a> is the quickest way to poke at a server by hand, and Claude Code, Codex or Cursor will let you try it for real.</p></li></ul><p>Spend a little time reading about the two ideas that are core to this challenge. The<a href="iframe.php?url=https%3A%2F%2Fmodelcontextprotocol.io%2F"> Model Context Protocol specification</a> explains how tools are described and called. The<a href="iframe.php?url=https%3A%2F%2Fdocs.oracle.com%2Fen%2Fdatabase%2Foracle%2Foracle-database%2F26%2Fvecse%2F"> Oracle AI Vector Search guide</a> covers the <code>VECTOR</code> type, embeddings, and vector indexes.</p><p>Finally, grab some code to index. I would suggest cloning two real projects in different languages, so you exercise multi-repository and multi-language support from the start:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;683a94e1-f976-430c-9293-b1a6e546154f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">mkdir -p ~/ccctx-repos
git clone &lt;https://github.com/pallets/flask.git&gt; ~/ccctx-repos/flask
git clone &lt;https://github.com/gin-gonic/gin.git&gt; ~/ccctx-repos/gin</code></pre></div><p>Throughout this challenge I will call the tool <code>ccctx</code>, but name yours whatever you like.</p><p></p><h3><strong>Step 1</strong></h3><p>In this step your goal is to get the database running and create somewhere to put the code.</p><p>Start Oracle AI Database 26ai Free as a local container from <code>container-registry.oracle.com/database/free:latest</code>. It should listen on port 1521 and store its data on a mounted volume, so that your index survives a restart.</p><p>Then create your schema. You need somewhere to record repositories, files, and symbols. A symbol is a single indexed chunk of code, and for each one you will want to know which repository and file it came from, its path, its language, its name, what kind of thing it is (a function, a method, a class), and the line range it occupies, along with the source code itself.</p><p>Finally, give your tool a way to register a codebase. The user should be able to point it at a local directory, give it a short name, and later list everything that has been registered along with how many files and symbols each one holds.</p><p><strong>Testing:</strong> Check the container is up and the database is ready:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e4fd1ac2-c6a5-4cd8-bac2-9bd903aacdcc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker ps
docker logs oracle26ai | tail -5</code></pre></div><p>You are looking for <code>DATABASE IS READY TO USE!</code> in the logs. Connect with SQLcl, SQL*Plus, or your language&#8217;s driver and confirm your tables exist.</p><p>Then register both repositories and list them:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;6d15ad5f-9eaa-4236-a194-c5145e3567e0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx repo add ~/ccctx-repos/flask --name flask
ccctx repo add ~/ccctx-repos/gin --name gin
ccctx repo list</code></pre></div><p>You should see something like:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;3bad77c9-3d89-4409-890f-b9f8f835268e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">NAME   PATH                       FILES  SYMBOLS
flask  /home/you/ccctx-repos/flask    0        0
gin    /home/you/ccctx-repos/gin      0        0</code></pre></div><p>Both counts are zero, because we have not indexed anything yet. That is next.</p><h3><strong>Step 2</strong></h3><p>In this step your goal is to ingest a codebase and parse it into meaningful chunks.</p><p>Walk the working tree of a registered repository and decide what is worth indexing. Respect the repository&#8217;s <code>.gitignore</code>, and skip anything that will only add noise: binary files, and vendored dependency directories.</p><p>Rather than chopping files into arbitrary blocks of text, cut them at function, method, and class boundaries, so that every chunk is a complete, self-contained piece of code. Support at least three languages. Some files will not parse, either because you have no grammar for that language or because the code is broken, so fall back to 60 line windows with 10 lines of overlap rather than skipping them. The overlap is important, because it stops a chunk boundary from slicing a relevant section of code in half.</p><p>For every symbol you store, record the commit SHA the repository was sitting at when you indexed it. You are not using it yet, but in Step 5 it becomes the thing that makes re-ingestion cheap, and it means every chunk is traceable back to an exact revision of the code.</p><p>Round the step off by reporting what happened: how many files you scanned, how many you skipped and why, how many symbols you indexed, and how long it all took.</p><p><strong>Testing:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;9e452607-654c-4fce-9970-4f06d1354e44&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx ingest flask</code></pre></div><p>You should output a summary along these lines:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;d8e6e33f-6666-4d96-938e-7b7836119a0e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Scanned   214 files
Skipped    38 files (12 ignored, 24 binary, 2 too large)
Indexed  1,847 symbols in 9.2s</code></pre></div><p>Check the chunking is doing something sensible rather than just producing 60 line windows everywhere:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;39a20bc7-3de1-4543-af50-aa9d4e6c047d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx symbols --repo flask --limit 5</code></pre></div><p>Each row should name a real function or class with a believable line range. Pick one and compare it against the file on disk to confirm the boundaries line up.</p><p>Now ingest the Go repository too, and confirm your language detection is working:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;f969cb90-a072-4092-861b-e33f3f7c3359&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx ingest gin
ccctx stats --by-language</code></pre></div><p>You should see Python symbols from <code>flask</code> and Go symbols from <code>gin</code>, with a handful of fallback chunks for files neither grammar could parse.</p><h3><strong>Step 3</strong></h3><p>In this step your goal is to make the code searchable by meaning rather than by exact text.</p><p>Load an embedding model into the database itself. Oracle publishes a prebuilt ONNX version of <code>all_MiniLM_L12_v2</code>, which you can load with <code>DBMS_VECTOR.LOAD_ONNX_MODEL</code> and give a name like <code>ALL_MINILM_L12_V2</code>. Once it is loaded, the database can turn text into vectors on its own. No API key, no network call, and your source code never leaves the database to be indexed. For a lot of teams that last point is the difference between being allowed to build this and not.</p><p>Store the embeddings in a <code>VECTOR(384, FLOAT32)</code> column, and have the database populate it with <code>VECTOR_EMBEDDING</code> as rows are inserted, rather than computing vectors in your application and sending them over. Then build an HNSW index over that column so searches stay fast as the index grows. Use <code>ORGANIZATION INMEMORY NEIGHBOR GRAPH DISTANCE COSINE WITH TARGET ACCURACY 95</code>.</p><p>With that in place, add a search command. Given a question in plain English, it should return the 10 closest symbols by cosine distance. Throw away anything below a cosine similarity of 0.3, because a bad match is worse than no match at all when the results are going to be fed to a language model.</p><p><strong>Testing:</strong> First confirm the model is loaded and working:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;fbb0352a-e8b6-44ef-95ca-9fc99a0ba1de&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">SELECT model_name FROM user_mining_models;

SELECT VECTOR_EMBEDDING(ALL_MINILM_L12_V2 USING &#8216;hello world&#8217; as data) AS v;</code></pre></div><p>The first should list <code>ALL_MINILM_L12_V2</code>, the second should return a 384 element vector.</p><p>Re-ingest so your symbols get embeddings, then try a question that shares no words with the code you expect back:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;b57bf49f-eaf7-41f0-a032-aba87a2170ca&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx search &#8220;how are incoming requests routed to handlers&#8221;</code></pre></div><p>You should get routing and dispatch code from both repositories, ranked by similarity, even though none of it contains the phrase you typed. That is the whole point: grep could not have found this.</p><p>Check your similarity floor is doing its job by searching for something completely unrelated to the indexed code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;902d2990-4d7d-4098-9efb-d9f885035df9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx search &#8220;recipe for sourdough bread&#8221;</code></pre></div><p>You should get few results or none at all, rather than ten weak matches presented as if they were useful.</p><h3><strong>Step 4</strong></h3><p>In this step your goal is to combine semantic search with keyword search, so exact names win when exact names matter.</p><p>Semantic search is excellent at &#8220;where do we handle retries&#8221; and surprisingly bad at &#8220;show me <code>url_for</code>&#8221;. When you know the exact identifier you are after, you want plain old keyword matching. When you are describing a concept, you want vectors. Real code search needs both, which is why hybrid search is so useful.</p><p>Create a hybrid vector index over your symbol text, so lexical and semantic matching are served from one index rather than two. Query it with <code>DBMS_HYBRID_VECTOR.SEARCH</code>, using <code>RRF</code> as the <code>search_scorer</code> and <code>UNION</code> as the <code>search_fusion</code> mode. Reciprocal rank fusion is a neat trick: rather than trying to make two incompatible scores comparable, it throws the scores away and combines the rankings. A result that both methods rank highly rises to the top, and neither method can dominate the other.</p><p>Make sure that when someone searches for an identifier that exists in the code, the definition of that identifier comes back above its call sites. There are usually hundreds of call sites and exactly one definition, and the definition is almost always what the reader wanted.</p><p>Finally, now that you have more than one repository indexed, let the user choose. Search everything by default, and provide a flag to narrow a search to one or more named repositories.</p><p><strong>Testing:</strong> Search for an exact identifier and check what comes first:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;15d46c7c-e7d4-4173-b410-9867ff164e71&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx search &#8220;url_for&#8221;</code></pre></div><p>The definition of <code>url_for</code> should be the top result, not one of the many places it gets called.</p><p>Compare the three modes on the same query to see the fusion working:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;245e772c-6745-4d6b-bc6c-138810f75a10&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx search &#8220;url_for&#8221; --mode vector
ccctx search &#8220;url_for&#8221; --mode text
ccctx search &#8220;url_for&#8221; --mode hybrid</code></pre></div><p>Vector mode will return things that are conceptually about URL building. Text mode will return everything containing the string. Hybrid should look better than either.</p><p>Then check repository scoping:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;bc01f999-60f4-4b2f-9343-b67756e49d6b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx search &#8220;middleware chain&#8221; --repo gin</code></pre></div><p>Every result should come from <code>gin</code>, with nothing from <code>flask</code>.</p><h3><strong>Step 5</strong></h3><p>In this step your goal is to make re-indexing cheap, so you can keep the brain current.</p><p>Right now every ingest starts from scratch on a large repository that is slow enough that you will stop doing it. And a stale index is worse than no index because you cannot tell when to trust it. The fix is to only do the work that the changes require.</p><p>Record the commit SHA of each repository the last time you indexed it. If someone asks you to re-ingest while the working tree is still at that revision, say so and do nothing, unless they insist.</p><p>When there are changes, work out what they are by comparing the working tree against the last indexed commit, and only re-parse and re-embed the files that were added, changed, or renamed. Everything else is already correct and does not need touching.</p><p>Deletions need care. When a file is removed or renamed, its old symbols and embeddings have to go too. Orphaned chunks are poison in a search index, because they point at code that is not there anymore and there is nothing in the results to tell the reader that.</p><p>Lastly, give the user an escape hatch: a full rebuild that throws away a repository&#8217;s index and starts over. You will want it the first time you change your chunking rules.</p><p><strong>Testing:</strong> Re-ingest with nothing changed:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;3607f52d-a384-4a02-85a6-68496d85caa5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx ingest flask</code></pre></div><p>It should tell you the repository is already up-to-date and do no work.</p><p>Now make a change and try again:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;fa2d1b6a-ea46-4025-a988-8cf562343df3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">cd ~/ccctx-repos/flask
git checkout HEAD~20
cd -
ccctx ingest flask</code></pre></div><p>The summary should report a handful of changed files rather than the whole repository, and it should be noticeably faster than the first ingest was.</p><p>Test deletion handling by removing a file, re-ingesting, and confirming its symbols have gone:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;8771eab9-10b9-4973-b14e-94f21200fb44&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">rm ~/ccctx-repos/flask/src/flask/json/tag.py
ccctx ingest flask
ccctx search &#8220;json tag serialisation&#8221; --repo flask</code></pre></div><p>No result should point at the deleted file. Restore it with <code>git checkout</code> afterwards.</p><p>Finally check the full rebuild:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;375a04d6-ac6a-418a-b53f-13fe647cd95f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx ingest flask --full</code></pre></div><p>This should re-index everything, with counts matching your original ingest.</p><h3><strong>Step 6</strong></h3><p>In this step your goal is to turn the relationships between bits of code into something you can query.</p><p>Searching finds code that looks like what you asked for. It cannot tell you what will break if you change a function. For that you need the structure of the code, not just its text.</p><p>While you are ingesting, extract the relationships you can see: which files import which other files, which symbols call which other symbols, and which file defines which symbol. You will not catch everything, especially anything dynamic, and that is fine. A graph that captures most direct calls is enormously useful even when it is incomplete.</p><p>Expose those relationships as a SQL property graph with <code>CREATE PROPERTY GRAPH</code>, built over the tables you already have, with files and symbols as vertex tables and imports, calls, and defines as edge tables. You are not copying the data into a separate graph database, you are putting a graph-shaped view over the rows you already stored.</p><p>Then answer the two questions every developer asks before changing anything. &#8220;What calls this?&#8221; and &#8220;What does this call?&#8221; using a <code>GRAPH_TABLE</code> query with a <code>MATCH</code> pattern. Once those work, go further and compute a blast radius: follow the call edges out to a depth of 3 hops and report every symbol that could be affected, along with how many hops away it is. Distance matters, because a direct caller is a very different risk from something three steps downstream.</p><p><strong>Testing:</strong> Pick a function with a decent number of callers and look both ways:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;fb94b14c-9aa6-4332-9584-1ec341d9bfaf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx callers url_for
ccctx calls url_for</code></pre></div><p>Cross-check the results against <code>grep -rn &#8220;url_for&#8221; ~/ccctx-repos/flask</code>. Your graph should find the real call sites, and unlike grep, it should not be confused by the string appearing in a comment or a docstring.</p><p>Now try the blast radius:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;8d22ae38-fce6-4642-b809-f6c6e0c0b50d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx impact url_for --depth 3</code></pre></div><p>You should get a list of affected symbols with a hop count, and there should be more results at 3 hops than at 1:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;334d0bda-8358-4f30-8a41-1f553098c9e2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx impact url_for --depth 1 | wc -l
ccctx impact url_for --depth 3 | wc -l</code></pre></div><h3><strong>Step 7</strong></h3><p>In this step your goal is to hand all of this to an AI client over MCP.</p><p>Everything so far has been for you at a terminal. Now make it available to the tools you actually code with. Expose the brain as an MCP server over standard input and output. There is no network listener and no authentication to worry about, because the client starts your server as a child process and talks to it over standard io.</p><p>Provide four tools, described well enough that a model can work out when to reach for each one. Tool descriptions are effectively prompts, so it is worth spending time on them.</p><ul><li><p>A <code>search_code</code> tool that takes a query, optional repository and language filters, and a result count that defaults to 10. It should return the file path, symbol name, line range, and a snippet for each hit.</p></li><li><p>A <code>get_symbol</code> tool that returns the full source of a named symbol, along with its file path and line range, for when the snippet is not enough.</p></li><li><p>A <code>find_callers</code> tool and an <code>impact_of</code> tool, both backed by the property graph you built in Step 6, so a client can trace call sites and work out the consequences of a change without opening a single file.</p></li></ul><p><strong>Testing:</strong> Start with the MCP Inspector, which lets you call tools by hand:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;070f0459-79bf-4faa-beb3-58a2b2903732&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">npx @modelcontextprotocol/inspector ccctx mcp</code></pre></div><p>Confirm all four tools appear with their descriptions and parameters, then call <code>search_code</code> with <code>{&#8221;query&#8221;: &#8220;how are requests routed&#8221;, &#8220;repo&#8221;: &#8220;flask&#8221;}</code> and check you get the same results your CLI gives.</p><p>Then wire it into a real client and use it for some real coding:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;baf6e8bf-9f3a-4959-9ae6-21c4505e7f6e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">claude mcp add ccctx -- ccctx mcp</code></pre></div><p>Start a session in an unrelated directory, so the client has no filesystem access to the code and has to use your server, and ask it something like &#8220;using ccctx, how does Flask decide which view function handles a request?&#8221;. Watch which tools it chooses. If it reaches for the wrong one, your descriptions need work, and that is a genuinely useful thing to learn.</p><h3><strong>Step 8</strong></h3><p>In this step your goal is to have your tool write and maintain its own architecture notes.</p><p>Search answers narrow questions well. It is much weaker at &#8220;what does this module actually do,&#8221; because that answer is not written down in any single chunk. So have your tool write it down.</p><p>Generate a wiki page for each module or directory once it holds at least 10 indexed symbols. A page should cover what the module is for, its key symbols, and which other modules it depends on. You already have everything needed to work that out: the symbols, and the import edges from Step 6.</p><p>Store each page&#8217;s citations as rows pointing at real file and symbol records, not as text in the prose. This is the part that makes the wiki trustworthy. A citation that is just a sentence saying &#8220;<code>see app.py line 40</code>&#8221; starts rotting the moment someone edits that file, and nothing tells you. A citation that is a foreign key cannot point at something that is not there.</p><p>That also gives you staleness for free. When the commit SHA of a symbol a page cites changes, mark that page stale, and recompile stale pages on the next re-ingestion. A wiki that quietly goes out of date is worse than no wiki, because people keep believing it.</p><p>Then make the pages useful: expose a <code>read_wiki</code> MCP tool, and include wiki pages in your hybrid search results alongside code symbols, so a question about a whole module can be answered by the page rather than by ten disconnected functions.</p><p><strong>Testing:</strong> Build the wiki and read a page:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;83353fdc-0304-4d0d-b2fb-11f97ceaab00&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx wiki build --repo flask
ccctx wiki list --repo flask
ccctx wiki show flask/json</code></pre></div><p>The page should describe what the module does and name real symbols from it. Check the citations are stored as references rather than prose:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e98624fe-8737-4ed4-9532-2fe41e298a60&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx wiki citations flask/json</code></pre></div><p>Every citation should resolve to a symbol that exists. Now test staleness by changing a cited function:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;cccc94ad-b333-48a8-9025-18b9212b612a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">cd ~/ccctx-repos/flask
# edit a function that the flask/json page cites, then commit it
git commit -am &#8220;tweak&#8221;
cd -
ccctx ingest flask
ccctx wiki list --repo flask</code></pre></div><p>The <code>flask/json</code> page should be marked stale and then recompiled. Finally, confirm pages show up in search:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;4141763d-4abd-4571-a9d3-186da931137a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx search &#8220;what does the json module do&#8221;</code></pre></div><p>A wiki page should rank above the individual functions.</p><h3><strong>Step 9</strong></h3><p>In this step your goal is to give your server a memory, so it stops working the same things out over and over. You can draw on the ideas for<a href="iframe.php?url=https%3A%2F%2Fblogs.oracle.com%2Fdevelopers%2Foracle-ai-agent-memory-a-governed-unified-memory-core-for-enterprise-ai-agents"> AI Agent Memory</a> and if you&#8217;re working in Python, leverage<a href="iframe.php?url=https%3A%2F%2Fwww.oracle.com%2Fdatabase%2Fai-agent-memory%2F"> Oracle&#8217;s AI Agent Memory Python package</a> for this step.</p><p>Every time a client calls one of your tools, record it: the question, which tool was used, what came back, and when. This is your episodic memory, a raw log of everything that has happened. On its own it is not much use, because by next week there will be thousands of rows and no way to find anything in them.</p><p>So distil it. Turn those raw runs into durable facts about the codebase, each with a subject, the fact itself, a confidence score, and the run IDs it came from so you can always trace it back. Merge duplicates as you go rather than accumulating fifty near-identical variations of the same observation. Run this on demand, and automatically after each re-ingestion, so the memory keeps pace with the code.</p><p>Then expose it. A <code>recall</code> tool should return the relevant facts and recent runs for a repository, so an agent opens a session already knowing what has been worked out before. This is the loop that makes the whole thing self-improving: the server answers a question, records what it found, distils it into a fact, and hands that fact back the next time someone asks something similar.</p><p><strong>Testing:</strong> Ask your server several related questions through the MCP Inspector or a real client, then look at what it remembered:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;57844074-530b-4517-96d8-52682db62439&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx memory list --repo flask --limit 10
</code></pre></div><p>You should see one row per tool call. Now distil them:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e8d389d7-8fbd-4b40-bd2c-1376f3e8e767&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx memory consolidate --repo flask
ccctx memory facts --repo flask</code></pre></div><p>You should get a much shorter list of facts than you had runs, each traceable to the runs it came from. Ask two near-identical questions, consolidate again, and confirm you get one merged fact rather than two.</p><p>Then check recall works end-to-end. Start a fresh client session and ask &#8220;what do you already know about this codebase?&#8221;. The client should call <code>recall</code> and come back with the facts, without searching for them.</p><h3><strong>Step 10</strong></h3><p>In this step your goal is to let your server learn from which results actually turned out to be useful.</p><p>Start recording when a client uses a result by citing it in an answer or opening it. Mark it as used, so you can tell the difference between a result you returned and a result that helped.</p><p>Then feed that back into ranking carefully. Boost frequently used symbols on future searches, but cap the boost at 20 percent. Feedback loops in search have a nasty habit of eating themselves: popular results get boosted, so they get shown more, so they get used more, so they get boosted further, until your search only ever returns the same five things. A bounded boost nudges the ordering without ever overruling relevance.</p><p>Add decay for the same reason. Halve the weight of feedback every 30 days, so what the tool learned during last quarter&#8217;s refactor fades out instead of haunting your searches forever.</p><p><strong>Testing:</strong> Build your gold set as a simple file of questions and expected paths, then take a baseline:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;db99510d-b841-4206-b2b7-2bc4e3c4c8bd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx eval --gold gold.json

Questions: 20
recall@10: 0.65 (13/20)</code></pre></div><p>Now generate feedback by running sessions and marking useful results, then measure again:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;4dff8180-3f29-47ca-875d-170f4ab334e6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx eval --gold gold.json --with-feedback</code></pre></div><p>Recall should improve, or at least hold steady. If it drops, your boost is too strong and is pushing correct answers out of the top 10, which is exactly the failure this step is designed to let you catch.</p><p>Check the cap is real by hammering a single symbol with feedback and confirming it does not take over:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;7573c808-8ddf-4dff-8860-27cc4425f378&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx search &#8220;database connection pooling&#8221;</code></pre></div><p>A heavily used but irrelevant symbol should still not appear for an unrelated query.</p><p>Test decay by backdating some feedback and confirming its influence has halved:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;a58d7a4d-0a57-4228-981a-fd9a47f37ffe&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">ccctx feedback list --repo flask</code></pre></div><h3><strong>Going Further</strong></h3><p>Once you have the core server working, here are some ideas to take it further:</p><ul><li><p>Index git history as searchable content: commit messages, diffs, and blame. It lets the brain answer &#8220;why is this code like this&#8221; and not just &#8220;what does it do,&#8221; which is often the question you actually have.</p></li><li><p>Add an MCP tool that answers &#8220;who should review this change&#8221; by combining the blast radius from Step 6 with blame data.</p></li><li><p>Extend staleness detection from wiki pages to semantic facts, so the memory knows when what it believes about the code has been invalidated by a commit.</p></li><li><p>Expose the whole thing over HTTP instead of stdio, with OAuth, an email allowlist, and a read-only flag that disables any tool that writes. That is what you need before a team can share one brain.</p></li><li><p>Move the database to an Always Free Autonomous AI Database instance in the cloud, so your index is available from any machine.</p></li><li><p>Add JSON Relational Duality views over your content tables, so the same rows can be read as clean JSON documents by your application and as normalised relational data by your SQL.</p></li><li><p>Support incremental indexing triggered by a git hook or a file watcher, so the index updates as you work rather than when you remember to run it.</p></li><li><p>Let the server suggest where new code should live by finding the module whose wiki page best matches a description of what you are about to build.</p></li><li><p>Benchmark your retrieval against a plain grep and against your editor&#8217;s built-in search on the same gold set, and see where each one wins.</p></li></ul><h3><strong>Share Your Solutions!</strong></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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social"> Bluesky</a> or<a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F"> LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the<a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions"> Coding Challenges Shared Solutions</a> Github repo.</p><h3><strong>Request for Feedback</strong></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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social"> Bluesky</a>,<a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F"> LinkedIn</a> or through<a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F"> SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #138 - DMB]]></title><description><![CDATA[This challenge is to build your own DBM.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-138-dmb</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-138-dmb</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 26 Sep 2026 08:01:34 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff94a1b6a-0210-4bc4-97f8-e5a06ee12e5a_2400x1666.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 #139 - DBM</h2><p>This challenge is to build your own version of DBM, the classic Unix key-value database library.</p><p>DBM has been around since the earliest days of Unix. It was written by Ken Thompson and released by AT&amp;T in 1979. The name stands for Database Manager, and along with its descendants (ndbm, gdbm, Berkeley DB) it has been quietly powering everything from mail servers to Apache web server modules for decades.</p><p>DBM was one of the first NoSQL systems, a key-value store that let you fetch data by a single key, long before NoSQL became popular. Showing once again that everything that&#8217;s new, is old.</p><p>DBM stores key-value pairs on disk using a clever trick called extendible hashing. Instead of a fixed-size hash table that you have to rebuild when it fills up, extendible hashing grows gracefully as you add keys. It splits buckets when they overflow maps hash values to buckets. The result is fast single-key lookups that only need a couple of disk reads, no matter how large your database grows.</p><h2><strong>Fancy Winning A MacBook Neo or some Merch?Then Join Kesta&#8217;s Hacktober Event.</strong></h2><p>This October I&#8217;m pleased to be part of the <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4cZ83bN">Kestra Hacktober event</a>. It&#8217;s a great way to contribute to open source this October and win some great prizes!</p><p>There are two ways to enter:</p><ol><li><p>Fix an issue in their open-source codebase.</p></li></ol><p>They&#8217;ve identified and scoped a number of issues across their core and plugin repositories. They&#8217;re tagged good-first-issue, so you can find one that interests you.</p><ol start="2"><li><p>Build a blueprint, a ready-to-use flow template that other developers can pick up and run.</p></li></ol><p>Whichever way you take part, there are prizes to be won!</p><p>&#129351; 1st place: 13&#8221; MacBook Neo</p><p>&#129352; 2nd place: Apple iPad</p><p>&#129353; 3rd place: $150 Amazon gift card</p><p>You can find out more and sign up on <a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4cZ83bN">Kestra&#8217;s Hacktober page</a>.</p><p>OK, on with the coding challenge!</p><h2>The Challenge - Building Your Own DBM</h2><p>In this challenge you&#8217;re going to build your own version of DBM. You&#8217;ll start with a simple key-value store in memory, work through extendible hashing, then move the whole thing to disk. By the end you&#8217;ll have a working on-disk hash table database with its own command-line tool.</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. DBM involves low-level disk I/O and binary data handling, so pick a language you&#8217;re comfortable with for reading and writing binary files. C, Go, Rust, Python, and Java all work well here. If you&#8217;re looking to learn something new, Go&#8217;s standard library has excellent support for binary encoding and file I/O or C will give you the most authentic &#8220;building a Unix tool&#8221; experience. Rust gives you the &#8220;rewrite in rust&#8221; modern vibe.</p><p>Before you start coding, spend a few minutes playing with a real DBM implementation to get a feel for how it works:</p><pre><code><code># Install GNU dbm if you don't have it

# Create a database and add some keys
echo 'store greeting "Hello, world!"' | gdbmtool test.db
echo 'store name Alice' | gdbmtool test.db
echo 'store language English' | gdbmtool test.db

# Fetch keys back
echo 'fetch greeting' | gdbmtool test.db
echo 'fetch name' | gdbmtool test.db

# List every key with its value
echo 'list' | gdbmtool test.db

# Delete a key
echo 'delete language' | gdbmtool test.db

# Try to fetch a deleted key
echo 'fetch language' | gdbmtool test.db</code></code></pre><p>Notice how the store operation inserts or updates a key, fetch retrieves it, and delete removes it. The list command iterates over all keys in the database. These are the core operations you&#8217;ll be implementing.</p><h3>Step 1</h3><p>In this step your goal is to build a simple in-memory key-value store, a hash table that supports put, get, and delete operations.</p><p>Think of it as building a dictionary or map, but one that you&#8217;ll later move to disk. For now, keep it simple: your hash table should take arbitrary byte sequences as keys and values, use a hash function to determine where each key lives, and handle collisions somehow (chaining or open addressing are both fine).</p><p>You don&#8217;t need to worry about persistence yet, keep everything in memory. The goal is to get a working hash table that can store, retrieve, and delete key-value pairs.</p><p><strong>Testing:</strong> Write a small test program that exercises your hash table.</p><h3>Step 2</h3><p>In this step your goal is to replace your simple hash table with the extendible hashing that powers DBM.</p><p>Fixed-size hash tables are fine for small in-memory stores, but they don&#8217;t scale. On disk, if you allocate 10,000 buckets upfront and only store 100 keys, you&#8217;re wasting disk space. If you start with 10 buckets and later need to store 100,000 keys, you&#8217;d have to rebuild the entire table. Extendible hashing solves this by growing the database on demand.</p><p>Here&#8217;s how it works. You have the directory, which is an array of pointers to buckets. Each bucket has a fixed capacity and a local depth. The directory has a global depth. You use the most significant bits of the hash value to index into the directory, which tells you which bucket to use.</p><p>When a bucket fills up, you split it: the bucket&#8217;s local depth increases by one, and you redistribute its entries between the original bucket and a new one based on the next bit of the hash. If the bucket&#8217;s local depth was already equal to the global depth, you double the directory size and increment the global depth first.</p><p>The result: as you add keys, the directory grows and buckets split, but you never need to rehash everything. Each lookup requires reading one bucket from disk (plus following the directory), which is typically just one or two disk reads.</p><p>You can read more about <a href="iframe.php?url=https%3A%2F%2Falgorithmroom.com%2Fdsa%2Fextended-hashing">extended hashing here</a>.</p><p><strong>Testing:</strong> Write a test that inserts enough keys to trigger at least three bucket splits. Then verify you can retrieve every key you inserted. Print out the directory state (global depth, directory size, bucket count) at each split to help debug:</p><pre><code><code>Inserting key 1:  global depth=1, buckets=2, directory size=2
Inserting key 2:  global depth=1, buckets=2, directory size=2
Inserting key 3:  global depth=1, buckets=3, directory size=2
Inserting key 4:  global depth=2, buckets=4, directory size=4</code></code></pre><p>Retrieve every key and confirm the values are correct. Delete a few keys and verify they&#8217;re gone. Insert new keys into the freed space and confirm they end up in the right buckets.</p><h3>Step 3</h3><p>In this step your goal is to persist your database to disk.</p><p>You&#8217;ll need to design an on-disk format. The original DBM used two files: a <code>.dir</code> file for the directory and a <code>.pag</code> file for the bucket data. GDBM and Berkeley DB use a single file. Either approach works. Pick whichever feels simpler to you.</p><p>Each bucket on disk should contain a header with the bucket&#8217;s local depth, a count of entries, and space for the key-value pairs themselves. Since keys and values can vary in length, you&#8217;ll need a way to encode where each record starts and how long it is within the bucket.</p><p>When a bucket overflows and can&#8217;t fit another entry - even after splitting - you&#8217;ll need overflow buckets.</p><p>Make your put, get, and delete operations read from and write to disk rather than memory. Think about when to flush writes: flushing after every operation guarantees durability but is slow; buffering writes is faster but risks losing data on a crash. Start with flushing after every mutation for correctness, then consider adding an option to buffer later.</p><p><strong>Testing:</strong> Create a new database, insert several keys, then reopen it and verify all keys are still there:</p><pre><code><code># First run: create and populate
ccdbm create test.db
ccdbm put test.db key1 "value one"
ccdbm put test.db key2 "value two"

# Second run: reopen and verify
ccdbm get test.db key1     # should print "value one"
ccdbm get test.db key2     # should print "value two"</code></code></pre><p>Delete a key, close and reopen, and confirm the deleted key is truly gone. Insert enough keys to trigger overflow buckets, then verify retrieval still works correctly. Use a hex dump tool (<code>xxd</code> or <code>hexdump</code>) to inspect your database file and confirm the on-disk format matches what you expect.</p><h3>Step 4</h3><p>In this step your goal is to support iterating over all keys in the database with firstkey and nextkey operations.</p><p>Hash tables are great for single-key lookups but not naturally suited for iteration. DBM solves this by providing firstkey() to get the first key in the database (whatever that means for a hash table) and nextkey(key) to get the next key after a given one.</p><p>The simplest approach is to walk through the directory entries and then through each bucket&#8217;s records in order. Since the directory is indexed by hash bits, the iteration order will seem random, that&#8217;s expected for a hash-based store.</p><p>You should also think about what happens when keys are deleted during iteration. In the original DBM, deleted records were simply marked as empty slots within their bucket. Your iteration should skip over these deleted slots.</p><p><strong>Testing:</strong> Populate a database with several keys, then iterate over all of them and confirm every key you inserted is returned exactly once:</p><pre><code><code>ccdbm put test.db apple "red"
ccdbm put test.db banana "yellow"
ccdbm put test.db cherry "red"

ccdbm list test.db
# Should print: apple, banana, cherry (order may vary)</code></code></pre><p>Delete a key, then iterate again and confirm the deleted key no longer appears. Insert new keys into the empty slots and confirm iteration picks them up.</p><h3>Step 5</h3><p>In this step your goal is to implement a reorganise (compact) operation.</p><p>Over time, as keys are inserted and deleted, your database accumulates waste: deleted records leave empty slots in buckets. A reorganise operation rebuilds the entire database from scratch, rehashing every key into fresh buckets and reclaiming the wasted space.</p><p>The process is straightforward: iterate over every key-value pair in the old database, insert them all into a new empty database, then replace the old files with the new ones. After reorganising, there should be no empty slots and no overflow buckets (assuming your bucket size and hash function distribute keys well).</p><p><strong>Testing:</strong></p><pre><code><code># Insert and delete some keys to create fragmentation
ccdbm put test.db a "1"
ccdbm put test.db b "2"
ccdbm put test.db c "3"
ccdbm delete test.db a
ccdbm delete test.db c

# Show stats before reorganise
ccdbm stats test.db

# Reorganise
ccdbm reorganise test.db

# Show stats after
ccdbm stats test.db</code></code></pre><p>Verify the database file is smaller after reorganising. Confirm all remaining keys are still retrievable after the operation.</p><h3>Step 6</h3><p>In this step your goal is to build a command-line interface for your DBM. You&#8217;ve been using it piecemeal for testing - now wrap everything in a clean CLI tool called <code>ccdbm</code>.</p><p>Your CLI should support these commands:</p><ul><li><p><code>ccdbm create &lt;file&gt;</code> - create a new empty database file</p></li><li><p><code>ccdbm put &lt;file&gt; &lt;key&gt; &lt;value&gt;</code> - insert or update a key-value pair</p></li><li><p><code>ccdbm get &lt;file&gt; &lt;key&gt;</code> - retrieve the value for a key</p></li><li><p><code>ccdbm delete &lt;file&gt; &lt;key&gt;</code> - delete a key</p></li><li><p><code>ccdbm list &lt;file&gt;</code> - list all keys in the database</p></li><li><p><code>ccdbm stats &lt;file&gt;</code> - show database statistics</p></li><li><p><code>ccdbm reorganise &lt;file&gt;</code> - compact the database</p></li></ul><p>The stats command should display useful information: the number of buckets, global depth, total key count, file size on disk, and number of overflow buckets. This gives the user insight into how their database is performing and whether it might benefit from a reorganise.</p><p><strong>Testing:</strong> Run through a complete workflow with your CLI:</p><pre><code><code># Create
ccdbm create demo.db

# Populate
ccdbm put demo.db title "The Lord of the Rings"
ccdbm put demo.db author "J.R.R. Tolkien"
ccdbm put demo.db year "1954"

# Retrieve
ccdbm get demo.db title    # The Lord of the Rings
ccdbm get demo.db author   # J.R.R. Tolkien

# List all keys
ccdbm list demo.db         # title, author, year

# Check stats
ccdbm stats demo.db

# Delete one
ccdbm delete demo.db year

# Verify deletion
ccdbm get demo.db year     # Error: key not found

# Reorganise
ccdbm reorganise demo.db

# Verify data still intact
ccdbm get demo.db title    # The Lord of the Rings</code></code></pre><p>Try with a larger dataset - insert a few thousand keys and verify both retrieval and iteration work correctly. Measure how long a lookup takes and how long a reorganise takes as the database grows.</p><h3>Going Further</h3><p>Here are some ideas to take your implementation further:</p><ul><li><p>Support storing arbitrary binary data by accepting key and value inputs as hex strings (e.g., <code>ccdbm put test.db hexkey deadbeef</code>)</p></li><li><p>Add a batch mode that reads commands from a file or stdin, processing multiple operations in a single session</p></li><li><p>Benchmark your DBM against GNU dbm or Berkeley DB on the same dataset and see how your performance compares</p></li><li><p>Try different hash functions (djb2, FNV-1a, MurmurHash3) and measure how evenly they distribute keys across buckets</p></li><li><p>Add support for key expiry with a time-to-live (TTL), where keys are automatically removed after a configurable duration</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge  #137 - fzf]]></title><description><![CDATA[This challenge is to build your own fuzzy finder tool.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-137-fzf</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-137-fzf</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 19 Sep 2026 08:01:18 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f1e2448-732e-4e56-a445-5b5a9d27808d_2400x1801.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 #137 - fzf</h2><p>This challenge is to build your own version of <code>fzf</code>, the command-line fuzzy finder.</p><p><code>fzf</code> is one of those tools that changes how you use a terminal. You pipe a list of things into it, start typing a few letters, and it narrows a list of a million items down to the one you wanted before you have finished typing. It is used for finding files, checking out git branches, searching shell history, killing processes, and about a hundred other things.</p><p>Underneath the friendly interface there is a surprising amount of interesting engineering: a scoring algorithm that ranks matches the way a human would, a terminal user interface built from raw escape sequences, and a design that is responsive while a huge stream of input. Building your own <code>fzf</code> is a great way to learn all three.</p><h2>The Challenge - Building Your Own fzf</h2><p>In this challenge you&#8217;re going to build your own fuzzy finder. It will read a list of items, take over the terminal, and let the user type a query that filters and re-ranks the list on every keystroke. By the end you&#8217;ll have a tool you can drop into your own shell aliases and scripts in place of the real <code>fzf</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. You&#8217;ll be reading key presses one at a time and drawing directly to the terminal, so look for a language that lets you put the terminal into raw mode and write escape sequences, either from its standard library or from a small terminal library. Avoid anything that hides the terminal from you completely.</p><p>Before you write any code, spend time with the real thing. Install <code>fzf</code> if you don&#8217;t already have it, then read the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fjunegunn%2Ffzf%2Fblob%2Fmaster%2Fman%2Fman1%2Ffzf.1">fzf man page</a> and the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fjunegunn%2Ffzf">project README</a>. Pipe some things into it and watch how it behaves as you type:</p><pre><code><code>find . -type f | fzf
git ls-files | fzf
history | fzf</code></code></pre><p>Pay attention to the small details, like what happens to the screen when you quit, what the counter above the prompt shows, and which items float to the top when you type a few letters.</p><p>You&#8217;ll also want some test data. A big list of lines is ideal, so grab a word list and generate a large file of paths to use throughout the challenge:</p><pre><code><code>wc -l /usr/share/dict/words
find / -type f 2&gt;/dev/null | head -1000000 &gt; /tmp/paths.txt
wc -l /tmp/paths.txt</code></code></pre><p>Keep both files handy, you&#8217;ll use the small one for correctness and the large one for performance.</p><h3>Step 1</h3><p>In this step your goal is to build the skeleton of the finder: read items, show them, and let the user pick one.</p><p>Your tool should read newline-delimited items from standard input and display them in an interactive full-screen list, with a prompt where the user&#8217;s query will eventually go. Pressing <code>Enter</code> should accept the item the cursor is on and print it to standard output. Pressing <code>Ctrl-C</code>, <code>Ctrl-G</code> or <code>Esc</code> should abort without printing anything.</p><p>The finder should draw itself on the alternate screen buffer, so when it exits the user&#8217;s original terminal contents and cursor position are exactly as they were before. It should exit with the right status code, because scripts depend on these: <code>0</code> when an item was selected, <code>1</code> when there was no match, <code>2</code> on error, and <code>130</code> when the finder was interrupted.</p><p><strong>Testing:</strong> Run your finder against a small list and select an item:</p><pre><code><code>printf "apple\nbanana\ncherry\n" | ccfzf</code></code></pre><p>You should get a full screen list of the three items, and after pressing <code>Enter</code> you should be back at your normal shell prompt with your previous screen contents intact, and the chosen item printed. Check the exit codes:</p><pre><code><code>printf "apple\nbanana\n" | ccfzf; echo "exit: $?"</code></code></pre><p>Selecting an item should report <code>exit: 0</code>, and pressing <code>Esc</code> should report <code>exit: 130</code>. Try it with a much larger input to make sure nothing falls over:</p><pre><code><code>ccfzf &lt; /tmp/paths.txt</code></code></pre><h3>Step 2</h3><p>In this step your goal is to make the finder filter as the user types.</p><p>Your finder should accept typed characters into the query and narrow the list to the items that match. A match is a fuzzy match: the query characters must appear in the item in order, but they do not have to be next to each other. So <code>abc</code> matches <code>a_big_cat</code> and <code>alphabetacharlie</code>, but not <code>acb</code>.</p><p>Matching should use smart case by default, which means the query is matched case-insensitively unless it contains an uppercase letter, in which case it becomes case-sensitive. The user should be able to override this with <code>-i</code> or <code>--ignore-case</code> to force case-insensitive matching, and <code>+i</code> or <code>--no-ignore-case</code> to force case-sensitive matching.</p><p>Now that there is a query, the rest of the default key bindings should work too. <code>Ctrl-K</code>, <code>Ctrl-P</code> and <code>Up</code> should move the cursor up the screen, and <code>Ctrl-J</code>, <code>Ctrl-N</code> and <code>Down</code> should move it back down. Remember that the default layout draws the list upwards from the prompt with the best match at the bottom, so moving up the screen walks further down the results, and the cursor starts on the best match with nowhere below it to go. <code>Backspace</code> should delete the character before the cursor, <code>Ctrl-W</code> should delete the word before it, <code>Ctrl-U</code> should clear the query, and <code>Ctrl-A</code> and <code>Ctrl-E</code> should jump the cursor to the start and end of the query.</p><p>Finally, add <code>-q</code> or <code>--query=STR</code> so the finder can be started with a query already typed. It saves a lot of typing when you are testing the steps that follow.</p><p><strong>Testing:</strong> Filter the word list and watch the results change on every keystroke:</p><pre><code><code>ccfzf &lt; /usr/share/dict/words</code></code></pre><p>Type <code>fxf</code> and confirm every remaining item contains an <code>f</code>, then a <code>x</code>, then an <code>f</code>, in that order. Type <code>README</code> against a file list and check that smart case has made the match case-sensitive, then try the same query with <code>-i</code> and confirm lower case matches come back:</p><pre><code><code>git ls-files | ccfzf -i</code></code></pre><p>Move up and down with the arrow keys and with the control keys and confirm both work, then check each editing key does what it should. Check that <code>-q</code> starts you off with the list already narrowed:</p><pre><code><code>git ls-files | ccfzf -q readme</code></code></pre><h3>Step 3</h3><p>In this step your goal is to rank the matches so the best one is at the top.</p><p>A fuzzy match on its own is not much use when a query matches ten thousand items. Your finder should score every match and sort the results best first. A good score rewards the things a we care about: characters that matched next to each other, characters that matched at the start of a word (after a space, <code>/</code>, <code>-</code>, <code>_</code> or <code>.</code>), characters that matched at a camelCase transition, and matches that start nearer the beginning of the item.</p><p>Your finder should support <code>--algo=v1|v2</code> and default to <code>v2</code>, which finds the best possible alignment of the query within the item. The <code>v1</code> algorithm should be a faster option that is not guaranteed to find the best match.</p><p>When two items score the same, <code>--tiebreak=CRI[,..]</code> should decide the order, supporting the criteria <code>length</code>, <code>begin</code>, <code>end</code> and <code>index</code>, and defaulting to <code>length</code>. Finally, the list should show its work: highlight the characters of each item that the query matched.</p><p>Have a read of the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fjunegunn%2Ffzf%2Fblob%2Fmaster%2Fsrc%2Falgo%2Falgo.go">fzf algorithm notes</a> if you want to see how the original approaches this, but try it yourself first.</p><p><strong>Testing:</strong> Check that the obvious best match wins:</p><pre><code><code>printf "src/app/main.go\nsrc/main_test.go\nmain.go\ndocs/domain.md\n" | ccfzf -q main</code></code></pre><p><code>main.go</code> should sort above <code>docs/domain.md</code>, and the matched characters in each line should be highlighted. Compare the two algorithms on the same query:</p><pre><code><code>ccfzf --algo=v1 &lt; /tmp/paths.txt
ccfzf --algo=v2 &lt; /tmp/paths.txt</code></code></pre><p>Type the same query into both and confirm <code>v2</code> gives at least as good a top result. Then check tiebreaking:</p><pre><code><code>printf "abc\nabcdef\nabcd\n" | ccfzf -q abc --tiebreak=length</code></code></pre><p>The shortest item should be first. Run it again with <code>--tiebreak=index</code> and confirm the input order is used instead.</p><h3>Step 4</h3><p>In this step your goal is to make the finder start instantly and stay responsive, no matter how much input is coming.</p><p>Right now your finder probably reads all of its input before it draws anything. Real <code>fzf</code> starts up immediately and keeps reading in the background, so the user can begin typing while items are still arriving. Your finder should do the same: show the interface as soon as it starts, then update the item list, the match count and the result list as more input arrives, without ever blocking the keyboard.</p><p>To show what is happening, add an info line that displays how many items currently match out of the total number read so far. It should support <code>--info=default|inline|hidden</code> so the user can move it onto the prompt line or turn it off.</p><p>Two flags change the order items are considered in: <code>--tac</code> reverses the input, and <code>--no-sort</code> leaves matches in input order instead of score order.</p><p>The real test of this step is scale. Your finder should stay interactive with an input of at least one million items, filtering and re-sorting on every keystroke with no pause you can feel.</p><p><strong>Testing:</strong> Check that the finder appears before the input has finished:</p><pre><code><code>find / -type f 2&gt;/dev/null | ccfzf</code></code></pre><p>The interface should appear immediately and the counter should climb while you type. Try a deliberately slow producer:</p><pre><code><code>(for i in $(seq 1 20); do echo "item $i"; sleep 0.5; done) | ccfzf</code></code></pre><p>You should be able to type and move around while new items keep appearing. Then test at scale:</p><pre><code><code>ccfzf &lt; /tmp/paths.txt</code></code></pre><p>Type a query one character at a time and confirm each keystroke feels instant. Check the info line with <code>--info=inline</code> and <code>--info=hidden</code>, and confirm <code>--tac</code> and <code>--no-sort</code> change the order as expected:</p><pre><code><code>ccfzf --tac &lt; /tmp/paths.txt
ccfzf --no-sort &lt; /tmp/paths.txt</code></code></pre><h3>Step 5</h3><p>In this step your goal is to support extended-search mode, which turns the query box into a small search language.</p><p>Extended-search mode is enabled by default. In it, the query is split on spaces and each term must match, so <code>src go</code> finds items containing a fuzzy match for <code>src</code> and a fuzzy match for <code>go</code>. Individual terms can change how they match:</p><ul><li><p><code>'wild</code> matches items that contain <code>wild</code> exactly, not fuzzily.</p></li><li><p><code>^music</code> matches items that start with <code>music</code>.</p></li><li><p><code>.mp3$</code> matches items that end with <code>.mp3</code>.</p></li><li><p><code>!fire</code> matches items that do not contain a fuzzy match for <code>fire</code>.</p></li><li><p><code>a | b | c</code> matches items that match any one of the terms.</p></li></ul><p>Two flags change the rules. <code>+x</code> or <code>--no-extended</code> turns the mode off so the whole query is treated as one fuzzy term. <code>-e</code> or <code>--exact</code> makes every term an exact match, and in that mode a leading <code>'</code> means the opposite, a fuzzy term.</p><p><strong>Testing:</strong> Build a small list you can reason about:</p><pre><code><code>printf "music/song.mp3\nmusic/song.wav\nvideo/clip.mp4\nfirewall.md\nsrc/main.go\n" &gt; /tmp/ext.txt</code></code></pre><p>Then check each form of term:</p><pre><code><code>ccfzf -q "^music .mp3$" &lt; /tmp/ext.txt
ccfzf -q "'song" &lt; /tmp/ext.txt
ccfzf -q "!fire" &lt; /tmp/ext.txt
ccfzf -q "mp3 | mp4" &lt; /tmp/ext.txt</code></code></pre><p>The first should leave only <code>music/song.mp3</code>, and the last should leave the two media files. Confirm <code>+x</code> treats a query with spaces as a single fuzzy term, and that <code>-e</code> makes <code>song</code> an exact match while <code>'song</code> becomes fuzzy again.</p><h3>Step 6</h3><p>In this step your goal is to let the user pick more than one item, and to control the query and the output from the command line.</p><p>Your finder should support <code>-m</code> or <code>--multi</code> for multi-select. <code>Tab</code> should mark the item under the cursor and move on, <code>Shift-Tab</code> should unmark it, marked items should be visibly different in the list, and accepting should print all of the marked items rather than just the one under the cursor. <code>--multi=MAX</code> should limit how many items can be marked.</p><p>Alongside that, add the options that make the finder easy to drive from a script:</p><ul><li><p><code>-print-query</code> prints the final query as the first line of output.</p></li><li><p><code>f</code> or <code>-filter=STR</code> skips the interface completely and prints the matching items to standard output in score order.</p></li><li><p><code>1</code> or <code>-select-1</code> accepts immediately when exactly one item matches, and <code>0</code> or <code>-exit-0</code> exits immediately when nothing matches.</p></li></ul><p><strong>Testing:</strong> Mark a few items and check they all come out:</p><pre><code><code>git ls-files | ccfzf -m</code></code></pre><p>Press <code>Tab</code> on three items, then <code>Enter</code>, and confirm all three are printed. Check the limit is enforced with <code>--multi=2</code>. Then check the scripting options:</p><pre><code><code>git ls-files | ccfzf -f go
git ls-files | ccfzf -q main --print-query
printf "only-one\n" | ccfzf -1 -q only; echo "exit: $?"
printf "apple\n" | ccfzf -0 -q zzz; echo "exit: $?"</code></code></pre><p>The filter run should print matches in the same order the interactive list would show them, without ever clearing the screen. The <code>-1</code> run should print <code>only-one</code> without you pressing anything, and the <code>-0</code> run should exit straight away with <code>exit: 1</code>.</p><h3>Step 7</h3><p>In this step your goal is to make your finder a good citizen of the shell.</p><p>So far your finder always takes over the whole screen. Add <code>--height=[~]HEIGHT[%]</code> so it can instead draw in a window below the cursor, leaving the rest of the terminal visible, with <code>~</code> meaning the window should shrink to fit the content. This is how most people use <code>fzf</code> day to day.</p><p>The list arrangement should be configurable too, with <code>--layout=default|reverse|reverse-list</code>. The default puts the list above a prompt at the bottom, <code>reverse</code> puts the prompt at the top with the list below it, and <code>reverse-list</code> puts the list at the top with the prompt at the bottom. <code>--reverse</code> should be a shorthand for <code>--layout=reverse</code>.</p><p>Then add the features that let it slot into pipelines and key bindings:</p><ul><li><p><code>-ansi</code> should interpret ANSI colour codes in the input, rendering the colours in the list and matching against the text rather than the escape sequences.</p></li><li><p><code>-read0</code> should read NUL-delimited input and <code>-print0</code> should write NUL-delimited output, so your finder is safe with filenames containing newlines.</p></li><li><p><code>-expect=KEYS</code> should accept on any of the listed keys as well as <code>Enter</code>, and print the name of the key that was pressed as the first line of output.</p></li><li><p>When standard input is a terminal, your finder should run <code>$FZF_DEFAULT_COMMAND</code> to produce the item list, and it should read default options from <code>$FZF_DEFAULT_OPTS</code> before the ones on the command line.</p></li><li></li></ul><p><strong>Testing:</strong> Check the window and layouts:</p><pre><code><code>git ls-files | ccfzf --height=40%
git ls-files | ccfzf --height=~40% --reverse</code></code></pre><p>Your shell prompt and previous output should still be visible above the finder, and the second command should shrink the window when few items match. Then check the rest:</p><pre><code><code>grep --color=always -r "func" . | ccfzf --ansi
find . -type f -print0 | ccfzf --read0 --print0 | xargs -0 ls -l
git ls-files | ccfzf --expect=ctrl-o,ctrl-e
export FZF_DEFAULT_COMMAND='find . -type f'
export FZF_DEFAULT_OPTS='--reverse --height=40%'
ccfzf</code></code></pre><p>With <code>--ansi</code> the colours from <code>grep</code> should appear in the list and typing an escape sequence like <code>[0m</code> should match nothing. The <code>--expect</code> run should print <code>ctrl-o</code> on its own line before the selected item. The last run should start the finder with no input piped in, using the command and options from the environment.</p><h3>Step 8</h3><p>In this step your goal is to add the two features that make <code>fzf</code> genuinely powerful: matching on part of an item, and previewing what is under the cursor.</p><p>First, fields. <code>-d</code> or <code>--delimiter=REGEX</code> should split each item into fields, defaulting to AWK-style whitespace splitting. <code>-n</code> or <code>--nth=N[,..]</code> should restrict matching to the given fields, so the user can search on one column and ignore the rest. <code>--with-nth=N[,..]</code> should change which fields are displayed. Both should accept the field index expressions <code>1</code>, <code>-1</code>, <code>3..5</code>, <code>2..</code>, <code>..-3</code> and <code>..</code>.</p><p>Second, previews. <code>--preview=COMMAND</code> should run a command each time the cursor moves and show its output in a preview window beside the list. Inside the command, <code>{}</code> should be replaced with the current item single quoted, <code>{1}</code>, <code>{2}</code> and so on with its fields, <code>{n}</code> with its zero-based index, and <code>{+}</code> with the selected items. <code>--preview-window=[POSITION][,SIZE[%]]</code> should place the window <code>up</code>, <code>down</code>, <code>left</code> or <code>right</code>, defaulting to <code>right,50%</code>.</p><p><strong>Testing:</strong> Search on a single column of a table:</p><pre><code><code>ps aux | ccfzf -n 11.. --with-nth 11..</code></code></pre><p>You should be matching and seeing only the command, not the process statistics. Try a different delimiter:</p><pre><code><code>ccfzf -d : -n 1 &lt; /etc/passwd</code></code></pre><p>Typing a user name should match, but typing part of a home directory path should not. Then check the previews:</p><pre><code><code>git ls-files | ccfzf --preview 'head -50 {}'
git ls-files | ccfzf --preview 'head -50 {}' --preview-window=up,30%
git log --oneline | ccfzf --preview 'git show {1}'</code></code></pre><p>The preview should update as you move the cursor, the window should move where you asked it to, and the <code>git log</code> example should show the full commit for the hash in the first field.</p><h3>Going Further</h3><p>Once you have the core finder working, here are some ideas to take it further:</p><ul><li><p>Add <code>-prompt</code>, <code>-pointer</code> and <code>-marker</code> so the interface can be styled.</p></li><li><p>Add <code>-color</code> to support custom colour schemes, including the built-in <code>dark</code>, <code>light</code> and <code>16</code> themes.</p></li><li><p>Add <code>-bind</code> so users can attach their own actions to their own keys.</p></li><li><p>Add <code>-header</code> and <code>-header-lines</code> to keep a fixed header above the list.</p></li><li><p>Add <code>-cycle</code> so moving past the end of the list wraps around to the start.</p></li><li><p>Add <code>-history</code> so queries are saved between runs and can be recalled with <code>Ctrl-P</code> and <code>Ctrl-N</code>.</p></li><li><p>Add the <code>reload</code> action so the item list can be regenerated from a new command while the finder is running.</p></li><li><p>Add <code>-listen</code> so a running finder can be controlled over HTTP.</p></li><li><p>Write the shell integration: <code>Ctrl-T</code> to insert a file path, <code>Ctrl-R</code> to search history, and <code>*&lt;Tab&gt;</code> completion.</p></li><li><p>Benchmark your finder against the real <code>fzf</code> on a list of ten million items and see how close you can get.</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #136 - Hyperfine]]></title><description><![CDATA[This challenge is to build your own benchmarking tool.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-136-hyperfine</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-136-hyperfine</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 12 Sep 2026 08:03:11 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc1d01282-cf5d-4eb0-98b8-c34b59b6ee91_2400x1746.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 #136 - Hyperfine</h2><p>This challenge is to build your own version of <code>hyperfine</code>, a command-line benchmarking tool.</p><p>When we want to benchmark the performance of a program or script the easy answer is to use <code>time</code>. If the old tool took 45 minutes and the new one takes 4 minutes, time is good enough. If the difference isn&#8217;t so glaringly obvious you need. proper benchmarking tool.</p><p>A proper benchmarking tool exists to run each command several times, throw away the noise, work out how confident it can be in the result, and warn you when the numbers should not be trusted.</p><p>That is what makes hyperfine such an interesting project. The process side of it is straightforward. The statistics and interpretation of the results is interesting and fun!</p><h2>The Challenge - Building Your Own Hyperfine</h2><p>In this challenge you&#8217;re going to build your own version of <code>hyperfine</code>, a benchmarking tool for the command line. Your tool will take one or more commands, run each of them enough times to get a reliable answer, and report how long they took with a proper measure of how much that time varied. It will compare commands against each other, run the same command with different parameters, export the results for further analysis, and warn you when something about the measurements looks suspicious.</p><p>The tricky parts are not where you might expect. Spawning a process and timing it is easy. Working out how many times to run it, removing the cost of starting a shell from the measurement, deciding whether a slow run is a real result or a random blip, and putting a sensible error bar on a comparison, those are the parts that will make you think.</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 will need to start a child process, wait for it to finish, and measure both the elapsed wall clock time and the processor time it used in user space and in the kernel. Most languages expose the second part through something like <code>getrusage</code> or <code>wait4</code>, so it is worth checking how yours does it before you start.</p><p>Install the real <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fsharkdp%2Fhyperfine">hyperfine</a> so that you have something to compare your output against as you go. Run it on a few commands and get a feel for what it prints:</p><pre><code><code>hyperfine 'sleep 0.3'
hyperfine --runs 5 'sleep 0.3'
hyperfine 'sleep 0.1' 'sleep 0.2'</code></code></pre><p>Throughout this challenge I will use <code>sleep</code> as the command being benchmarked. That is deliberate: <code>sleep 0.3</code> should take almost exactly 300 milliseconds, so you always know what the right answer looks like and can tell when your tool is wrong. I&#8217;ll call our version <code>cchyperfine</code>, but name yours whatever you like.</p><h3>Step 1</h3><p>In this step your goal is to time a single run of a single command.</p><p>Your tool should take one command as an argument, run it, and report how long it took. Run the command through an intermediate shell, the way <code>hyperfine</code> does, so that anything you can type at a shell prompt works as a benchmark: pipes, redirection, quoting, shell built-ins, and so on. On Unix that means <code>/bin/sh -c</code>, on Windows <code>cmd.exe /C</code>.</p><p>As well as the elapsed time, record how much processor time the command used in user space and how much it spent in the kernel. Those two numbers are useful later for telling the difference between a command that is working hard and one that is waiting around.</p><p>Don&#8217;t worry about the output of the benchmarked command appearing on your screen and making a mess. We&#8217;ll deal with that in the last step.</p><p><strong>Testing:</strong></p><pre><code><code>cchyperfine 'sleep 0.3'</code></code></pre><p>You should see a time of roughly 300 milliseconds, plus a small amount of overhead for starting the shell and the <code>sleep</code> process. The user and system times should both be tiny, because <code>sleep</code> does nothing but wait.</p><p>Check that the shell really is involved:</p><pre><code><code>cchyperfine 'echo hello | wc -c'</code></code></pre><p>This should run and report a time rather than complaining about an unknown command or an unexpected <code>|</code>.</p><h3>Step 2</h3><p>In this step your goal is to run a command many times and report the statistics.</p><p>Add a <code>-r</code> / <code>--runs NUM</code> option that runs the command that many times, then report a summary of the results in the same shape that <code>hyperfine</code> uses: a mean with a standard deviation next to it, the user and system times, then the fastest and slowest run along with how many runs were performed.</p><pre><code><code>Benchmark 1: sleep 0.3
  Time (mean &#177; &#963;):     304.7 ms &#177;   1.6 ms    [User: 1.1 ms, System: 1.8 ms]
  Range (min &#8230; max):   302.6 ms &#8230; 308.4 ms    5 runs</code></code></pre><p>Pick the time unit automatically based on how big the numbers are, so a command that takes a few hundred microseconds is not reported as <code>0.0003 s</code>. Add a <code>-u</code> / <code>--time-unit</code> option taking <code>microsecond</code>, <code>millisecond</code> or <code>second</code> for when the reader wants to choose for themselves.</p><p><strong>Testing:</strong></p><pre><code><code>cchyperfine --runs 5 'sleep 0.3'</code></code></pre><p>The mean should be close to 300 ms and the standard deviation should be small, a couple of milliseconds at most on an idle machine. Compare your output against the real thing:</p><pre><code><code>hyperfine --runs 5 'sleep 0.3'</code></code></pre><p>Then check the unit handling:</p><pre><code><code>cchyperfine --runs 5 -u second 'sleep 0.3'
cchyperfine --runs 50 'sleep 0.005'</code></code></pre><p>The first should report seconds, and the second should choose milliseconds by itself.</p><h3>Step 3</h3><p>In this step your goal is to decide how many runs to perform automatically.</p><p>Asking the reader how many runs they want is not much of an improvement on running the command by hand. Instead, when no exact number of runs is given, you should work it out: time the first run, use that to estimate how many runs fit into a minimum benchmarking time of three seconds, and go with that. Never do fewer than ten runs, however fast the command is, because you cannot say anything useful with just a handful of measurements.</p><p>Add <code>-m</code> / <code>--min-runs NUM</code> (defaulting to 10) and <code>-M</code> / <code>--max-runs NUM</code> (no limit by default) so the reader can move those boundaries.</p><p>A benchmark that runs for more than a few seconds also needs to show some sign of life, so display a progress bar while the runs are happening, along with an estimate of how much longer it will take.</p><p><strong>Testing:</strong></p><pre><code><code>cchyperfine 'sleep 0.3'</code></code></pre><p>Three seconds divided by 0.3 seconds is 10, and the minimum is 10, so this should perform 10 runs. Now try a much faster command:</p><pre><code><code>cchyperfine 'sleep 0.01'</code></code></pre><p>This should settle on roughly 300 runs, and you should watch a progress bar fill up while it does them. Then check the boundaries are respected:</p><pre><code><code>cchyperfine --min-runs 50 'sleep 0.3'
cchyperfine --max-runs 20 'sleep 0.01'</code></code></pre><p>The first should perform 50 runs even though 10 would have been enough, and the second should stop at 20.</p><h3>Step 4</h3><p>In this step your goal is to remove the cost of the shell from your measurements.</p><p>Every time you run a command through a shell, you are also measuring the cost of starting that shell. It is only a few milliseconds, but if the command you are benchmarking takes five milliseconds then a few milliseconds is an enormous error.</p><p>Before benchmarking anything, your tool should measure how long the shell takes to start and do nothing. You can work this out by timing 50 runs of an empty command. Subtract that overhead from every measurement afterwards, so what you report is the cost of the command rather than the cost of the shell.</p><p>Sometimes the reader does not want a shell at all. Add <code>-S</code> / <code>--shell</code> to choose a different shell, with <code>none</code> meaning &#8220;run the command directly, with no shell in the middle&#8221;, and <code>-N</code> as a shorthand for <code>--shell=none</code>.</p><p><strong>Testing:</strong></p><pre><code><code>cchyperfine 'true'
cchyperfine -N 'true'</code></code></pre><p>Both should report a time close to zero rather than the few milliseconds the shell costs, because that overhead has been taken off. Compare with the real tool, which does exactly the same thing:</p><pre><code><code>hyperfine 'true'</code></code></pre><p>Then check the shell options behave:</p><pre><code><code>cchyperfine -N 'sleep 0.3'
cchyperfine --shell=bash 'sleep 0.3'</code></code></pre><p>Both should still report about 300 ms.</p><h3>Step 5</h3><p>In this step your goal is to control what happens around each run.</p><p>Real benchmarks need set up. Sometimes you need to build the thing first, sometimes you need to clear a cache before every single run, and sometimes you need to kill a server you started.</p><p>Add five options for this:</p><ul><li><p><code>w</code> / <code>-warmup NUM</code> performs NUM runs before the timed ones and throws the results away, which fills disk caches so that the numbers you keep are all measured under the same conditions.</p></li><li><p><code>s</code> / <code>-setup CMD</code> runs once before the timed runs of a benchmark begin.</p></li><li><p><code>p</code> / <code>-prepare CMD</code> runs before each timed run.</p></li><li><p><code>C</code> / <code>-conclude CMD</code> runs after each timed run.</p></li><li><p><code>c</code> / <code>-cleanup CMD</code> runs once after all the runs of a benchmark have finished.</p></li></ul><p>The time taken by the prepare and conclude commands must not end up in the measurement. Only the command being benchmarked is timed. <code>--prepare</code> and <code>--conclude</code> can each be given once for all commands, or once per command when several commands are being benchmarked.</p><p><strong>Testing:</strong> The order things happen in is the thing to check here, so have each hook leave a trace:</p><pre><code><code>rm -f order.log
cchyperfine --runs 3 \
  --setup 'echo setup &gt;&gt; order.log' \
  --prepare 'echo prepare &gt;&gt; order.log' \
  --conclude 'echo conclude &gt;&gt; order.log' \
  --cleanup 'echo cleanup &gt;&gt; order.log' \
  'echo run &gt;&gt; order.log'
cat order.log</code></code></pre><p>You should see <code>setup</code> once at the start, then <code>prepare</code>, <code>run</code> and <code>conclude</code> repeated three times, then <code>cleanup</code> once at the end.</p><p>Check that the hooks are not being timed:</p><pre><code><code>cchyperfine --runs 5 --prepare 'sleep 0.2' 'sleep 0.1'</code></code></pre><p>The reported mean should be about 100 ms, not 300 ms. Finally, try a warmup on something that touches the disk:</p><pre><code><code>cchyperfine --warmup 3 --runs 10 'find . -name "*.md"'</code></code></pre><h3>Step 6</h3><p>In this step your goal is to tell the reader when the numbers cannot be trusted.</p><p>This is what separates a good benchmarking tool from <code>time</code> in a loop. Your tool should notice four things and say so.</p><p>First, when a benchmarked command exits with a non-zero status, stop and report an error, because timing something that failed is meaningless. Add <code>-i</code> / <code>--ignore-failure</code> for the cases where the reader knows the command exits non-zero and wants the timing anyway, printing the warning <code>Ignoring non-zero exit code.</code></p><p>Second, when a command takes less than five milliseconds, warn that the result may be inaccurate, because the shell startup time cannot be measured much more precisely than that, and suggest the <code>-N</code> option.</p><p>Third, look for statistical outliers. Work out how far each measurement sits from the median, measured in units of the median absolute deviation, and treat anything beyond a threshold as an outlier. This is a more robust way of spotting a stray result than using the mean and standard deviation, because one wild measurement does not drag the threshold along with it. When you find outliers, suggest re-running on a quiet machine, and mention <code>--warmup</code> and <code>--prepare</code> if they are not already being used.</p><p>Fourth, check whether the first run in particular was much slower than the rest. That is the fingerprint of a cold cache rather than a busy machine, so it should get its own warning, pointing the reader at <code>--warmup</code> and <code>--prepare</code>.</p><p><strong>Testing:</strong></p><pre><code><code>cchyperfine 'false'</code></code></pre><p>This should report an error and exit non-zero. Then:</p><pre><code><code>cchyperfine -i 'false'</code></code></pre><p>This should complete and print the warning about ignoring the non-zero exit code. For the fast execution warning:</p><pre><code><code>cchyperfine -N 'true'</code></code></pre><p>For the outlier warning, run a benchmark while giving your machine something else to do in another terminal, for example a build or <code>yes &gt; /dev/null</code>:</p><pre><code><code>cchyperfine --runs 30 'sleep 0.05'</code></code></pre><p>For the slow first run warning, make a large file, drop it out of your cache if you can, and read it without a warmup:</p><pre><code><code>dd if=/dev/urandom of=big.bin bs=1048576 count=200
cchyperfine --runs 10 'wc -c big.bin'</code></code></pre><h3>Step 7</h3><p>In this step your goal is to compare several commands with each other.</p><p>Your tool should accept more than one command and benchmark each in turn, numbering them <code>Benchmark 1</code>, <code>Benchmark 2</code> and so on. Afterwards, print a summary that compares them: find the fastest, then express every other command as a multiple of it, with an error bar that takes the variation in both sets of measurements into account.</p><pre><code><code>Summary
  'sleep 0.1' ran
    2.01 &#177; 0.02 times faster than 'sleep 0.2'
    3.02 &#177; 0.03 times faster than 'sleep 0.3'</code></code></pre><p>Add <code>--reference CMD</code> for when the reader wants everything compared against a particular command rather than the fastest one, which is what you want when you are asking &#8220;how much did my change cost?&#8221; rather than &#8220;which of these is quickest?&#8221;. Add <code>-n</code> / <code>--command-name NAME</code> so that a long command line can be given a short, readable name in the output, once for each command being benchmarked.</p><p><strong>Testing:</strong></p><pre><code><code>cchyperfine 'sleep 0.1' 'sleep 0.2' 'sleep 0.3'</code></code></pre><p>The summary should report the second command as about 2 times slower than the first and the third as about 3 times slower, with small error bars. Then change the reference:</p><pre><code><code>cchyperfine --reference 'sleep 0.2' 'sleep 0.1' 'sleep 0.3'</code></code></pre><p>Everything should now be compared against <code>sleep 0.2</code>, so <code>sleep 0.1</code> is about 2 times faster and <code>sleep 0.3</code> about 1.5 times slower. Finally, check the names:</p><pre><code><code>cchyperfine -n fast 'sleep 0.1' -n slow 'sleep 0.2'</code></code></pre><p>The output should say <code>fast</code> and <code>slow</code> rather than repeating the commands.</p><h3>Step 8</h3><p>In this step your goal is to run the same benchmark across a range of parameter values.</p><p>Very often the question is not &#8220;which command is faster&#8221; but &#8220;how does this command behave as I change one thing about it&#8221;. Add two options for that, both of which replace <code>{VAR}</code> in the command with the current value:</p><ul><li><p><code>P</code> / <code>-parameter-scan VAR MIN MAX</code> runs a benchmark for each value in the range, with <code>D</code> / <code>-parameter-step-size DELTA</code> controlling the size of the steps.</p></li><li><p><code>L</code> / <code>-parameter-list VAR VALUES</code> runs a benchmark for each value in a comma-separated list. Given more than once, it should benchmark every possible combination of the values.</p></li></ul><p><strong>Testing:</strong></p><pre><code><code>cchyperfine -P delay 0.1 0.3 -D 0.1 'sleep {delay}'</code></code></pre><p>This should run three benchmarks, taking about 100 ms, 200 ms and 300 ms, and the summary should show them getting steadily slower. Then:</p><pre><code><code>cchyperfine -L delay 0.1,0.2,0.3 'sleep {delay}'</code></code></pre><p>That should produce the same three benchmarks. Now try two parameters at once:</p><pre><code><code>cchyperfine -L outer 0.1,0.2 -L inner 0.1,0.2 'sleep {outer}; sleep {inner}'</code></code></pre><p>You should get four benchmarks, one for each combination.</p><h3>Step 9</h3><p>In this step your goal is to export the results for further analysis.</p><p>Printing to the terminal is fine for a quick comparison, but benchmark results are much more useful when you can plot them, put them in a report, or track them over time in a build pipeline. Add <code>--export-json FILE</code>, which should record for each command its mean, standard deviation, median, user time, system time, minimum, maximum, the individual timings of every single run, the exit codes, and the values of any parameters used.</p><p>Then add <code>--export-csv FILE</code>, <code>--export-markdown FILE</code>, <code>--export-asciidoc FILE</code> and <code>--export-orgmode FILE</code>, which need only the summary statistics rather than the individual timings.</p><p>If you match the JSON that the real <code>hyperfine</code> produces, the plotting scripts that ship with it will work on your output too, which is a nice way to check you got the format right.</p><p><strong>Testing:</strong></p><pre><code><code>cchyperfine --runs 10 --export-json results.json 'sleep 0.1' 'sleep 0.2'
cat results.json</code></code></pre><p>The file should contain two entries, each with a <code>times</code> array holding exactly 10 measurements, and the mean should match what was printed on screen. Then:</p><pre><code><code>cchyperfine --runs 10 --export-markdown results.md 'sleep 0.1' 'sleep 0.2'
cat results.md</code></code></pre><p>That should give you a table you could paste straight into a pull request.</p><h3>Step 10</h3><p>In this step your goal is to control the input and output of the benchmarked commands.</p><p>Up to now the output of whatever you benchmark has been print on your terminal. By default it should be thrown away instead. Add <code>--output</code> taking <code>null</code> (the default), <code>pipe</code>, <code>inherit</code>, or a filename, and <code>--show-output</code> as a shorthand for <code>--output=inherit</code>. The <code>pipe</code> option matters because some programs, <code>grep</code> among them, notice when their output is going to <code>/dev/null</code> and take shortcuts, which would make your benchmark measure the wrong thing.</p><p>Add <code>--input</code> taking <code>null</code> (the default) or a filename, for benchmarking commands that read from standard input.</p><p>Set an environment variable <code>$HYPERFINE_ITERATION</code> for each run, holding the number of the current run, so that the benchmarked command can tell its runs apart. Finally add <code>--sort</code> taking <code>auto</code>, <code>command</code> or <code>mean-time</code> to order the results, and <code>--style</code> taking <code>auto</code>, <code>basic</code>, <code>full</code>, <code>nocolor</code>, <code>color</code> or <code>none</code> to control how much decoration the output has.</p><p><strong>Testing:</strong></p><pre><code><code>cchyperfine --runs 3 'echo hello'
cchyperfine --runs 3 --show-output 'echo hello'
</code></code></pre><p>The first should print nothing from <code>echo</code>, the second should print <code>hello</code> three times. Then check the input redirection:</p><pre><code><code>seq 1 100000 &gt; numbers.txt
cchyperfine --input numbers.txt 'wc -l'
</code></code></pre><p>Check the iteration variable:</p><pre><code><code>rm -f iterations.txt
cchyperfine --runs 3 'echo $HYPERFINE_ITERATION &gt;&gt; iterations.txt'
cat iterations.txt
</code></code></pre><p>You should see three different numbers. Finally:</p><pre><code><code>cchyperfine --sort mean-time --style basic 'sleep 0.3' 'sleep 0.1' 'sleep 0.2'
</code></code></pre><p>The results should be listed fastest first, with no colour or progress bar in the output.</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 <code>-time-unit</code> support for nanoseconds, and check your formatting still reads well at that scale.</p></li><li><p>Report peak memory usage alongside the timings, which is often the number that actually matters.</p></li><li><p>Warn when the machine looks like a bad place to benchmark, for example when the load average is high, the battery is discharging, or CPU frequency scaling is enabled.</p></li><li><p>Add an option to pin the benchmarked process to a particular CPU core, and see how much it reduces the variation.</p></li><li><p>Produce an HTML report with interactive charts from a set of exported JSON files.</p></li><li><p>Use it in a build pipeline to fail a change that makes a command measurably slower than the previous run, which means solving the harder problem of comparing results measured on different machines at different times.</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #135 - Voice Dictation App]]></title><description><![CDATA[This challenge is to build your own voice dictation app.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-135-voice-dictation</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-135-voice-dictation</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 05 Sep 2026 08:01:16 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd172d035-d170-4b2a-a8b0-b6e0c55e5ccb_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 #35 - Voice Dictation App</h2><p>This challenge is to build your own private voice dictation app. It&#8217;s a simple version of something like SuperWhisper or Whispr Flow.</p><p>Most dictation tools send your voice off to a server in the cloud. That works, but it means you need an internet connection, and your voice is leaving your machine. That comes with a cost, for the service, the latency and privacy. So, in this challenge you&#8217;ll build the whole pipeline yourself and keep it entirely on your own computer: capturing audio, turning it into text with a local speech model, tidying that text up, and then typing it directly into the app that currently has focus. All without a single network request.</p><p>Along the way you&#8217;ll get hands on with some genuinely tricky systems programming: catching a keypress that happens outside your own app, running a machine learning model, and injecting text into a program you don&#8217;t control and didn&#8217;t write.</p><h2>The Challenge - Building Your Own Voice Dictation App</h2><p>You&#8217;re going to build a background app that lets you press and hold a hotkey, speak, and release the hotkey to have your words appear as text wherever your cursor is, be that an email, a chat window, a code editor or a document.</p><p>Over the course of this challenge you&#8217;ll add local speech recognition, an AI cleanup pass that removes your &#8220;ums&#8221; and fixes your grammar, spoken formatting commands, a custom dictionary, and a history of everything you&#8217;ve dictated.</p><p>The speech recognition and text cleanup logic is much the same wherever you build it, but capturing a global hotkey and inserting text into another application&#8217;s focused field are done through operating system specific APIs.</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 the operating system you&#8217;ll target, macOS, Windows or Linux, and a programming language with good support for that OS&#8217;s accessibility or automation APIs (the APIs that let a program capture a global hotkey and type into whatever window has focus).</p><p>Spend a little time reading up on what your platform offers here: on macOS that&#8217;s the Accessibility API and <code>CGEventTap</code>, on Windows it&#8217;s <code>SetWindowsHookEx</code> and UI Automation, on Linux it&#8217;s typically AT-SPI along with a tool like <code>wtype</code> or <code>xdotool</code> .</p><p>You&#8217;ll also need a way to run speech-to-text locally. Have a look at <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fggerganov%2Fwhisper.cpp">whisper.cpp</a>, <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FSYSTRAN%2Ffaster-whisper">faster-whisper</a>, or <a href="iframe.php?url=https%3A%2F%2Falphacephei.com%2Fvosk%2F">Vosk</a>, and check which of them your chosen language and platform can use comfortably. You don&#8217;t need to install everything now, just confirm you have a path from &#8220;microphone audio&#8221; to &#8220;text&#8221; that runs entirely offline.</p><p>Finally, open a plain text editor and a couple of other everyday apps (a browser address bar, a notes app, a terminal) that you can use as targets to type into while you test each step.</p><h3>Step 1</h3><p>In this step your goal is to create a simple app and register a global, system-wide hotkey and use it to start and stop an audio recording.</p><p>Your app should let the user choose a hotkey from at least two options (for example the Fn key or the right Cmd/Ctrl key). While the app is running in the background, pressing and holding that hotkey should start recording from the system microphone, and releasing it should stop the recording immediately, regardless of which application is currently focused.</p><p><strong>Testing:</strong> With your app running in the background, switch focus to any other application (a browser, a text editor). Press and hold your configured hotkey, say a few words, then release it. Add some logging so you can confirm in your console or logs that a recording started on key down and stopped on key up, with a duration roughly matching how long you held the key.</p><h3>Step 2</h3><p>In this step your goal is to show a visual indicator confirming that recording is in progress.</p><p>While the hotkey is held down, display something the user can see, a small floating overlay near the cursor, an animation in the menu bar or system tray, whatever suits your platform, that clearly shows recording is active. It should disappear the moment the hotkey is released.</p><p><strong>Testing:</strong> Hold the hotkey and confirm the indicator appears within a fraction of a second. Release it and confirm the indicator disappears immediately. Try triggering it while different applications are focused to make sure it always appears regardless of what&#8217;s underneath it.</p><h3>Step 3</h3><p>In this step your goal is to transcribe the recorded audio to text entirely on your local machine.</p><p>Wire up the local speech-to-text engine you chose in Step Zero so that, when the hotkey is released, the recorded audio is transcribed and the resulting text is printed or logged. Make sure no audio or text ever leaves the machine,.</p><p><strong>Testing:</strong> Turn off your Wi-Fi or unplug your network cable, then hold the hotkey and say a short, clear sentence like &#8220;The quick brown fox jumps over the lazy dog.&#8221; Release the hotkey and confirm the transcribed text appears in your logs and roughly matches what you said, with the network still disconnected.</p><h3>Step 4</h3><p>In this step your goal is to let the user choose between at least two speech-to-text models with different size and accuracy trade-offs.</p><p>Offer at least two models, for example a small, fast model and a larger, more accurate one, in a settings screen. The first time a model is selected, download its weights to disk and show a progress indicator while that happens. Once downloaded, switching models should be instant.</p><p><strong>Testing:</strong> Select the smaller model, dictate a sentence, and note the response time. Switch to the larger model (triggering a download if it&#8217;s the first time you&#8217;ve selected it, and confirm the progress indicator moves) and dictate the same sentence again. Compare the accuracy and speed of the two.</p><h3>Step 5</h3><p>In this step your goal is to show a live, continuously updating preview of what&#8217;s being recognised while the hotkey is still held down.</p><p>Rather than waiting until the hotkey is released to show any text, update your visual indicator from Step 2 with the transcription so far, refreshing it as more speech is recognised.</p><p><strong>Testing:</strong> Hold the hotkey and speak a longer sentence slowly. Watch the preview and confirm it updates progressively as you speak, rather than appearing all at once only after you release the key.</p><h3>Step 6</h3><p>In this step your goal is to run a cleanup pass over the raw transcript that removes filler words and fixes grammar and punctuation.</p><p>After the hotkey is released and you have a raw transcript, run it through a local cleanup step before it&#8217;s used anywhere else. This should strip out filler words like &#8220;um&#8221; and &#8220;uh&#8221;, correct obvious grammar mistakes, and add sensible sentence casing and punctuation. This all needs to happen locally too, no calling out to a hosted AI cleanup API.</p><p><strong>Testing:</strong> Dictate a sentence with deliberate filler words, something like &#8220;so, um, I think we should, uh, meet on Tuesday&#8221;. Confirm the cleaned up output reads naturally, for example &#8220;I think we should meet on Tuesday.&#8221;, with the filler words gone and proper punctuation added. Repeat the network monitoring check from Step 3 to confirm the cleanup pass makes no outbound calls either.</p><h3>Step 7</h3><p>In this step your goal is to recognise spoken formatting commands and turn them into real line and paragraph breaks.</p><p>When the user says &#8220;new line&#8221; or &#8220;new paragraph&#8221; partway through dictating, your cleanup pass should detect the phrase and insert an actual line break or paragraph break at that point in the output, rather than transcribing the words literally.</p><p><strong>Testing:</strong> Dictate &#8220;Dear Sam, new paragraph, Thanks for your help this week, new line, Best, Alex&#8221; in one go. Confirm the output has an actual paragraph break after &#8220;Dear Sam,&#8221; and a line break before &#8220;Best, Alex&#8221;, and that the words &#8220;new paragraph&#8221; and &#8220;new line&#8221; do not appear in the output.</p><h3>Step 8</h3><p>In this step your goal is to detect dictated lists and format them as bulleted or numbered lists.</p><p>When someone dictates something that sounds like a list, for example &#8220;first buy milk, second walk the dog, third call mum&#8221;, your cleanup pass should recognise the pattern and produce an actual bulleted or numbered list in the output.</p><p><strong>Testing:</strong> Dictate a three item list using ordinal words (&#8221;first&#8221;, &#8220;second&#8221;, &#8220;third&#8221;). Confirm the output is formatted as a numbered or bulleted list with three separate items rather than one run-on sentence.</p><h3>Step 9</h3><p>In this step your goal is to insert the cleaned up text directly into whichever application and text field currently has focus.</p><p>Using your platform&#8217;s accessibility or automation APIs, insert the final text at the current cursor position in the focused field, in the focused application, the moment it&#8217;s ready. This should work across different kinds of apps: a browser text box, a native text editor, a chat app.</p><p><strong>Testing:</strong> Open a plain text editor, click into it so it has focus, and dictate a sentence. Confirm the text appears at your cursor position in the editor. Repeat with a browser address bar or search box, and with a chat or notes app, confirming the text lands correctly in each.</p><h3>Step 10</h3><p>In this step your goal is to add a clipboard fallback for when direct text insertion isn&#8217;t possible.</p><p>Some fields or apps won&#8217;t support direct insertion, for example because of missing permissions or an unsupported field type. Detect when insertion fails, copy the cleaned up text to the clipboard instead, and show the user a clear notification telling them what happened and that the text is ready to paste.</p><p><strong>Testing:</strong> Find or simulate a case where direct insertion fails (for example, by revoking the accessibility permission your app needs, or targeting a field type you know your implementation doesn&#8217;t support). Dictate something and confirm you see a fallback notification and that the text is sitting on the clipboard ready to paste.</p><h3>Step 11</h3><p>In this step your goal is to build a first launch setup flow that requests the permissions your app needs before it can be used.</p><p>On first launch, walk the user through granting microphone access and input injection/accessibility access, one at a time, checking that each has actually been granted before letting them move on to the next step.</p><p><strong>Testing:</strong> Reset your app&#8217;s permissions and relaunch it. Confirm you&#8217;re taken through the setup flow, that you can&#8217;t skip past a step whose permission hasn&#8217;t been granted yet, and that once both are granted, dictation works straight away.</p><h3>Step 12</h3><p>In this step your goal is to detect when a previously granted permission has been revoked and prompt the user to fix it.</p><p>If the user (or their OS) revokes microphone or accessibility access after setup, your app should notice this before attempting to dictate and prompt the user to re-grant the permission, rather than failing silently or crashing.</p><p><strong>Testing:</strong> With your app set up and working, manually revoke the accessibility permission in your OS settings, then try to dictate. Confirm your app detects the missing permission and shows a clear prompt to re-grant it, rather than silently failing.</p><h3>Step 13</h3><p>In this step your goal is to add a dictionary screen where the user can teach the app words it gets wrong.</p><p>Add a settings screen listing custom words and corrections, for example proper nouns, product names, or jargon, that the user can add to or edit. Apply these entries during the cleanup pass in Step 6, so a word the speech model consistently mishears gets corrected before the text is inserted.</p><p><strong>Testing:</strong> Dictate a word you know your speech model struggles with (an unusual name works well) and confirm it comes out wrong. Add a dictionary entry correcting it, dictate the same word again, and confirm it now comes out correctly.</p><h3>Step 14</h3><p>In this step your goal is to keep a local, searchable history of everything you&#8217;ve dictated.</p><p>Every time text is dictated, store the raw transcript, the cleaned up text, and a timestamp. Add a screen where the user can search and browse this history.</p><p><strong>Testing:</strong> Dictate several different sentences over a few minutes. Open the history screen and confirm all of them appear with the correct timestamps and both the raw and cleaned up versions. Search for a word that only appears in one of them and confirm only that entry is returned.</p><h3>Step 15</h3><p>In this step your goal is to show aggregate dictation statistics drawn from the history.</p><p>Using the history you built in Step 14, calculate and display statistics such as the total number of words dictated and the total number of dictation sessions.</p><p><strong>Testing:</strong> Note your current statistics, dictate a sentence with a known word count, and confirm the totals update by the expected amount. Restart your app and confirm the statistics persist rather than resetting.</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>Let the user define custom voice commands beyond &#8220;new line&#8221; and &#8220;new paragraph&#8221;, for example &#8220;select all&#8221; or &#8220;delete that&#8221;.</p></li><li><p>Add per application profiles, so the cleanup style (for example more formal for email, more relaxed for chat) changes depending on which app is focused.</p></li><li><p>Build an always on &#8220;ambient&#8221; mode as an alternative to push to talk, with a wake phrase to start and stop listening.</p></li><li><p>Add undo support, so a single keypress reverts the last inserted dictation.</p></li><li><p>Benchmark your different supported speech models for latency and accuracy against a fixed set of test recordings.</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #134 - Agentic Engineering Graph]]></title><description><![CDATA[This challenge is to build your own agentic engineering graph.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-134-agentic-engineering</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-134-agentic-engineering</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 29 Aug 2026 08:01:12 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff808d93b-f4d1-40a7-bd95-11d1d89550df_1280x720.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em><span>Hi, this is John with this week&#8217;s Coding Challenge.</span></em></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></p><p><em><span>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</span></em></p><h2><strong><span>Coding Challenge #134 - Agentic Engineering Graph</span></strong></h2><p><span>This challenge is to build your own agentic engineering graph. So what is an agentic engineering graph? It&#8217;s a small orchestration system that runs an autonomous coding loop: plan, write code, test, review, and retry, with state, that allows it to survive a crash and a control point where a human can step in and approve or reject a plan.</span></p><p><span>If you follow the agentic engineering discussion on social media and the latest tech news you&#8217;ll have seen that agent graphs have gone from a niche pattern to something every engineering team seems to be experimenting with this year.</span></p><p><span>The reason is simple: a single call to an agentic coding CLI is impressive but unreliable on its own, ask it to fix a bug and it might nail it the first time, or wander off down the wrong path with nothing to stop it. Or you have to sit there reading all the output and stop it manually. Yuk!</span></p><p><span>The fix that teams have converged on is the same one distributed systems adopted years ago: break the work into discrete steps, persist state between them, and let an orchestrator decide what runs next based on what actually happened rather than what was supposed to happen.</span></p><p><span>That&#8217;s what frameworks like LangGraph do, and it&#8217;s also what&#8217;s often inside CI pipelines and internal tools wherever teams are wiring agentic CLIs into their workflows.</span></p><p><span>In this challenge you&#8217;ll build the same kind of system yourself, without reaching for a graph framework. Your &#8220;graph&#8221; will be a small dispatcher and a set of scripts, your &#8220;nodes&#8221; will be calls to an agentic coding CLI running non-interactively, and your &#8220;state&#8221; will be plain files on disk that you could open and read at any point.</span></p><p><span>Along the way you&#8217;ll add a deterministic quality gate using</span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4gdItQW"><span> Sonar</span></a><span>&#8216;s static analysis CLI, so your agent has to satisfy a set of rules it cannot argue its way around, and then you&#8217;ll swap that gate for an AI-augmented review node using the </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F3ULd29w"><span>SonarQube MCP Server</span></a><span>, and watch the rest of your graph carry on working without caring which kind of node it&#8217;s talking to.</span></p><p><span>Ultimately that&#8217;ll look like this:</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vk05%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vk05!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png 424w, https://substackcdn.com/image/fetch/$s_!vk05!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png 848w, https://substackcdn.com/image/fetch/$s_!vk05!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png 1272w, https://substackcdn.com/image/fetch/$s_!vk05!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21vk05%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png" width="1456" height="484" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:484,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vk05!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png 424w, https://substackcdn.com/image/fetch/$s_!vk05!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png 848w, https://substackcdn.com/image/fetch/$s_!vk05!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png 1272w, https://substackcdn.com/image/fetch/$s_!vk05!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F87c5c304-c1b8-4e0c-a3d8-c50bccf0ffe2_1717x571.png 1456w" sizes="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><span>With the inner loop being this:</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21o98f%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!o98f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png 424w, https://substackcdn.com/image/fetch/$s_!o98f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png 848w, https://substackcdn.com/image/fetch/$s_!o98f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png 1272w, https://substackcdn.com/image/fetch/$s_!o98f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21o98f%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png" width="1117" height="374" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:374,&quot;width&quot;:1117,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!o98f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png 424w, https://substackcdn.com/image/fetch/$s_!o98f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png 848w, https://substackcdn.com/image/fetch/$s_!o98f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png 1272w, https://substackcdn.com/image/fetch/$s_!o98f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5f49fce9-2f7b-4416-86cf-b11d59551b66_1117x374.png 1456w" sizes="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><span>This challenge was created in collaboration with </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4gdItQW"><span>Sonar</span></a><span>.</span></p><h2><strong><span>The Challenge - Building Your Own Agentic Engineering Graph</span></strong></h2><p><span>You&#8217;re going to build a small system that takes a coding task, plans an approach, writes code, tests it, reviews it, and retries automatically when something fails, all the way through to a working result. It will survive being killed and restarted, it will let a user approve the plan before any code gets written, and then it will run one or more attempts at a task in parallel and pick the best one.</span></p><p><span>This challenge doesn&#8217;t require expertise in any particular programming language, most of what you&#8217;re building is small scripts and a dispatcher.</span></p><p><span>Pick whatever scripting or general-purpose language you&#8217;re most comfortable with for the dispatcher and the small helper scripts. The interesting parts of this challenge are architectural, so the programming language is genuinely up to you. As the individual programs are quite small it&#8217;ll be a fun project to learn a new language or (or go crazy and use several to compare).</span></p><h3><strong><span>Step Zero</span></strong></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>You&#8217;ll need three things.</span></p><p><span>First, an agentic coding CLI that can run non-interactively from a single command, let you scope which tools it&#8217;s allowed to use for a given invocation, and connect to MCP servers. Claude Code and Codex CLI both qualify. If you&#8217;re planning to use something else, check it supports all three of those before you start, because you&#8217;ll rely on each of them at different points in the challenge.</span></p><p><span>Second, </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4gdItQW"><span>SonarQube</span></a><span> running SonarQube Cloud (or the self-managed Community Build) gets you a working setup, and you&#8217;ll also want the sonar-scanner CLI installed and able to talk to it. You won&#8217;t need it until partway through the challenge, but it&#8217;s worth confirming it works now rather than discovering a setup problem later.</span></p><p><span>Third, a small coding task you can use as your test fixture throughout the challenge, ideally something with its own test suite. Pick something you can deliberately break, a function you can introduce a bug into, a rule you can violate on purpose, so you have a reliable way to drive the loop through both its happy path and its failure path at every step. I used one of the Redis clones I&#8217;ve built with students in my coding with AI classes.</span></p><h3><strong><span>Step 1</span></strong></h3><p><span>In this step your goal is to build the fragile version of an agentic workflow. This is the one you&#8217;ll spend the rest of the challenge replacing. This is the MVP or prototype.</span></p><p><span>You&#8217;re going to write a single script that invokes your agentic coding CLI non-interactively exactly once, with a prompt describing your fixture task, and writes whatever comes back straight to a file. No branching, no retry, and nothing recorded anywhere about what happened.</span></p><p><span>Once it runs, write down everything it doesn&#8217;t do. There&#8217;s no retry if the invocation fails, there&#8217;s no record anywhere that it ran, there&#8217;s no way to see what happened partway through, and if it produces something wrong there&#8217;s no way to try again short of running the whole thing over.</span></p><p><span>Keep that list. Every remaining step in this challenge addresses one of the items on it.</span></p><p><strong><span>Testing:</span></strong><span> Run your script against your fixture task and confirm you get a plausible attempt at the task written to a file. Then break it on purpose, point it at an invalid credential or an unreachable endpoint, and run it again. Confirm you get nothing useful back and no trace of what was attempted.</span></p><h3><strong><span>Step 2</span></strong></h3><p><span>In this step your goal is to turn that single script into the first version of your graph.</span></p><p><span>Define what a node is in your system: an executable that reads a state file from a run directory, invokes your agentic CLI with a narrowly scoped prompt and a limited set of tool permissions, and writes an updated state file back. Then write a dispatcher, a script that, given a run directory and a starting node name, runs that node, reads which node to run next from the state it wrote, and calls that node in turn.</span></p><p><span>Re-implement Step 1 as three separate nodes, wired together by your dispatcher in a straight line: a plan node that produces an approach, a code node that writes an implementation based on that plan, and a write node that commits the result to your fixture project.</span></p><p><strong><span>Testing:</span></strong><span> Point your dispatcher at your fixture task and run it from plan through to write. Confirm all three state files were created, in order, and that the final result matches what your Step 1 script produced. Open one of the intermediate state files and confirm it&#8217;s readable on its own, with no need to run anything to inspect it.</span></p><h3><strong><span>Step 3</span></strong></h3><p><span>In this step your goal is to add a test node and a self-correcting fix loop.</span></p><p><span>Add a test node that runs your fixture task&#8217;s test suite and writes the pass or fail result, along with the output, into the state file. Have your dispatcher branch on that result: on to a terminal END node if the tests pass, or to a fix node if they don&#8217;t.</span></p><p><span>Build fix as a node like any other: it reads the failure output and the previous code from state, asks your agentic CLI to repair it, writes the result back, and loops to test again. Add an iteration count to your state, and once it passes a limit you choose, route to a give_up terminal node instead of looping forever.</span></p><p><strong><span>Testing:</span></strong><span> Deliberately break your fixture task&#8217;s code so the tests fail, then run the graph from code onward. Confirm test fails once, fix runs, test runs again and passes, and the dispatcher reaches END. Now introduce a failure your fixture genuinely can&#8217;t be talked out of, run it again, and confirm the loop stops at give_up once it hits your limit rather than running indefinitely.</span></p><h3><strong><span>Step 4</span></strong></h3><p><span>In this step your goal is to make a run survive a process dying.</span></p><p><span>After every node runs, write an entry to a run manifest recording which node ran, when, and whether it succeeded. When the dispatcher starts, have it check for an existing manifest for that run and, if one exists, resume from the node after the last one that completed successfully rather than starting from plan again.</span></p><p><span>Add a small command that lists past runs by reading their manifests, showing each run&#8217;s current node and status at a glance.</span></p><p><strong><span>Testing:</span></strong><span> Start a run, and once the manifest shows plan complete but before code has finished, kill the dispatcher process. Restart it against the same run directory and confirm it picks up at code, not from the beginning. List your runs and confirm the status shown matches what actually happened.</span></p><h3><strong><span>Step 5</span></strong></h3><p><span>In this step your goal is to let a human review and approve a plan before any code gets written.</span></p><p><span>Add an interrupt point right after the plan node. Rather than moving straight on to code, the dispatcher should stop and wait until a human has reviewed the plan file, optionally edited it, and given a clear signal that it&#8217;s ready to proceed.</span></p><p><span>You shouldn&#8217;t need any special machinery to keep a run paused for as long as you like. Because your state already lives in files rather than in memory, a paused run is just a run directory sitting there until someone comes back to it and runs the tool again.</span></p><p><strong><span>Testing:</span></strong><span> Start a run and confirm it stops after plan and waits. Edit the plan file, give your readiness signal, and confirm code then runs using your edited version rather than the original. This time, stop the dispatcher entirely while it&#8217;s paused, walk away, and come back later to confirm the run is exactly where you left it and resumes correctly.</span></p><h3><strong><span>Step 6</span></strong></h3><p><span>In this step your goal is to add a deterministic quality gate using </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4gdItQW"><span>SonarQube</span></a><span>.</span></p><p><span>Add a review node that runs sonar-scanner against the code your graph has produced and parses the resulting quality gate status into your state. Ensure you set sonar.qualitygate.wait=true and sonar.qualitygate.timeout for your environment.</span></p><p><span>Extend your dispatcher&#8217;s branching so that a failed gate, whether from a new bug, a vulnerability, or coverage dropping below your threshold, routes to fix with the Sonar findings attached to state, exactly the way a failing test does.</span></p><p><strong><span>Testing:</span></strong><span> Introduce an issue into your fixture that </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4gdItQW"><span>SonarQube</span></a><span> reliably catches, something like a hardcoded secret or a deeply nested conditional, and confirm review fails the gate and routes to fix, with the loop eventually producing a version that passes. Then run the same input through review twice in a row and confirm you get identical findings both times. That repeatability is the whole point of this step.</span></p><p><span>This is what we want to see in SonarQube, passing quality gates:</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21J2UM%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!J2UM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png 424w, https://substackcdn.com/image/fetch/$s_!J2UM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png 848w, https://substackcdn.com/image/fetch/$s_!J2UM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png 1272w, https://substackcdn.com/image/fetch/$s_!J2UM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21J2UM%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png" width="1139" height="561" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:561,&quot;width&quot;:1139,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!J2UM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png 424w, https://substackcdn.com/image/fetch/$s_!J2UM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png 848w, https://substackcdn.com/image/fetch/$s_!J2UM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png 1272w, https://substackcdn.com/image/fetch/$s_!J2UM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fffa940bd-ebba-4699-84f0-dda21db34676_1139x561.png 1456w" sizes="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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><span>Useful reference: </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F3UbDqJA"><span>SonarScanner docs</span></a><span>.</span></p><h3><strong><span>Step 7</span></strong></h3><p><span>In this step your goal is to extend that review node with one backed by an AI agent, using the SonarQube MCP Server.</span></p><p><span>Instead of parsing sonar-scanner&#8217;s output yourself, have review invoke your agentic CLI with the SonarQube MCP server attached and a scoped prompt instructing it to query issues, security hotspots, and the quality gate through tool calls. Keep the state it writes identical in shape to what the CLI-based version wrote, so your dispatcher&#8217;s branching logic doesn&#8217;t need to change at all.</span></p><p><strong><span>Testing:</span></strong><span> Run the graph against the same fixture issue you used in Step 6, through the MCP-backed review node this time, and confirm it reaches the same pass or fail decision. Inspect the node&#8217;s own record of what it did and confirm it queried SonarQube through tool calls rather than guessing at an answer.</span></p><p><span>Useful reference: </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F3ULd29w"><span>SonarQube MCP Server</span></a><span>.</span></p><h3><strong><span>Step 8</span></strong></h3><p><span>In this step your goal is to run several attempts at a task in parallel and pick the best one.</span></p><p><span>Add a node that starts several candidate attempts at the same task at once, each working in its own isolated copy of the project, each running its own test and review nodes independently of the others. Add a join node that looks at the candidates once they&#8217;ve all finished, discards any that failed the tests or the quality gate, and picks a winner from what&#8217;s left using a rule you define, such as the fewest Sonar issues or the fastest test run.</span></p><p><span>You should make the number of candidates configurable.</span></p><p><strong><span>Testing:</span></strong><span> Run the fan-out with at least three candidates against your fixture task and confirm all three run without interfering with each other&#8217;s files or state. Deliberately make one candidate worse than the others, either by constraining it or sabotaging its starting point, and confirm your join node correctly excludes it and picks a winner from the remaining candidates according to your chosen rule.</span></p><h3><strong><span>Step 9</span></strong></h3><p><span>In this step your goal is to make a run&#8217;s history visible.</span></p><p><span>Log the node name, timestamp, and duration for every transition your dispatcher makes, either through your agentic CLI&#8217;s own lifecycle hooks if it has them, or by having the dispatcher log directly. Then build a simple way to render a run&#8217;s log as a timeline, so you can see the path a run actually took from start to finish.</span></p><p><strong><span>Testing:</span></strong><span> Run the full graph end to end on a task that needs at least one pass through the fix loop, and check the resulting timeline. Confirm every node that executed appears in the correct order with a plausible duration, and confirm a node like fix that ran more than once shows up more than once, in the right places.</span></p><h3><strong><span>Going Further</span></strong></h3><p><span>Once you have the core graph working, here are some ideas to take it further:</span></p><ul><li><p><span>Point your finished graph at a handful of real Coding Challenges as target tasks and see how far it gets unattended.</span></p></li><li><p><span>Swap which agentic CLI runs different nodes in the same graph, and confirm nothing else needs to change, since the only real contract is the files a node reads and writes.</span></p></li><li><p><span>Add a ship node that pushes a branch and opens a pull request once a candidate passes everything, and see what changes once your output is a pull request rather than a local file.</span></p></li><li><p><span>Track token usage and cost per node, so you can see exactly which part of the loop is the expensive one.</span></p></li><li><p><span>Add Sonar&#8217;s Gitar AI Code Review alongside SonarQube and have the review lead to agentic fix.</span></p></li><li><p><span>Turn your timeline from Step 9 into an actual diagram of the graph, showing which nodes ran, which were skipped, and where the loops happened.</span></p></li></ul><h3><strong><span>Share Your Solutions!</span></strong></h3><p><span>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</span><a href="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social"><span> Bluesky</span></a><span> or</span><a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F"><span> LinkedIn</span></a><span> or just post about it there and tag me. Alternately please add a link to it in the</span><a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions"><span> Coding Challenges Shared Solutions</span></a><span> Github repo</span></p><h3><strong><span>Request for Feedback</span></strong></h3><p><span>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.</span></p><p><span>You can reach me on</span><a href="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social"><span> Bluesky</span></a><span>,</span><a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F"><span> LinkedIn</span></a><span> or through</span><a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F"><span> SubStack</span></a></p><p><span>Thanks and happy coding!</span></p><p><span>John</span></p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #133 - Particle Playground]]></title><description><![CDATA[This challenge is to build your own particle playground.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-133-particle-playground</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-133-particle-playground</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 22 Aug 2026 08:01:30 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa15ce07a-b03f-4b05-acd3-1990e9d2108b_1536x1024.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 #133 - Particle Playground</h2><p>This challenge is to build your own particle playground: an interactive canvas where you paint materials and watch them behave. Sand will pile up. Water will flow and finds its level. Fire spreads and burns out. Fireworks launch, burst, and rain embers back down onto whatever you painted underneath.</p><p>Particle systems are the trick behind most of the physical-looking effects you see in games and creative coding: explosions, smoke, rain, magic spells, crumbling terrain. That&#8217;s where I first learned about them, creating computer graphics for simple games and firework simulators!</p><p>In this &#8220;particle system&#8221; we&#8217;re going to cover two different techniques. One is a swarm of free-flying objects pushed around by forces like gravity and wind. The other is a grid of cells updated by simple local rules, which is what lets thousands of grains of sand or drops of water run in real time without doing full physics on each one. In this challenge you&#8217;ll build both, then join them together: a firework whose burning embers can fall onto your material grid and set fire to anything flammable underneath.</p><h1><strong>Win $$$ - Just 48 hours left! Join The Ready, Spec, Ship Hackathon Hackathon I&#8217;m Running.</strong></h1><p>There&#8217;s just under 48 hours left in the <a href="iframe.php?url=https%3A%2F%2Fcodingagents.fyi%2Fhackathon%2Fkiro%2F">Ready, Spec, Ship Hackathon</a>!</p><p>There&#8217;s a $9,600 prize pool, plus 2000 FREE Kiro credits for every verified entry. Submissions close on midnight on the 23rd August, with the winners announced on 6 September.</p><p>Find out more and take part:<a href="iframe.php?url=https%3A%2F%2Fcodingagents.fyi%2Fhackathon%2Fkiro%2F">https://codingagents.fyi/hackathon/kiro/</a></p><p>It&#8217;s a great chance to checkout the Kiro agentic coding environment!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%217Aff%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7Aff!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png 424w, https://substackcdn.com/image/fetch/$s_!7Aff!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png 848w, https://substackcdn.com/image/fetch/$s_!7Aff!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!7Aff!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%217Aff%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1767562,&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/212130268?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.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_!7Aff!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png 424w, https://substackcdn.com/image/fetch/$s_!7Aff!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png 848w, https://substackcdn.com/image/fetch/$s_!7Aff!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!7Aff!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3dae3475-51eb-4f1c-af32-06161317d3a8_1535x1024.png 1456w" sizes="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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>OK, on with the challenge!</p><h2>The Challenge - Building Your Own Particle Playground</h2><p>You&#8217;re going to build a 2D particle simulation with two engines working side by side. The first is a classic particle engine: particles with position, velocity, and acceleration, pushed around by gravity, wind, and drag. The second is a cellular automaton: a grid of cells, each holding a material, updated every tick by rules like &#8220;sand falls if the cell below is empty&#8221;. You&#8217;ll build up materials one at a time, add an interface for painting them onto the canvas, and finish with a fireworks display that reuses your particle engine and can set your grid alight.</p><p>You can build this in any programming language, so pick whatever you&#8217;re comfortable with for 2D graphics and mouse input.</p><p>You&#8217;ll be making something like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21WcSV%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WcSV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png 424w, https://substackcdn.com/image/fetch/$s_!WcSV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png 848w, https://substackcdn.com/image/fetch/$s_!WcSV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png 1272w, https://substackcdn.com/image/fetch/$s_!WcSV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png 1456w" sizes="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21WcSV%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png" width="1015" height="459" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:459,&quot;width&quot;:1015,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:13482,&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/212130268?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.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_!WcSV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png 424w, https://substackcdn.com/image/fetch/$s_!WcSV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png 848w, https://substackcdn.com/image/fetch/$s_!WcSV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png 1272w, https://substackcdn.com/image/fetch/$s_!WcSV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5bc8b4bf-7b28-43a0-86b9-31696692375a_1015x459.png 1456w" sizes="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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></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 platform that gives you pixel-level 2D rendering and mouse input.</p><p>Before you start coding, it&#8217;s worth reading up on the two techniques you&#8217;re going to combine:</p><ul><li><p><a href="iframe.php?url=https%3A%2F%2Fnatureofcode.com%2Fparticles%2F">The Nature of Code, Chapter 4: Particle Systems</a> covers the classic force-driven particle engine you&#8217;ll build in Steps 1 and 2.</p></li><li><p><a href="iframe.php?url=https%3A%2F%2Fmaxbittker.com%2Fmaking-sandspiel%2F">Making Sandspiel</a> and <a href="iframe.php?url=https%3A%2F%2Fjason.today%2Ffalling-sand">Falling Sand</a> explain the cellular automaton approach you&#8217;ll build in Steps 3 to 5. You can also have a play with <a href="iframe.php?url=https%3A%2F%2Fsandspiel.club%2F">Sandspiel</a> itself to get a feel for how the materials should behave.</p></li></ul><h3>Step 1</h3><p>In this step your goal is to build a basic particle emitter.</p><p>Set up an empty window or canvas with a render loop that clears the screen and redraws every frame. Get it running at a steady frame rate before moving on.</p><p>Give each particle a position, a velocity, and an acceleration. On every frame, integrate acceleration into velocity and velocity into position, so particles move smoothly over time. When the user clicks or taps on the canvas, spawn one or more particles at that point. Render each particle as a circle, and have it fade out as it approaches the end of its lifespan. Once a particle&#8217;s lifespan expires (or it becomes fully transparent), remove it so your particle list doesn&#8217;t grow forever.</p><p><strong>Testing:</strong> Click a few times in different spots on the canvas. You should see small circles appear at each click point, drift briefly, and fade to nothing over a second or two. Click rapidly in one spot and confirm old particles disappear rather than piling up forever, watch a particle counter to be sure they&#8217;re actually being removed.</p><h3>Step 2</h3><p>In this step your goal is to make your particles respond to forces.</p><p>Add a constant downward gravity force applied to every particle every frame, so they arc and fall instead of drifting in a straight line. Add an adjustable wind force, a constant horizontal acceleration you can tune, and a drag force proportional to each particle&#8217;s velocity, so particles slow down over time even with no gravity or wind acting on them. Finally, detect when a particle reaches the edges or floor of the canvas, and have it bounce, reflecting its velocity and losing some speed (damping) each time it bounces.</p><p><strong>Testing:</strong> Click near the top of the canvas and watch particles fall under gravity rather than drifting evenly. Turn wind on and confirm particles curve sideways as they fall. Increase drag and confirm particles slow down and settle rather than bouncing forever. Drop particles onto the floor and count how many bounces it takes before a particle&#8217;s bounce height becomes negligible, it should be a handful, not dozens.</p><h3>Step 3</h3><p>In this step your goal is to add a second simulation substrate: a grid-based falling sand simulation.</p><p>Create a 2D grid of fixed-size cells, each holding a material type (start with just <code>empty</code> and <code>sand</code>). Implement the classic falling sand rule for sand: on each tick, a sand cell falls one cell straight down if the cell below is empty; if straight down is blocked but a diagonal (down-left or down-right) is free, it falls diagonally instead. Update the whole grid in a way that guarantees no cell is processed more than once per tick, for example by writing updates into a second buffer and swapping buffers at the end of the tick, or by scanning in an order that never lets an already-moved cell be revisited.</p><p><strong>Testing:</strong> Programmatically fill a vertical line of sand cells near the top of an otherwise empty grid and run the simulation. You should see the sand fall and form a roughly triangular pile with a natural angle of repose, not a perfectly flat-topped column and not sand that clips through the floor.</p><h3>Step 4</h3><p>In this step your goal is to add liquids and a static solid to your material grid.</p><p>Add a <code>water</code> material that falls like sand but, when it can&#8217;t fall any further, spreads sideways into an empty neighbouring cell. Add an <code>oil</code> material that behaves like water but is less dense, when a falling oil cell would land on top of a water cell, it should displace upward through the water instead of settling below it, so oil ends up floating on water. Add a <code>stone</code> material that is immovable and blocks every other material from passing through or displacing it.</p><p><strong>Testing:</strong> Fill a small pool of water and confirm it spreads out to find a level surface rather than staying in a pile like sand. Drop oil into a container of water and confirm it rises to the top over a few ticks. Build a stone wall across the middle of the grid and pour sand or water onto it, confirm nothing passes through or moves a stone cell.</p><h3>Step 5</h3><p>In this step your goal is to add fire, smoke, and steam.</p><p>Add a <code>wood</code> material that, like stone, doesn&#8217;t move, but is flammable. Add a <code>fire</code> material that ignites any flammable material (wood or oil) in an adjacent cell, and burns itself out to an empty cell after a fixed lifetime. Add a <code>smoke</code> material that spawns above burning cells, drifts upward, and dissipates back to empty over a fixed lifetime. Finally, when a water cell is adjacent to fire, convert it to <code>steam</code>; a steam cell should drift like smoke and convert back to water once it&#8217;s gone a fixed amount of time without contacting more fire.</p><p><strong>Testing:</strong> Build a small wood structure and light one cell of it on fire. Confirm the fire spreads across connected wood cells, each burning out to empty after a few seconds, and that smoke rises from the burning area and fades away rather than accumulating forever. Pour water next to a fire and confirm it turns to steam, drifts upward, and eventually turns back into water once it&#8217;s away from the flames.</p><h3>Step 6</h3><p>In this step your goal is to make the simulation interactive.</p><p>Add a palette letting the user pick the current material to paint, at minimum sand, water, oil, stone, wood, and fire. Add an adjustable brush size, so pressing and dragging the mouse or pointer paints a filled circle of cells of that size in the selected material. Add a button to clear the entire grid back to empty, and a way to pause and resume the simulation without losing the current grid state.</p><p><strong>Testing:</strong> Select each material in turn and paint a patch of it onto the grid, confirm the right material appears and behaves correctly (sand falls, water spreads, fire spreads and burns out). Increase and decrease the brush size and confirm the painted area scales accordingly. Pause the simulation mid-fall, confirm nothing moves while paused, then resume and confirm it continues from where it left off. Clear the grid and confirm every cell returns to empty.</p><h3>Step 7</h3><p>In this step your goal is to build a fireworks display on top of your particle engine. Building fireworks with particles was where I first used them. It was a fun introduction to computer graphics.</p><p>When the user clicks a launch point, spawn a rocket using the particle engine and forces you built in Steps 1 and 2, and let the user control how high it goes before exploding, for example with a power slider, or by how far they drag before releasing the click. On explosion, spawn a burst of ember particles following a pattern the user can select. Implement at least four of the classic firework styles described in <a href="iframe.php?url=https%3A%2F%2Fmedium.com%2F%40cluster_official%2Fcreating-different-kinds-of-fireworks-particle-styles-8f851782c254">this overview of firework particle styles</a>:</p><ul><li><p><strong>Ring / sphere</strong>: embers spread out evenly in all directions.</p></li><li><p><strong>Willow</strong>: embers have extra drag and gravity, so they trail and droop as they fall, rather than flying outward and vanishing.</p></li><li><p><strong>Crossette</strong>: each ember splits into a small secondary burst partway through its life.</p></li><li><p><strong>Strobe</strong>: embers flicker between visible and invisible as they age.</p></li></ul><p>Let embers fall under gravity after the burst, just like any other particle. When an ember from a low-height burst lands on your material grid, check the cell underneath it, if it holds a flammable material, ignite it, so a firework that bursts close to the ground can set fire to anything flammable you&#8217;ve painted there.</p><p><strong>Testing:</strong> Launch a firework at maximum height and confirm it explodes and spawns embers in your selected pattern, ring embers should spread evenly, willow embers should droop and trail, crossette embers should each produce a smaller secondary burst, and strobe embers should visibly flicker. Launch a low-height firework directly over a patch of wood or oil you&#8217;ve painted on the grid, and confirm falling embers ignite it. Launch a low-height firework over sand or stone and confirm nothing ignites.</p><h3>Step 8</h3><p>In this step your goal is to make the simulation perform well as particle and cell counts grow.</p><p>Decouple your simulation update from your render frame rate by running updates on a fixed timestep, this keeps the simulation speed consistent even if the display refresh rate varies. Cap the number of live particles, or reuse a pool of particle objects, so dense emitters and big fireworks don&#8217;t collapse your frame rate. Update your material grid so that, once it&#8217;s reasonably large, you only process cells that changed or were active on the previous tick, rather than scanning every cell every tick. Add an on-screen readout showing frames per second, the current live particle count, and the current active grid cell count.</p><p><strong>Testing:</strong> Fill a large grid (at least a few hundred cells wide) with a mix of falling sand and water and confirm the readout shows a healthy frame rate once most of it has settled and few cells are still active. Launch several fireworks in quick succession and confirm your particle count readout stays capped rather than climbing indefinitely, and that the frame rate holds up. Compare frame rate with and without your active-cell optimisation on a mostly-settled grid to confirm it&#8217;s actually helping.</p><h3>Going Further</h3><p>Once you have the core playground working, here are some ideas to take it further:</p><ul><li><p>Add more materials: acid that dissolves what it touches, plant seeds that grow when watered, ice that forms from water in a cold region and melts back</p></li><li><p>Add a save/load feature that serialises the grid and lets users share or reload a scene</p></li><li><p>Let the user resize the brush with a scroll wheel or pinch gesture, and add an eraser tool</p></li><li><p>Add a &#8220;wind zone&#8221; the user can draw that applies extra wind force to particles and embers passing through it</p></li><li><p>Support multiple simultaneous fireworks with different colours, and let the user pick a colour per launch</p></li><li><p>Render the grid with a lighting pass, so fire and embers cast a warm glow on nearby cells</p></li><li><p>Port the grid simulation to run on the GPU (for example with a fragment shader or compute shader) so you can push the grid size and particle count much higher</p></li><li><p>Add sound effects that react to events: a crackle for fire, a whoosh for launch, a boom for each firework burst</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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item><item><title><![CDATA[Just Four Days Left To Enter The Ready, Spec, Ship Hackathon!]]></title><description><![CDATA[What will you build and could you win a prize?]]></description><link>https://codingchallenges.substack.com/p/just-four-days-left-to-enter-the</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/just-four-days-left-to-enter-the</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Wed, 19 Aug 2026 08:02:04 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feb66742d-1692-4b6b-9f67-6775c24f3143_1672x941.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></p><p><span>I just wanted to remind you there are just </span><strong><span>4 days left</span></strong><span> to enter and submit to the Ready, Spec, Ship Hackathon.</span></p><p><span>If you&#8217;ve been thinking about entering, now&#8217;s the time to stop polishing the idea and start shipping the project.</span></p><p><span>You can enter solo or as a team of up to three people, and you can submit more than one project.</span></p><p><span>All verified entries receive f</span><strong><span>ree Kiro credits</span></strong><span> to use during the hackathon, and there&#8217;s a $9,600 prize pool across the top three projects.</span></p><p><span>&#129351; </span><strong><span>$5,000</span></strong><span> cash + 12 months of Kiro Pro Max<br>&#129352; </span><strong><span>$1,000</span></strong><span> cash + 12 months of Kiro Pro Max<br>&#129353; 12 months of Kiro Pro Max</span></p><p><span>Your project doesn&#8217;t need to be perfect. It needs to be submitted.</span></p><p><span>I&#8217;ve been building my screen recording tool alongside everyone, and the deadline has been a great push to keep making progress. I&#8217;ve also taken a detour along the way to submit a couple of PRs to </span><strong><span>Kiro</span></strong><span>&#8217;s new Kiro Crew tool which has become a favourite of mine.</span></p><p><span>&#128197; Submission deadline: 23 August midnight UTC<br>&#127942; Winners announced: 6 September</span></p><p><span>Submit here:<br></span><a href="iframe.php?url=https%3A%2F%2Fcodingagents.fyi%2Fhackathon%2Fkiro%2F"><span>https://codingagents.fyi/hackathon/kiro/</span></a></p><p><span>Four days. Build, finish, submit. &#128640;</span></p><p><span>It&#8217;s a great chance to checkout the Kiro agentic coding environment!</span><br><br><span>I hope to see you taking part.</span><br><br><span>Please let me know if you have any questions.</span><br><br><span>Regards, John</span></p><p><span>P.S. You can read about Kiro Crew here: </span><a href="iframe.php?url=https%3A%2F%2Fkiro.dev%2Fcrew%2F"><span>https://kiro.dev/crew/</span></a></p>]]></content:encoded></item><item><title><![CDATA[Coding Challenge #131 - Automated Code Review Agent]]></title><description><![CDATA[This challenge is to build your own code review agent.]]></description><link>https://codingchallenges.substack.com/p/coding-challenge-131-automated-code</link><guid isPermaLink="false">https://codingchallenges.substack.com/p/coding-challenge-131-automated-code</guid><dc:creator><![CDATA[John Crickett]]></dc:creator><pubDate>Sat, 15 Aug 2026 08:01:51 GMT</pubDate><enclosure url="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7cc28ca9-4b73-4a2e-9244-9cd5c122ad4b_2400x1600.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em><span>Hi, this is John with this week&#8217;s Coding Challenge.</span></em></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></p><p><em><span>If there is a Coding Challenge you&#8217;d like to see, please let me know by replying to this email&#128231;</span></em></p><h2><span>Coding Challenge #131 - Automated Code Review Agent</span></h2><p><span>This challenge is to build your own automated code review agent that reviews every pull request against your team&#8217;s coding guidelines and posts what it finds back as a comment.</span></p><p><em><span>This challenge was created in collaboration with </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4wM4EoF"><span>Kestra</span></a><span>, an </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4wOEdyA"><span>open source</span></a><span> orchestration platform where workflows are written as YAML and run anywhere, including on-premise and air-gapped environments. If you want a sense of where the first few steps are heading, their </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4qd9gS7"><span>AI GitHub PR Review blueprint</span></a><span> is a compact example of the idea, though you&#8217;ll be building well past it.</span></em></p><p><span>It&#8217;s relatively easy to write a script to review code with an LLM. Grab the diff, send them to a language model with a list of your team&#8217;s rules, and post the feedback as a comment. It takes an afternoon and it feels like magic. Then reality hits. The model provider rate limits you and the review silently disappears. Two people push at the same time and the bot posts three contradictory comments. Someone opens a 4,000 line pull request and the whole thing falls over. Worst of all, the bot starts confidently complaining about code that is perfectly fine, because all it can see is the diff and it has no idea what the rest of the file looks like.</span></p><p><span>In this challenge you&#8217;ll start with exactly that fragile script, and then rebuild it robustly. You&#8217;ll turn it into a declarative workflow with retries, event triggers and concurrency control. You&#8217;ll teach it to handle large pull requests, to run real static analysis tools instead of guessing, and finally to go and look things up for itself.</span></p><h2><span>The Challenge - Building Your Own Automated Code Review Agent</span></h2><p><span>You&#8217;re going to build a code review agent that watches a repository, reviews every pull request that opens, and posts its findings as a comment. It will survive a provider outage, cope with a pull request that touches sixty files, and ground its opinions in the output of real linters rather than vibes.</span></p><p><span>You don&#8217;t need deep experience with any particular language, because most of what you build will be YAML workflow definitions plus a small amount of scripting. What it does ask for is patience with real world plumbing: API tokens, webhooks, containers, and the awkward gap between something that works on your machine and something that works every time.</span></p><p><span>The fun parts are not the AI bits. Getting a model to write a code review is easy. The fun parts are everything that stops it being a demo project: what happens when the API call fails, what happens when three events arrive at once, and what happens when the diff is bigger than the context window?</span></p><h3><span>Step Zero</span></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>You&#8217;ll need three things before you start.</span></p><p><span>First, a GitHub personal access token with permission to read pull requests and write comments on a repository you own. Keep it somewhere safe, you&#8217;ll be using it throughout.</span></p><p><span>Second, an API key for a language model provider. Any of the big ones will do to begin with, and later in the challenge you&#8217;ll deliberately swap between several, so pick whichever is quickest for you to get started with today.</span></p><p><span>Third, and most importantly, something to review. A code reviewer is only as testable as the code you point it at, so you want a pull request where you already know what a good review would say.</span></p><p><span>I&#8217;ve put one together for you at </span><a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FJohnCrickett%2FReviewMe"><span>ReviewMe</span></a><span>. It&#8217;s a small Python checkout service, and every flaw in it is deliberate. Fork it, and you&#8217;ll find two pull requests waiting. The big one, which adds refund ratios and a reporting pack, is the one you&#8217;ll use for almost every step. It contains, on purpose:</span></p><ul><li><p><span>A live-looking Stripe key committed straight into app/config.py. Your reviewer should always catch this one.</span></p></li><li><p><span>A rewritten calculate_discount in app/pricing.py with conditionals nested five levels deep. Your reviewer should suggest early returns.</span></p></li><li><p><span>A new ratio = amount / order.total line in app/orders.py that looks like a division by zero waiting to happen. It isn&#8217;t, because the only caller guards against it, but that caller lives in app/checkout.py which this pull request does not touch. This one is a trap, and it matters much later.</span></p></li><li><p><span>A regenerated uv.lock with several hundred changed lines.</span></p></li><li><p><span>A rewritten app/report.py of around 650 lines.</span></p></li><li><p><span>An updated test file covering the pricing changes.</span></p></li></ul><p><span>The second pull request just fixes a spelling mistake in a comment. You&#8217;ll want that one in Step 4.</span></p><p><span>If you&#8217;d rather build your own, do, just create the same six issues.</span></p><p><span>You&#8217;ll also want Kestra running locally. It ships as a Docker image and the </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4fSHPIA"><span>quickstart</span></a><span> will get you to a working UI, though you won&#8217;t need it until Step 2. Alternatively it&#8217;s an open source project you can download from </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4wOEdyA"><span>Kestra&#8217;s GitHub</span></a><span>.</span></p><p><strong><span>Testing:</span></strong><span> Confirm your credentials work before writing anything substantial. Fetch the pull request with a single request to https://api.github.com/repos/{owner}/{repo}/pulls/{number}/files, using your token as a bearer token. You should get back a JSON array with one entry per changed file, each containing a patch field with the diff. If you get a 401 or an empty array, fix that now rather than debugging it later through three layers of workflow.</span></p><h3><span>Step 1</span></h3><p><span>In this step your goal is to build the fragile version, the one you&#8217;ll spend the rest of the challenge replacing.</span></p><p><span>Write a single script, in whatever language you like, that takes a repository owner, a repository name and a pull request number, fetches the list of changed files from the GitHub API, sends the diff to your language model along with a handful of review guidelines, and prints the review to your terminal. Read both the GitHub token and the model API key from environment variables. Don&#8217;t post anything to GitHub yet, just print it.</span></p><p><span>Keep this deliberately small. It should be well under a hundred lines. The point is not to build something good, it&#8217;s to have a working baseline and to prove your credentials and your prompt both do what you expect.</span></p><p><span>Once it runs, write down everything it doesn&#8217;t do. Mine looked like this: if the API call gets rate limited the review is simply lost, there is no record anywhere that it ran, the only way to trigger it is to run it by hand or put it on a cron job and hope, and if it produces nothing useful there is no way to find out why. Keep that list. Every remaining step in this challenge addresses one of the items on it.</span></p><p><strong><span>Testing:</span></strong><span> Run your script against the refund and reporting pull request from Step Zero. You should get a review printed to your terminal that mentions the Stripe key sitting in app/config.py. If it doesn&#8217;t spot that, your prompt needs work before you go any further, because everything after this depends on the review being worth reading.</span></p><p><span>Now break it on purpose. Set your model API key to an invalid value and run it again. Notice what you get: probably a stack trace, and definitely no review.</span></p><h3><span>Step 2</span></h3><p><span>In this step your goal is to get the same pull request data flowing through a workflow instead of a script.</span></p><p><span>Get Kestra running locally and create a namespace for your work. Kestra organises flows into namespaces the way a filesystem uses directories, so something like company.reviews is fine.</span></p><p><span>Now build your first flow. It should take the repository owner, the repository name and the pull request number as inputs, so you can type them into the UI and hit execute. It should fetch the pull request&#8217;s changed files, and it should log what came back. Kestra has an HTTP request task for this, and templating that lets you drop input values into a URL.</span></p><p><span>Once that works, add a second fetch for the pull request itself, at /repos/{owner}/{repo}/pulls/{number}, which gets you the title and description. The author&#8217;s own explanation of what they were trying to do are often some of the most useful context you can give a reviewer, human or otherwise.</span></p><p><span>Don&#8217;t add the model yet. This step is about getting comfortable with the shape of a flow, and about the fact that you can now see every execution, its inputs, its outputs and its logs, in a UI. That&#8217;s the first item off your Step 1 list gone.</span></p><p><strong><span>Testing:</span></strong><span> Execute the flow from the UI with your fork&#8217;s owner, name and pull request number. Open the execution in the UI, click through to the task outputs, and confirm you can see the diff for each changed file and the pull request title. Execute it a second time and confirm both runs appear in the execution history with their inputs recorded.</span></p><h3><span>Step 3</span></h3><p><span>In this step your goal is to turn the diff into an actual review.</span></p><p><span>Add a task that sends the diff and the pull request metadata to a language model. Kestra&#8217;s AI plugin has a chat completion task that takes a provider, a model and a list of messages, so you can point it at whichever provider you set up in Step Zero.</span></p><p><span>There are two key things here. First is the system prompt: tell the model it is an engineer reviewing a pull request, and be specific about what you want back. Ask for a Summary, an Issues Found section where every issue is tagged CRITICAL, MAJOR or MINOR, a Suggestions section, and a Verdict. Structure matters more than you&#8217;d think, because in a few steps you&#8217;ll be merging several of these together and sorting them by severity.</span></p><p><span>The second is the guidelines. Take them as a flow input rather than burying them in the prompt. Your team&#8217;s coding standards will change, and the people who want to change them are often not the people who are comfortable editing a workflow. Making the rules an input rather than part of the code is a small decision that pays off every time someone asks for a new rule.</span></p><p><strong><span>Testing:</span></strong><span> Run the flow against the pull request. The review should come back in the structure you asked for, with the Stripe key tagged CRITICAL and the nesting in calculate_discount showing up as at least a MINOR issue.</span></p><p><span>Now change the guidelines input without touching the flow. Remove the rule about nesting and add one demanding that every function has a docstring. Run it again. The nesting complaint should disappear, and you should not have edited a single line of the flow to make that happen.</span></p><h3><span>Step 4</span></h3><p><span>In this step your goal is to close the loop and get the review onto the pull request.</span></p><p><span>Add a final task that posts the review as a comment, using the GitHub issues comments endpoint. Pull requests are issues as far as that part of the API is concerned, so the URL is /repos/{owner}/{repo}/issues/{number}/comments.</span></p><p><span>While you&#8217;re here, stop passing your tokens around in plain sight. Kestra has a secret() function for this. One thing that catches people out: in the open source edition, secrets come from environment variables prefixed with SECRET_, and the value has to be base64 encoded. If you paste a raw token in, you&#8217;ll get a confusing failure rather than a clear one, so encode it first.</span></p><p><span>Finally, add one small piece of judgement. If the review found no issues at all, log that fact and skip posting. A bot that comments &#8220;looks good to me&#8221; on every single trivial pull request gets muted fast, and a muted bot is worth nothing.</span></p><p><strong><span>Testing:</span></strong><span> Run the flow and check the pull request on GitHub. The review should be there as a comment, readable, with its headings intact.</span></p><p><span>Then test the quiet path. Run the flow against the second pull request, the one that fixes the spelling mistake in a comment. Nothing should be posted, and the execution log should tell you why it chose not to. If your reviewer manages to find a CRITICAL issue in a corrected spelling, your prompt is being far too eager and that will only get worse from here.</span></p><h3><span>Step 5</span></h3><p><span>In this step your goal is to make the choice of model a detail rather than a decision.</span></p><p><span>Right now your flow is welded to one provider. Change the provider on your completion task to a different one, Anthropic or Google Gemini or OpenAI, whichever you didn&#8217;t use in Step 3, and run the same pull request through again. You&#8217;ll need a second API key, and you should not need to touch anything else in the flow. Three lines change. The fetching, the prompt, the posting and the skip logic all stay exactly as they are.</span></p><p><span>Then do something more interesting. Point it at a model running locally on your own machine via Ollama and run the review again with no outbound calls to any commercial API at all. This is the version that can run inside a bank or a defence contractor, and being able to switch to it by editing three lines is the whole argument for keeping your orchestration separate from your model.</span></p><p><span>While you&#8217;re doing all this, record the token usage and the completion task reports for each provider on the same pull request, alongside how long each one took. You now have a cost comparison based on your own code rather than someone&#8217;s pricing page.</span></p><p><strong><span>Testing:</span></strong><span> Run the identical flow against the same pull request with at least three different providers, including a local model. All three should produce a usable review that catches the Stripe key.</span></p><p><span>Compare them. Note the token counts, the wall clock time, and where the reviews disagree. The local model will likely be slower and less sharp. Decide for yourself whether it&#8217;s good enough, because that&#8217;s a real decision teams have to make.</span></p><h3><span>Step 6</span></h3><p><span>In this step your goal is to stop running the review by hand.</span></p><p><span>Replace manual execution with a webhook trigger, and point a GitHub webhook at it so that opening a pull request runs the review automatically. Secure the trigger with a key, otherwise anyone who finds the URL can make your workflow burn tokens on their behalf.</span></p><p><span>If you&#8217;re running Kestra locally, GitHub can&#8217;t reach you directly. A tunnelling tool such as ngrok will give you a public URL that forwards to your machine, which is fine for the length of this challenge.</span></p><p><span>Two bits of care are needed here. GitHub sends a lot of events to that webhook, so your flow should act on opened, reopened and synchronize, and quietly ignore everything else. Somebody adding a label should not trigger a code review.</span></p><p><span>The second is more subtle, and it&#8217;s the reason this step and the next are separate. synchronize fires on every single push. If someone pushes three quick fixes in a row, you&#8217;ll get three reviews running at once, all posting comments, at least two of them already out of date by the time they land. Set a concurrency limit on the flow so that only one review of a given pull request runs at a time, and choose what happens to the ones that arrive while it&#8217;s busy. Cancelling them is usually right for code review, because a review of the final state is worth more than three reviews of states nobody will ever look at again.</span></p><p><strong><span>Testing:</span></strong><span> Push a new commit to the pull request and watch the execution start on its own. The comment should appear on GitHub without you touching anything.</span></p><p><span>Then try to break it. Push three commits in quick succession. You should end up with one review, not three. Check the execution list and confirm you can see the ones that were cancelled and why.</span></p><p><span>Finally, add a label to the pull request. Nothing should happen at all.</span></p><h3><span>Step 7</span></h3><p><span>In this step your goal is to make the workflow survive a bad day.</span></p><p><span>Add retries to the tasks that talk to the outside world. Both the GitHub API and every model provider will rate limit you eventually, and a single 429 should not mean a lost review. Three attempts with an increasing delay between them handles the overwhelming majority of real failures.</span></p><p><span>Add a timeout to the model call. Providers occasionally accept a request and then simply never answer, and without a timeout your execution will sit there indefinitely looking like it&#8217;s working.</span></p><p><span>Then handle the case where it genuinely fails anyway. Add error handling to the flow that posts a short notice to Slack or Discord when a review fails, including the execution ID so you can go straight to the logs. This is the difference between a system you trust and a system you have to check on. Without it, a broken review looks exactly the same as a pull request nobody bothered to review, and you won&#8217;t find out for weeks.</span></p><p><strong><span>Testing:</span></strong><span> Set your model API key to an invalid value and push a commit. The flow should retry, fail cleanly, and you should get a notification containing the execution ID. Follow that ID back to the logs and confirm you can see exactly what went wrong.</span></p><p><span>Restore the key and confirm normal service resumes. If you can, test the retry path properly by pointing a task at an endpoint that returns a 429 and watching the attempts in the logs with the delay growing between them.</span></p><h3><span>Step 8</span></h3><p><span>In this step your goal is to handle a pull request that is too big to review in one go.</span></p><p><span>Right now you send the entire diff to the model as a single prompt. That works fine for the small pull requests you&#8217;ve been testing with and falls apart on a real one. Change your flow so that it reviews each changed file separately and in parallel, rather than concatenating everything into one enormous prompt.</span></p><p><span>That immediately raises the question of which files are worth reviewing at all. Nobody wants an AI review of a lockfile. Skip files that are generated, vendored or locked. Whatever you skip, say so in the output. A reviewer that silently ignores half the pull request is worse than no reviewer, because you&#8217;ll trust it.</span></p><p><span>Then put it back together. Merge the per file reviews into a single comment, ordered with the most severe issues first, so that a reader sees the CRITICAL finding at the top rather than four paragraphs down under a MINOR note about naming. One comment, not one per file.</span></p><p><strong><span>Testing:</span></strong><span> Run it against the pull request, which has uv.lock, it should be skipped, and the comment should say so and say why. The remaining files, config.py, orders.py, pricing.py and the tests, should each have been reviewed.</span></p><p><span>Check the execution graph in the UI. You should be able to see the per file reviews running side by side rather than one after another. Compare the total time to what it took in Step 4 and note the difference.</span></p><h3><span>Step 9</span></h3><p><span>In this step your goal is to stop your reviewer guessing about things a tool can simply know.</span></p><p><span>A language model asked to spot an unused import is doing an expensive impression of a linter, and it is worse at it. So run the real thing. Clone the repository at the pull request&#8217;s head commit, run a static analysis tool appropriate to the language, and capture what it finds. ReviewMe is Python and already set up for ruff, so uv run ruff check . will do it. If you built your own fixture in another language, eslint or semgrep will get you to the same place.</span></p><p><span>Run that tool inside a container rather than installing it on the machine running Kestra. Kestra&#8217;s task runners exist for exactly this: the linter, its version and its dependencies live in a Docker image, and your workflow stays clean. This also means your reviewer can handle a repository in a language nobody on your platform team has installed locally.</span></p><p><span>Then feed the findings into the review prompt, and change your instructions. Tell the model to report the tool&#8217;s findings as fact, and to spend its own commentary on the things a linter fundamentally cannot judge: whether the change matches the author&#8217;s stated intent, whether the naming makes sense, whether the approach is sound. This is the division of labour that makes automated review actually useful. Deterministic tools for deterministic problems, and the model for the judgement calls.</span></p><p><span>This is the longest step in the challenge, mostly because getting a repository checked out and a container running takes more fiddling than the rest. Give yourself a bit of time.</span></p><p><strong><span>Testing:</span></strong><span> Run it against the pull request. The comment should now contain findings that are clearly attributable to the linter, reported precisely with file and line numbers, alongside softer commentary from the model.</span></p><p><span>Push a violation your linter definitely catches, an unused variable is ideal, and confirm it appears in the review with the exact line number. Then check the model isn&#8217;t taking credit for it or quietly contradicting it. If the two disagree, your prompt needs to be firmer about which one wins.</span></p><h3><span>Going Further</span></h3><p><span>Once you have the core reviewer working, here are some ideas to take it further:</span></p><ul><li><p><span>Post inline comments on specific lines using GitHub&#8217;s review API, rather than one summary comment at the bottom.</span></p></li><li><p><span>Have the reviewer reply to itself when a new commit fixes something it flagged earlier, so a pull request shows the issue being resolved rather than repeated.</span></p></li><li><p><span>Keep a record of every review, its token cost and its verdict, and build a dashboard showing what your reviewer costs per month and which rules fire most often.</span></p></li><li><p><span>Backfill the last fifty merged pull requests and compare your agent&#8217;s findings to the comments humans actually left. This is the honest evaluation, and it will be humbling.</span></p></li><li><p><span>Split the workflow into a reusable subflow that any repository in your organisation can call with its own guidelines, rather than copying the flow around.</span></p></li><li><p><span>Keep your flows in Git and deploy them through CI, so the reviewer is itself code reviewed.</span></p></li><li><p><span>Add support for GitLab or Bitbucket by swapping the fetch and post tasks, keeping everything in between identical.</span></p></li><li><p><span>Give teams their own namespaces and access controls, so the security team can edit review guidelines without being able to change the workflow itself.</span></p></li></ul><h3><span>P.S. If You Enjoy Coding Challenges Here Are Four Ways You Can Help Support It</span></h3><ol><li><p><span>Refer a friend or colleague to the newsletter. &#128591;</span></p></li><li><p><span>Sign up for a </span><a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2Fsubscribe"><span>paid subscription</span></a><span> - 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 </span><a href="iframe.php?url=https%3A%2F%2Fshop.codingchallenges.fyi%2F"><span>courses</span></a><span>.</span></p></li><li><p><span>Buy one of my </span><a href="iframe.php?url=https%3A%2F%2Fshop.codingchallenges.fyi%2F"><span>courses</span></a><span> that walk you through a Coding Challenge.</span></p></li><li><p><span>Subscribe to the </span><a href="iframe.php?url=https%3A%2F%2Fwww.youtube.com%2F%40codingchallengesfyi"><span>Coding Challenges YouTube channel</span></a><span>!</span></p></li></ol><h3><span>Share Your Solutions!</span></h3><p><span>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 </span><a href="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social"><span>Bluesky</span></a><span> or </span><a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F"><span>LinkedIn</span></a><span> or just post about it there and tag me. Alternately please add a link to it in the </span><a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions"><span>Coding Challenges Shared Solutions</span></a><span> Github repo</span></p><h3><span>Request for Feedback</span></h3><p><span>I&#8217;m writing these challenges to help you develop your skills as a software engineer based on how I&#8217;ve approached my own personal learning and development. What works for me, might not be the best way for you - so if you have suggestions for how I can make these challenges more useful to you and others, please get in touch and let me know. All feedback greatly appreciated.</span></p><p><span>You can reach me on </span><a href="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social"><span>Bluesky</span></a><span>, </span><a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F"><span>LinkedIn</span></a><span> or through </span><a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F"><span>SubStack</span></a></p><p><span>Thanks and happy coding!</span></p><p><span>John</span></p>]]></content:encoded></item><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="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2898b4f0-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="iframe.php?url=https%3A%2F%2Fcodingagents.fyi%2Fhackathon%2Fkiro%2F">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="iframe.php?url=https%3A%2F%2Fkiro.dev%2F">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="iframe.php?url=https%3A%2F%2Fcodingagents.fyi%2Fhackathon%2Fkiro%2F">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="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%2124ZO%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1b5d6db9-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="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%2124ZO%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1b5d6db9-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="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%215M1_%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1e0398d8-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="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%215M1_%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F1e0398d8-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="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">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="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F007b617b-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="iframe.php?url=https%3A%2F%2Fcodingagents.fyi%2Fhackathon%2Fkiro%2F"><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="iframe.php?url=https%3A%2F%2Fkiro.dev%2F"><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="iframe.php?url=https%3A%2F%2Fcodingagents.fyi%2Fhackathon%2Fkiro%2F"><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="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9107af6-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="iframe.php?url=https%3A%2F%2Ffandf.co%2F4fMDaJe"><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="iframe.php?url=https%3A%2F%2Ffandf.co%2F4fMDaJe"><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="iframe.php?url=https%3A%2F%2Ffandf.co%2F4vR2x1C"><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="iframe.php?url=https%3A%2F%2Ffandf.co%2F4wZgM5n"><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="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21ix7d%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6c955c34-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="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21ix7d%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F6c955c34-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="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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="iframe.php?url=https%3A%2F%2Ffandf.co%2F4fcyiNt"><span>here</span></a><span>.</span></p><p>Check out the previous coding challenge, <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.fyi%2Fchallenges%2Fchallenge-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="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%2134VM%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F08cef745-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="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%2134VM%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F08cef745-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="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21XVsp%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F71642998-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="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21XVsp%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F71642998-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="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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="iframe.php?url=https%3A%2F%2Fadk.dev%2Ftutorials%2Fmulti-tool-agent%2F">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="iframe.php?url=https%3A%2F%2Fspiffe.io%2F"><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="iframe.php?url=https%3A%2F%2Ffandf.co%2F3TKucUg"><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="iframe.php?url=https%3A%2F%2Fopentelemetry.io%2Fdocs"><span>OpenTelemetry</span></a><span> and export its traces to </span><a href="iframe.php?url=https%3A%2F%2Ffandf.co%2F4yFju1g"><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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">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="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JN6I%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb2446c47-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="iframe.php?url=https%3A%2F%2Fanam.ai%2F%3Futm_source%3Dcodingchallenges%26amp%3Butm_medium%3Dnewsletter%26amp%3Butm_campaign%3Dcara4-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="iframe.php?url=https%3A%2F%2Fanam.ai%2F%3Futm_source%3Dcodingchallenges%26amp%3Butm_medium%3Dnewsletter%26amp%3Butm_campaign%3Dcara4-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="iframe.php?url=https%3A%2F%2Fanam.ai%2F%3Futm_source%3Dcodingchallenges%26amp%3Butm_medium%3Dnewsletter%26amp%3Butm_campaign%3Dcara4-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="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JN6I%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb2446c47-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="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21JN6I%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fb2446c47-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="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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="iframe.php?url=https%3A%2F%2Fanam.ai%2F%3Futm_source%3Dcodingchallenges%26amp%3Butm_medium%3Dnewsletter%26amp%3Butm_campaign%3Dcara4-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="iframe.php?url=https%3A%2F%2Fanam.ai%2Fdocs%2F%3Futm_source%3Dcodingchallenges%26amp%3Butm_medium%3Dnewsletter%26amp%3Butm_campaign%3Dcara4-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="iframe.php?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DBHfrXVMV-SE">mastering behavioural interviews</a>. Also check out Austen&#8217;s newsletter: <a href="iframe.php?url=https%3A%2F%2Fthebehavioral.substack.com%3Fr%3Dgjveo">Mastering Behavioral Interviews</a>. He also has a <a href="iframe.php?url=https%3A%2F%2Fthebehavioral.substack.com%2Fp%2Fbehavioral-prep-on-a-budget-peer">guide to doing mock interviews</a>.</p><p><strong>Testing:</strong> Use the <a href="iframe.php?url=https%3A%2F%2Flab.anam.ai%2F%3Futm_source%3Dcodingchallenges%26amp%3Butm_medium%3Dnewsletter%26amp%3Butm_campaign%3Dcara4-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="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21h51z%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd778d50a-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="auto, 100vw"><img src="iframe.php?url=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21h51z%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fd778d50a-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="auto, 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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">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="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc71eb9c-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="iframe.php?url=https%3A%2F%2Fman.openbsd.org%2Ftmux">tmux man page</a> and the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Ftmux%2Ftmux%2Fwiki%2FGetting-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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">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="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F86efb9dd-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="iframe.php?url=https%3A%2F%2Farxiv.org%2Fabs%2F2005.09007">arxiv.org/abs/2005.09007</a>) and the code at <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fxuebinqin%2FU-2-Net">github.com/xuebinqin/U-2-Net</a>. The popular <code>rembg</code> library (<a href="iframe.php?url=https%3A%2F%2Fgithub.com%2Fdanielgatis%2Frembg">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="iframe.php?url=https%3A%2F%2Farxiv.org%2Fabs%2F1706.05587">arxiv.org/abs/1706.05587</a>).</p></li><li><p><strong>MediaPipe Image Segmenter and Selfie Segmentation</strong> from Google (<a href="iframe.php?url=https%3A%2F%2Fai.google.dev%2Fedge%2Fmediapipe%2Fsolutions%2Fvision%2Fimage_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="iframe.php?url=https%3A%2F%2Fsegment-anything.com%2F">segment-anything.com</a>, paper at <a href="iframe.php?url=https%3A%2F%2Farxiv.org%2Fabs%2F2304.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="iframe.php?url=https%3A%2F%2Farxiv.org%2Fabs%2F2011.11961">arxiv.org/abs/2011.11961</a>, <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FZHKKKe%2FMODNet">github.com/ZHKKKe/MODNet</a>) and <strong>BiRefNet</strong> (<a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FZhengPeng7%2FBiRefNet">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="iframe.php?url=https%3A%2F%2Fdl.acm.org%2Fdoi%2F10.1145%2F1015706.1015720">dl.acm.org/doi/10.1145/1015706.1015720</a>, and there is a clear walkthrough in the OpenCV docs at <a href="iframe.php?url=https%3A%2F%2Fdocs.opencv.org%2F4.x%2Fd8%2Fd83%2Ftutorial_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="iframe.php?url=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FOtsu%2527s_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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">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="iframe.php?url=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7740c43e-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="iframe.php?url=http%3A%2F%2Fwww.xmailserver.org%2Fdiff2.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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a> or <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or just post about it there and tag me. Alternately please add a link to it in the <a href="iframe.php?url=https%3A%2F%2Fgithub.com%2FCodingChallengesFYI%2FSharedSolutions">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="iframe.php?url=https%3A%2F%2Fbsky.app%2Fprofile%2Fjohncrickett.bsky.social">Bluesky</a>, <a href="iframe.php?url=https%3A%2F%2Fwww.linkedin.com%2Fin%2Fjohncrickett%2F">LinkedIn</a> or through <a href="iframe.php?url=https%3A%2F%2Fcodingchallenges.substack.com%2F">SubStack</a></p><p>Thanks and happy coding!</p><p>John</p>]]></content:encoded></item></channel></rss>