[{"data":1,"prerenderedAt":1015},["ShallowReactive",2],{"post-\u002Fblog\u002Fthe-line-nothing-could-see":3,"blog-all":522},{"id":4,"title":5,"author":6,"body":7,"category":504,"date":505,"description":506,"draft":507,"extension":508,"image":509,"meta":510,"navigation":185,"path":511,"project":512,"readingMinutes":255,"seo":513,"series":514,"seriesOrder":514,"stem":515,"tags":516,"__hash__":521},"blog\u002Fblog\u002Fthe-line-nothing-could-see.md","The Line Nothing Could See","EkoHacks Team",{"type":8,"value":9,"toc":496},"minimark",[10,14,17,20,25,47,50,54,57,72,79,82,86,89,92,99,313,321,324,328,331,334,337,385,388,469,472,475,479,482,486,489,492],[11,12,13],"p",{},"You learn more about an engineer from a refactor than from a feature. A feature has a spec to hide behind. A refactor has only judgement: what the author chose to touch, what they treated as fixed, and which questions they asked before deciding a line deserved to live.",[11,15,16],{},"That observation shapes how we work. The team works real issues on EkoLite, our small real time backend, and when a refactoring story comes back as a pull request we read it twice. Once for the code, and once for the reasoning the code implies. The gaps we find in the second reading are the curriculum. Not a curriculum we planned in advance, but the one the work itself just wrote.",[11,18,19],{},"Here is one of those readings.",[21,22,24],"h2",{"id":23},"what-arrived","What arrived",[11,26,27,28,33,34,38,39,42,43,46],{},"The story asked for a cleanup pass over two small classes: the shutdown policy we wrote about in ",[29,30,32],"a",{"href":31},"\u002Fblog\u002Fthe-five-second-goodbye","The five second goodbye",", and the nullable ",[35,36,37],"code",{},"ProcessWrapper"," it leans on. The PR that came back was careful work. A duplicated guard extracted into a helper. A long signal handler moved out of ",[35,40,41],{},"arm()"," into a named method, byte for byte. A stub timer bug genuinely found and fixed: the fake clock fired timers in insertion order where the real ",[35,44,45],{},"setTimeout"," fires them by deadline, and the fix came pinned with a new test, with no existing test modified. Every test green, the description honest about what changed and why.",[11,48,49],{},"Nothing wrong in the ship it sense. Which is exactly when the interesting review starts, because a refactor that works is finally quiet enough to show you how its author thinks.",[21,51,53],{"id":52},"reading-the-reasoning","Reading the reasoning",[11,55,56],{},"Three of the changes, read together, told one story.",[11,58,59,60,63,64,67,68,71],{},"The extracted guard took a parameter. The old code had two identical checks, each throwing its own message: ",[35,61,62],{},"advanceTime only available on null instance",", ",[35,65,66],{},"simulateSignal only available on null instance",". The new helper reproduced each string exactly, so it needed the caller to pass its own method name, and the author typed that parameter as a strict union of the two names. You can see the reasoning: the messages are existing behaviour, existing behaviour is a contract, contracts must be preserved precisely, and strictness is free. What nobody asked is whether anything depends on those strings. Nothing does. There is not a single ",[35,69,70],{},"toThrow"," in the test file. The author preserved a constraint no one had set, and paid for it with a parameter where a caller can now pass the wrong name and ship an error that blames the wrong method.",[11,73,74,75,78],{},"The timer fix came with a cleanup line. After firing due timers, the new code compacted the internal array so fired and cancelled timers would not accumulate. Preventing growth is a good production instinct. But this array lives inside a test stub that exists for the length of a test and holds at most four timers, and the field it compacts is private, read by nothing except the loop that already skips dead entries. No test can fail because of that line. No test could ever fail because of that line. And the shape of it was revealing in its own right: the field was declared ",[35,76,77],{},"readonly",", so instead of reassigning a filtered array, the code splices the array with a spread of a filter of itself, a small contortion whose whole purpose is to mutate around a declaration the author did not feel allowed to question.",[11,80,81],{},"That was the pattern. Every choice optimised for visible tidiness: deduplicate, tighten types, clean up after yourself, match the interface exactly. All respectable instincts, and none of them wrong on their own. What was missing was one question, asked of any single line before keeping it or adding it: what can observe this? Which test is this line answerable to? Two of the four threads we opened on that PR were that one question wearing different clothes. The other two were its mirror image: a behaviour a test should have watched, and didn't.",[21,83,85],{"id":84},"teaching-with-a-failing-test","Teaching with a failing test",[11,87,88],{},"So how do you teach that? Not with a comment that says remove this. A verdict teaches the fix and nothing else. The rule we hold ourselves to as reviewers is the same rule we hold the code to: every claim gets backed by a test.",[11,90,91],{},"If the claim is about shape, the proof is the existing suite staying green after the simpler version. That is what we wrote on the guard helper and the compaction line: here is the simpler form, here is how the neighbouring wrappers already solve the same problem, and the evidence that nothing breaks is the suite you already have.",[11,93,94,95,98],{},"If the claim is about behaviour, the reviewer owes a failing test. The timer fix, good as it was, kept one gap from the old loop: a timer created inside another timer's callback could never fire in the same ",[35,96,97],{},"advanceTime"," call, although the real process would fire it. Rather than explain that in prose and hope, the review thread carried this:",[100,101,106],"pre",{"className":102,"code":103,"language":104,"meta":105,"style":105},"language-ts shiki shiki-themes github-dark","it('fires a timer scheduled by another timer, when both come due in the same advance', () => {\n  const proc = ProcessWrapper.createNull();\n  const order: number[] = [];\n\n  proc.startTimer(10, () => {\n    order.push(10);\n    proc.startTimer(5, () => order.push(15));\n  });\n\n  proc.advanceTime(100);\n\n  expect(order).toEqual([10, 15]);\n});\n","ts","",[35,107,108,135,157,180,187,207,223,253,259,264,278,283,307],{"__ignoreMap":105},[109,110,113,117,121,125,128,132],"span",{"class":111,"line":112},"line",1,[109,114,116],{"class":115},"svObZ","it",[109,118,120],{"class":119},"s95oV","(",[109,122,124],{"class":123},"sU2Wk","'fires a timer scheduled by another timer, when both come due in the same advance'",[109,126,127],{"class":119},", () ",[109,129,131],{"class":130},"snl16","=>",[109,133,134],{"class":119}," {\n",[109,136,138,141,145,148,151,154],{"class":111,"line":137},2,[109,139,140],{"class":130},"  const",[109,142,144],{"class":143},"sDLfK"," proc",[109,146,147],{"class":130}," =",[109,149,150],{"class":119}," ProcessWrapper.",[109,152,153],{"class":115},"createNull",[109,155,156],{"class":119},"();\n",[109,158,160,162,165,168,171,174,177],{"class":111,"line":159},3,[109,161,140],{"class":130},[109,163,164],{"class":143}," order",[109,166,167],{"class":130},":",[109,169,170],{"class":143}," number",[109,172,173],{"class":119},"[] ",[109,175,176],{"class":130},"=",[109,178,179],{"class":119}," [];\n",[109,181,183],{"class":111,"line":182},4,[109,184,186],{"emptyLinePlaceholder":185},true,"\n",[109,188,190,193,196,198,201,203,205],{"class":111,"line":189},5,[109,191,192],{"class":119},"  proc.",[109,194,195],{"class":115},"startTimer",[109,197,120],{"class":119},[109,199,200],{"class":143},"10",[109,202,127],{"class":119},[109,204,131],{"class":130},[109,206,134],{"class":119},[109,208,210,213,216,218,220],{"class":111,"line":209},6,[109,211,212],{"class":119},"    order.",[109,214,215],{"class":115},"push",[109,217,120],{"class":119},[109,219,200],{"class":143},[109,221,222],{"class":119},");\n",[109,224,226,229,231,233,236,238,240,243,245,247,250],{"class":111,"line":225},7,[109,227,228],{"class":119},"    proc.",[109,230,195],{"class":115},[109,232,120],{"class":119},[109,234,235],{"class":143},"5",[109,237,127],{"class":119},[109,239,131],{"class":130},[109,241,242],{"class":119}," order.",[109,244,215],{"class":115},[109,246,120],{"class":119},[109,248,249],{"class":143},"15",[109,251,252],{"class":119},"));\n",[109,254,256],{"class":111,"line":255},8,[109,257,258],{"class":119},"  });\n",[109,260,262],{"class":111,"line":261},9,[109,263,186],{"emptyLinePlaceholder":185},[109,265,267,269,271,273,276],{"class":111,"line":266},10,[109,268,192],{"class":119},[109,270,97],{"class":115},[109,272,120],{"class":119},[109,274,275],{"class":143},"100",[109,277,222],{"class":119},[109,279,281],{"class":111,"line":280},11,[109,282,186],{"emptyLinePlaceholder":185},[109,284,286,289,292,295,298,300,302,304],{"class":111,"line":285},12,[109,287,288],{"class":115},"  expect",[109,290,291],{"class":119},"(order).",[109,293,294],{"class":115},"toEqual",[109,296,297],{"class":119},"([",[109,299,200],{"class":143},[109,301,63],{"class":119},[109,303,249],{"class":143},[109,305,306],{"class":119},"]);\n",[109,308,310],{"class":111,"line":309},13,[109,311,312],{"class":119},"});\n",[11,314,315,316,320],{},"Paste it in, watch it fail, and the claim is no longer the reviewer's opinion. It is a fact about the code that the author can hold in their own hands. The comment then explains why it fails, in this case that the stub advances its clock to the end of the window before any callback runs, so the nested timer anchors its deadline in a future the loop has already passed. And it ends with a hint that points at the mechanism, not the diff: the stub has to move time the way the real clock does, step by step from one due timer to the next. The author writes the code. The test decides when they are done. What came back, and why the fix ran deeper than the one red test, is its own piece, ",[29,317,319],{"href":318},"\u002Fblog\u002Fthe-clock-that-learned-to-walk","The clock that learned to walk",".",[11,322,323],{},"Two more habits keep the thread honest. When a finding predates the PR, the comment says so first, because blame is a fact to state and not a feeling to manage; two of our four findings were older than the branch, merely made visible by it. And a thread does not close when the test goes green. It closes when the author writes back what they found, in their own words. Green is the code understanding the change. The sentence is the person understanding it, and the sentence is the one we cannot ship without.",[21,325,327],{"id":326},"the-exit-no-guard-could-see","The exit no guard could see",[11,329,330],{},"The nested timer was one of the two mirror image threads, the behaviour a test should have watched. The other was the double exit, and it tested this piece's own claim, that a thread closes on the sentence and not the green, harder than anything else on the branch.",[11,332,333],{},"The policy arms a deadline, then closes the app: a clean close exits 0, a deadline that fires first exits 1 and stops waiting. But the close that lost the race is still out there, and when it finally settles its handler runs anyway. The original code let it record a clean exit 0 on top of the hard exit 1 already taken. A shutdown that reports it timed out, and then reports it went cleanly. Both cannot be true, and older than the branch, it had stayed quiet because nothing had made it speak.",[11,335,336],{},"The author fixed it the honest way, a boolean that remembers an exit already happened, red test first, then green. Every habit in this piece says the thread closes there. It did not, because green was the code agreeing with the change, and the change was not yet the whole design. The flag guarded the two exits it was written beside. A second signal, the operator who means it, leaves on its own line, and that line never touched the flag. So a second red test, pasted into the same thread, ran straight to two exits again:",[100,338,340],{"className":102,"code":339,"language":104,"meta":105,"style":105},"proc.simulateSignal('SIGTERM');\nproc.simulateSignal('SIGTERM');   \u002F\u002F exits hard, forgets to remember it\nresolveClose();                   \u002F\u002F the first close lands, a clean 0 on top\n",[35,341,342,357,374],{"__ignoreMap":105},[109,343,344,347,350,352,355],{"class":111,"line":112},[109,345,346],{"class":119},"proc.",[109,348,349],{"class":115},"simulateSignal",[109,351,120],{"class":119},[109,353,354],{"class":123},"'SIGTERM'",[109,356,222],{"class":119},[109,358,359,361,363,365,367,370],{"class":111,"line":137},[109,360,346],{"class":119},[109,362,349],{"class":115},[109,364,120],{"class":119},[109,366,354],{"class":123},[109,368,369],{"class":119},");   ",[109,371,373],{"class":372},"sAwPA","\u002F\u002F exits hard, forgets to remember it\n",[109,375,376,379,382],{"class":111,"line":159},[109,377,378],{"class":115},"resolveClose",[109,380,381],{"class":119},"();                   ",[109,383,384],{"class":372},"\u002F\u002F the first close lands, a clean 0 on top\n",[11,386,387],{},"The comment named the shape, not the line. This class has four ways out, second signal, deadline, close resolves, close rejects, and the flag was checked at two of them. A guard that lives at each exit is a guard you can forget to add the next time you open one, which is precisely what had happened. So the answer was not a third check. It was to give the question a single home, a small gate the exits pass through instead of each remembering the rule for itself:",[100,389,391],{"className":102,"code":390,"language":104,"meta":105,"style":105},"private exitOnce(code: number): void {\n  if (this.exited) {\n    return;\n  }\n  this.exited = true;\n  this.proc.exit(code);\n}\n",[35,392,393,409,423,431,436,451,464],{"__ignoreMap":105},[109,394,395,398,401,404,407],{"class":111,"line":112},[109,396,397],{"class":119},"private ",[109,399,400],{"class":115},"exitOnce",[109,402,403],{"class":119},"(code: number): ",[109,405,406],{"class":130},"void",[109,408,134],{"class":119},[109,410,411,414,417,420],{"class":111,"line":137},[109,412,413],{"class":130},"  if",[109,415,416],{"class":119}," (",[109,418,419],{"class":143},"this",[109,421,422],{"class":119},".exited) {\n",[109,424,425,428],{"class":111,"line":159},[109,426,427],{"class":130},"    return",[109,429,430],{"class":119},";\n",[109,432,433],{"class":111,"line":182},[109,434,435],{"class":119},"  }\n",[109,437,438,441,444,446,449],{"class":111,"line":189},[109,439,440],{"class":143},"  this",[109,442,443],{"class":119},".exited ",[109,445,176],{"class":130},[109,447,448],{"class":143}," true",[109,450,430],{"class":119},[109,452,453,455,458,461],{"class":111,"line":209},[109,454,440],{"class":143},[109,456,457],{"class":119},".proc.",[109,459,460],{"class":115},"exit",[109,462,463],{"class":119},"(code);\n",[109,465,466],{"class":111,"line":225},[109,467,468],{"class":119},"}\n",[11,470,471],{},"The second signal and the deadline leave through it now, and the rule that used to be copied at each door lives in one. Then the thread closed, on the sentence. The author wrote back, in their own words, what the second signal had been doing before the gate: exiting at once without marking that it had exited, so the still pending close resolved a clean goodbye on top of the hard one. Nobody asked for a recital of the fix. They described the bug as they had come to see it, and that description is the thing no passing suite could have told us.",[11,473,474],{},"Set the two ends of the pull request beside each other. It opened with a line nothing could see, a cleanup that ran for no one, and it closed with an exit no guard could see, a way out the flag never covered. The same absence in opposite clothes: one a line the code did not need, the other a line the code did need and no test yet watched for. A refactor's quiet gift is that it moves the furniture enough for both to throw a shadow. Standing where the shadow falls, twice on the same thread if that is what it takes, is the review.",[21,476,478],{"id":477},"why-we-work-this-way","Why we work this way",[11,480,481],{},"This is the whole method. Engineers work a real backlog on a real codebase, the review reads the reasoning and not just the diff, and the gap gets taught at the exact moment it costs something, with evidence the author can run rather than authority they have to take on trust. Nobody learns the missing question from a slide. They learn it from a failing test that would not exist if they had asked it.",[21,483,485],{"id":484},"the-move-to-take-home","The move to take home",[11,487,488],{},"Refactoring by form says: deduplicate, tighten, clean, align. Refactoring answerable to tests asks one question first: what can observe this line? If the answer is a test, name it, and let it hold the line in place. If the answer is nothing, the line is either missing its test or it does not belong, and the refactor phase is precisely when you are allowed to say which.",[11,490,491],{},"And if you review other people's refactors: back every claim with a test. Green suite for claims about shape, a failing test the author can paste in for claims about behaviour. The moment your review can be run instead of believed, it stops being a judgement and becomes a conversation about the code, which is the only conversation that was ever worth having.",[493,494,495],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":105,"searchDepth":137,"depth":137,"links":497},[498,499,500,501,502,503],{"id":23,"depth":137,"text":24},{"id":52,"depth":137,"text":53},{"id":84,"depth":137,"text":85},{"id":326,"depth":137,"text":327},{"id":477,"depth":137,"text":478},{"id":484,"depth":137,"text":485},"Craft and Practice","2026-07-07T22:43:00.000Z","Reviewing the reasoning under a refactor, not only the diff. Ask of every line what can observe it, and back each review claim with a test the author can run.",false,"md","\u002Fimages\u002Fblog\u002Fthe-line-nothing-could-see-hero.jpg",{},"\u002Fblog\u002Fthe-line-nothing-could-see","EkoLite",{"title":5,"description":506},null,"blog\u002Fthe-line-nothing-could-see",[517,518,519,520],"code-review","refactoring","teaching","testing","ahgjLKLi-RMcPEgzp6BZ5AUa8DnbfJ96IfgnccF0A8U",[523,534,546,553,563,569,578,585,592,601,608,617,624,631,644,653,660,667,676,684,692,702,709,717,724,730,736,745,753,763,769,776,786,794,800,810,818,826,833,841,849,856,864,866,873,879,886,895,903,910,917,926,932,940,946,954,960,969,976,983,991,997,1003,1009],{"path":524,"title":525,"description":526,"date":527,"author":6,"image":528,"readingMinutes":209,"category":504,"project":529,"tags":530,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fa-branch-is-inventory","A Branch Is Inventory","Unintegrated code is stock in a warehouse, and it depreciates. Our branch was one merge behind the trunk and that was enough for a conflict. What waiting costs.","2026-07-09T09:38:00.000Z","\u002Fimages\u002Fblog\u002Fa-branch-is-inventory-hero.jpg","The Dojo",[531,532,533],"git","continuous-integration","craft",{"path":535,"title":536,"description":537,"date":538,"author":6,"image":514,"readingMinutes":225,"category":539,"project":540,"tags":541,"draft":507,"series":545,"seriesOrder":159},"\u002Fblog\u002Fa-map-drawn-by-the-build","A Map Drawn by the Build","Thirty six of our pages had never been visited by Google because nothing said they existed. The sitemap is drawn from the build, and it found a draft.","2026-08-22T20:30:00.000Z","Shipping and Operations","ekohacks.com",[542,543,544],"seo","operations","nuxt","From Invisible to Findable",{"path":547,"title":548,"description":549,"date":550,"author":6,"image":514,"readingMinutes":209,"category":539,"project":540,"tags":551,"draft":507,"series":545,"seriesOrder":261},"\u002Fblog\u002Fa-privacy-policy-you-can-diff","A Privacy Policy You Can Diff","Our legal pages are files in the same repository as the site. Each now lists every change to its text, with the date and the diff, drawn from git at build time.","2026-08-23T16:00:00.000Z",[552,543,544],"privacy",{"path":554,"title":555,"description":556,"date":557,"author":6,"image":558,"readingMinutes":225,"category":559,"project":512,"tags":560,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fan-off-switch-not-a-disguise","An Off Switch, Not a Disguise","A Nullable differs from a stub in whether production code can tell the fake is there. Finding the word fixed a socket wrapper that had been lying to its tests.","2026-07-20T09:05:00.000Z","\u002Fimages\u002Fblog\u002Fan-off-switch-not-a-disguise-hero.jpg","Testing and TDD",[561,520,562],"nullables","websocket",{"path":564,"title":565,"description":566,"date":567,"author":6,"image":568,"readingMinutes":182,"category":504,"project":514,"tags":514,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fbuilding-engineering-teams-that-learn-together","Building Engineering Teams That Learn Together","The strongest engineering teams are not the ones with the most talented individuals. They are the ones that have learned how to learn together.","2026-02-05T09:00:00.000Z","\u002Fimages\u002Fblog\u002Fteams-that-learn-together-hero.jpg",{"path":570,"title":571,"description":572,"date":573,"author":6,"image":574,"readingMinutes":182,"category":539,"project":529,"tags":575,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fderived-state-recompute","The Fix Was Merged, but the Numbers Were Still Wrong","We fixed a scoring bug, reviewed it, merged it, and production still showed the old numbers. Derived data is a cache that goes stale, and it must be recomputed.","2026-07-08T21:43:55.000Z","\u002Fimages\u002Fblog\u002Fderived-state-recompute-hero.jpg",[543,576,577],"deployment","idempotency",{"path":579,"title":580,"description":581,"date":582,"author":583,"image":584,"readingMinutes":209,"category":539,"project":514,"tags":514,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fdockerising-nestjs-nuxt-monorepo-with-pnpm","Dockerising a NestJS and Nuxt Monorepo with pnpm","Dockerising a NestJS and Nuxt monorepo with pnpm: separate multi stage Dockerfiles for development and production, one Compose file, reproducible environments.","2026-02-09T09:00:00.000Z","Ogochukwu Okpala","\u002Fimages\u002Fblog\u002Fdocker-monorepo-hero.png",{"path":586,"title":587,"description":588,"date":589,"author":6,"image":514,"readingMinutes":225,"category":539,"project":540,"tags":590,"draft":507,"series":545,"seriesOrder":591},"\u002Fblog\u002Ffifty-posts-and-an-empty-robots-txt","Fifty Posts and an Empty robots.txt","Our site scored 100 for SEO and nobody could find it. The audit that started a series on making a site findable, and on what measuring visitors costs them.","2026-08-21T14:30:00.000Z",[542,543,533],0,{"path":593,"title":594,"description":595,"date":596,"author":6,"image":597,"readingMinutes":182,"category":504,"project":529,"tags":598,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Ffrom-convention-to-plugin","From Convention to Plugin","A five step procedure for webhook signature checks, easy to forget and silent when you did, became one Fastify plugin. The safe path is now impossible to skip.","2026-04-17T08:26:12.000Z","\u002Fimages\u002Fblog\u002Ffrom-convention-to-plugin-hero.jpg",[533,599,600],"fastify","security",{"path":602,"title":603,"description":604,"date":605,"author":6,"image":606,"readingMinutes":189,"category":539,"project":529,"tags":607,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fintegrate-against-the-trunk","Integrate Against the Trunk, Not Your Copy of It","A rebase onto a stale local main succeeds, looks resolved, and leaves the pull request conflicting. Everything on your machine is a photograph of the trunk.","2026-07-09T09:22:00.000Z","\u002Fimages\u002Fblog\u002Fintegrate-against-the-trunk-hero.jpg",[531,532,543],{"path":609,"title":610,"description":611,"date":612,"author":6,"image":613,"readingMinutes":182,"category":504,"project":529,"tags":614,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Flandmines","Landmines","Most CLAUDE.md files grow because documenting a convention feels like progress. Ours holds landmines only: facts the code cannot tell you, costly to step on.","2026-04-17T10:15:06.000Z","\u002Fimages\u002Fblog\u002Flandmines-hero.jpg",[533,615,616],"documentation","ai-agents",{"path":618,"title":619,"description":620,"date":621,"author":6,"image":622,"readingMinutes":209,"category":539,"project":529,"tags":623,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fnever-break-the-build","Never Break the Build","If the software worked five minutes ago, only five minutes of change can be to blame. What a green trunk buys, who pays for it, and a check that could not fail.","2026-07-09T09:54:00.000Z","\u002Fimages\u002Fblog\u002Fnever-break-the-build-hero.jpg",[532,543,520],{"path":625,"title":626,"description":627,"date":628,"author":6,"image":514,"readingMinutes":225,"category":539,"project":540,"tags":629,"draft":507,"series":545,"seriesOrder":137},"\u002Fblog\u002Fone-address-per-page","One Address per Page","Every page on our site answered with a redirect before it answered with a page. How we made the build emit one address, declare it, and check it live.","2026-08-22T18:30:00.000Z",[542,543,544,630],"netlify",{"path":632,"title":633,"description":634,"date":635,"author":6,"image":636,"readingMinutes":182,"category":637,"project":638,"tags":639,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fone-command-three-policies","One Command, Three Policies, Zero Duplication","Preflight, cut and ship each worked alone before one command composed them. Why the composition layer is forty lines, and how the scrollback became the record.","2026-07-21T17:20:00.000Z","\u002Fimages\u002Fblog\u002Fone-command-three-policies-hero.jpg","Design and Architecture","The EkoHacks CLI",[640,641,642,643],"composition","cli-design","narration","encoding-the-release",{"path":645,"title":646,"description":647,"date":648,"author":6,"image":649,"readingMinutes":225,"category":637,"project":512,"tags":650,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fone-way-to-fail","One Way to Fail","Closing the convergence so an upload fails in the same error shape as every other call, mostly by deleting the code that made the two shapes differ.","2026-07-20T10:05:00.000Z","\u002Fimages\u002Fblog\u002Fone-way-to-fail-hero.jpg",[651,561,652],"design","error handling",{"path":654,"title":655,"description":656,"date":657,"author":6,"image":658,"readingMinutes":209,"category":539,"project":529,"tags":659,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fready-to-ship-not-ready-to-release","Ready to Ship, Not Ready to Release","Most teams hide a delay between saying done and being able to ship. Being ready to release at any moment, and why our deploy ships the commit that was tested.","2026-07-09T10:02:00.000Z","\u002Fimages\u002Fblog\u002Fready-to-ship-hero.jpg",[532,576,543],{"path":661,"title":662,"description":663,"date":664,"author":6,"image":665,"readingMinutes":209,"category":504,"project":529,"tags":666,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Frebase-or-merge-is-the-wrong-argument","Rebase or Merge Is the Wrong Argument","Both commands reconcile the same commits and hit the same conflicts. What differs is when you resolve them. The loud argument hides a quiet one: long branches.","2026-07-09T09:30:00.000Z","\u002Fimages\u002Fblog\u002Frebase-or-merge-hero.jpg",[531,532,533],{"path":668,"title":669,"description":670,"date":671,"author":6,"image":672,"readingMinutes":159,"category":637,"project":529,"tags":673,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Freconstructing-the-past","Reconstructing the Past","Connect a repository with a year of history and a trend curve reaches back months. How a system shows a past it never saw, recomputed from one source of truth.","2026-07-07T21:26:35.000Z","\u002Fimages\u002Fblog\u002Freconstructing-the-past-hero.jpg",[651,674,675],"data","onboarding",{"path":677,"title":678,"description":679,"date":680,"author":6,"image":514,"readingMinutes":209,"category":559,"project":638,"tags":681,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fretiring-a-board","Retiring a Board: User Testing a CLI Before You Ship It","An afternoon of user testing against a real Linear board, and the four findings a green test suite could never have written.","2026-08-01T10:20:00.000Z",[682,561,641,683],"user-testing","linear",{"path":685,"title":686,"description":687,"date":688,"author":6,"image":689,"readingMinutes":189,"category":559,"project":529,"tags":690,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fretrofitting-testing-without-mocks","Retrofitting Testing Without Mocks","The server had 37 vi.mock calls and no test that touched the database. Five steps to remove every mock with Nullables: real logic, real data, sociable tests.","2026-04-17T12:30:37.000Z","\u002Fimages\u002Fblog\u002Fretrofitting-without-mocks-hero.jpg",[520,561,691],"mocks",{"path":693,"title":694,"description":695,"date":696,"author":6,"image":697,"readingMinutes":182,"category":539,"project":638,"tags":698,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Frun-it-by-hand-until-it-is-boring","Run It by Hand Until It Is Boring","Releasing EkoLite was a document before it was a command, and stayed one until running it by hand had nothing left to teach us. Why we automate operations last.","2026-07-21T17:00:00.000Z","\u002Fimages\u002Fblog\u002Frun-it-by-hand-hero.jpg",[699,700,701,643],"automation","releasing","process",{"path":703,"title":704,"description":705,"date":671,"author":6,"image":706,"readingMinutes":159,"category":539,"project":529,"tags":707,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fsafe-to-run-twice","Safe to Run Twice","Connect the same repository twice, or let a webhook redeliver an event. Idempotency in two halves: writes through a natural key, side effects behind a ledger.","\u002Fimages\u002Fblog\u002Fsafe-to-run-twice-hero.jpg",[543,577,708],"webhooks",{"path":710,"title":711,"description":712,"date":713,"author":6,"image":514,"readingMinutes":209,"category":539,"project":529,"tags":714,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fsource-or-execute","Source or Execute","Our dojo laptops are provisioned by one bash script of nearly twelve hundred lines. Splitting it into modules is two decisions, not one, and we chose a hybrid.","2026-08-01T09:00:00.000Z",[715,716,640,543],"shell","provisioning",{"path":718,"title":719,"description":720,"date":514,"author":721,"image":722,"readingMinutes":112,"category":637,"project":723,"tags":514,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fstage-then-transform-why-your-crawler-should-never-touch-production-tables","Stage, Then Transform: Crawlers Stay Out of Production","Where data from an external source lands first decides whether the pipeline still works in six months. The staging pattern, shown with a crawler we build now.","EkoHacks","\u002Fimages\u002Fblog\u002Fstage-then-transform-hero.jpg","Propi",{"path":725,"title":726,"description":727,"date":728,"author":6,"image":514,"readingMinutes":225,"category":559,"project":540,"tags":729,"draft":507,"series":545,"seriesOrder":189},"\u002Fblog\u002Ftelling-the-machine-what-the-page-is","Telling the Machine What the Page Is","Structured data on every post, and a test that runs after each build and fails it when a page lacks what a crawler needs. First run: forty nine problems.","2026-08-22T23:30:00.000Z",[542,520,544],{"path":731,"title":732,"description":733,"date":734,"author":6,"image":735,"readingMinutes":137,"category":559,"project":514,"tags":514,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Ftest-driven-development-is-not-about-tests","Test Driven Development Is Not About Tests","TDD is widely misunderstood. It is not a testing strategy. It is a design tool that produces simpler, more focused code by making you think before you type.","2026-02-01T09:00:00.000Z","\u002Fimages\u002Fblog\u002Ftdd-not-about-tests-hero.jpg",{"path":737,"title":738,"description":739,"date":740,"author":6,"image":741,"readingMinutes":189,"category":559,"project":638,"tags":742,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Ftesting-a-release-without-releasing","Testing a Release Without Releasing","How to test watch CI until green, then merge, hundreds of times a day with no CI in sight. Nullables, output trackers, and a fake that copies GitHub shapes.","2026-07-21T17:10:00.000Z","\u002Fimages\u002Fblog\u002Ftesting-a-release-without-releasing-hero.jpg",[561,743,744,643],"testing-without-mocks","james-shore",{"path":746,"title":747,"description":748,"date":749,"author":6,"image":514,"readingMinutes":209,"category":559,"project":529,"tags":750,"draft":507,"series":752,"seriesOrder":112},"\u002Fblog\u002Fthe-best-coverage-number-in-the-room","The Best Coverage Number in the Room","Same commit, same spec, same test count. The mocked suite ran 5.6 times faster, covered 3.5 fewer points of real code, and posted the best branch coverage.","2026-08-27T12:00:00.000Z",[520,561,691,751],"coverage","What Mocks Cost",{"path":754,"title":755,"description":756,"date":757,"author":6,"image":758,"readingMinutes":189,"category":637,"project":512,"tags":759,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-bug-that-resolved-successfully","The Bug That Resolved Successfully","A bug that threw no error and dropped every document because the client guessed a collection from a name. The fix was one honest field on the ready message.","2026-07-05T10:16:16.000Z","\u002Fimages\u002Fblog\u002Fbug-resolved-successfully-hero.jpg",[760,761,520,762],"protocol","pubsub","tdd",{"path":318,"title":764,"description":765,"date":766,"author":6,"image":767,"readingMinutes":209,"category":559,"project":512,"tags":768,"draft":507,"series":514,"seriesOrder":514},"The Clock That Learned to Walk","A stub clock that teleported to the end of the window instead of passing through time, so a timer set inside another never fired. A real clock model fixed it.","2026-07-07T22:44:42.000Z","\u002Fimages\u002Fblog\u002Fthe-clock-that-learned-to-walk-hero.jpg",[561,520,518],{"path":770,"title":771,"description":772,"date":773,"author":6,"image":774,"readingMinutes":189,"category":504,"project":529,"tags":775,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-conflict-nobody-wrote","The Conflict Nobody Wrote","A pull request conflicted in a file neither author had typed into. A generated document recorded test line numbers, both branches shifted them, git merges text.","2026-07-09T09:14:00.000Z","\u002Fimages\u002Fblog\u002Fthe-conflict-nobody-wrote-hero.jpg",[531,532,533],{"path":777,"title":778,"description":779,"date":780,"author":6,"image":781,"readingMinutes":255,"category":539,"project":529,"tags":782,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-container-that-wasnt-there","The Container That Wasn't There","Production served HTML where the browser asked for JavaScript, and nothing in our code had changed. A week debugging a platform we cannot see inside.","2026-07-17T14:30:00.000Z","\u002Fimages\u002Fblog\u002Fcontainer-that-wasnt-there-hero.jpg",[543,783,784,785],"debugging","european-tech","community",{"path":787,"title":788,"description":789,"date":790,"author":6,"image":791,"readingMinutes":209,"category":637,"project":512,"tags":792,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-design-we-never-drew","The Design We Never Drew","How the upload path in EkoLite arrived rather than being drawn, and the two habits, test first and integrate always, that turn an emerging shape into a design.","2026-07-20T10:00:00.000Z","\u002Fimages\u002Fblog\u002Fthe-design-we-never-drew-hero.jpg",[651,762,793],"continuous integration",{"path":795,"title":796,"description":797,"date":798,"author":6,"image":799,"readingMinutes":182,"category":504,"project":514,"tags":514,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-dojo-way-deliberate-practice-for-software-developers","The Dojo Way: Deliberate Practice for Software Developers","Most developers learn on the job, and the job rarely teaches the fundamentals. The Dojo borrows from martial arts to make a space for deliberate practice.","2026-01-22T09:00:00.000Z","\u002Fimages\u002Fblog\u002Fthe-dojo-way-hero.jpg",{"path":801,"title":802,"description":803,"date":804,"author":6,"image":805,"readingMinutes":225,"category":504,"project":529,"tags":806,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-done-column-is-a-syllabus","The Done Column Is a Syllabus","We asked Linear for every story our team has ever finished, all 187 of them, and read the column end to end. It turned out to be the course we teach.","2026-07-31T09:00:00.000Z","\u002Fimages\u002Fblog\u002Fdone-column-syllabus-hero.jpg",[807,808,809,683],"graphql","stories","xp",{"path":811,"title":812,"description":813,"date":814,"author":6,"image":815,"readingMinutes":189,"category":559,"project":638,"tags":816,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-failing-test-is-the-review","The Failing Test Is the Review","Every behaviour in the CLI landed as two commits: a red test, then the code that makes it green. Reviewing intent before implementation, and why red never bent.","2026-07-21T17:05:00.000Z","\u002Fimages\u002Fblog\u002Fthe-failing-test-is-the-review-hero.jpg",[762,817,517,643],"red-first",{"path":819,"title":820,"description":821,"date":822,"author":6,"image":823,"readingMinutes":189,"category":539,"project":638,"tags":824,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-first-real-run-said-no","The First Real Run Said No","The tool's first real outing ended with FAIL and a stopped rail, and we count it as a success. A naive sounding question then uncovered a quiet failure mode.","2026-07-21T17:25:00.000Z","\u002Fimages\u002Fblog\u002Fthe-first-real-run-said-no-hero.jpg",[700,825,531,643],"preflight",{"path":31,"title":827,"description":828,"date":829,"author":6,"image":830,"readingMinutes":189,"category":539,"project":512,"tags":831,"draft":507,"series":514,"seriesOrder":514},"The Five Second Goodbye","A shutdown that arms its own deadline so a hung close cannot strand the process, with the policy lifted out of the boot shell so every decision can be tested.","2026-07-05T12:03:00.000Z","\u002Fimages\u002Fblog\u002Fthe-five-second-goodbye-hero.jpg",[832,561,520],"shutdown",{"path":834,"title":835,"description":836,"date":837,"author":6,"image":838,"readingMinutes":209,"category":539,"project":512,"tags":839,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-framework-in-the-description-field","The Framework in the Description Field","Our package called itself a framework, but it could not be installed. The fix was to stop trusting the build and become the stranger who installs the tarball.","2026-07-08T17:28:39.000Z","\u002Fimages\u002Fblog\u002Fframework-description-field-hero.jpg",[840,520,533],"packaging",{"path":842,"title":843,"description":844,"date":845,"author":6,"image":846,"readingMinutes":182,"category":637,"project":638,"tags":847,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-gate-always-asks","The Gate Always Asks","A release automates things you cannot take back, so which of them may a machine do without asking a human? Pauses, and a yes flag that opens no door.","2026-07-21T17:15:00.000Z","\u002Fimages\u002Fblog\u002Fthe-gate-always-asks-hero.jpg",[699,848,651,643],"human-in-the-loop",{"path":850,"title":851,"description":852,"date":853,"author":6,"image":854,"readingMinutes":209,"category":539,"project":529,"tags":855,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-gate-went-red-and-it-was-right","The Gate Went Red, and It Was Right","We added the missing check on our API specification and it failed on its first run, because the file was never a function of the source. A clean machine saw it.","2026-07-09T10:10:00.000Z","\u002Fimages\u002Fblog\u002Fgate-went-red-hero.jpg",[532,543,533],{"path":857,"title":858,"description":859,"date":860,"author":6,"image":861,"readingMinutes":209,"category":539,"project":512,"tags":862,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-knock-and-the-axe","The Knock and the Axe","SIGTERM is a request your process can hear and SIGKILL is removal it never sees, and the fixed window between them is exactly where graceful shutdown lives.","2026-07-20T12:00:00.000Z","\u002Fimages\u002Fblog\u002Fthe-knock-and-the-axe-hero.jpg",[832,543,863],"signals",{"path":511,"title":5,"description":506,"date":505,"author":6,"image":509,"readingMinutes":255,"category":504,"project":512,"tags":865,"draft":507,"series":514,"seriesOrder":514},[517,518,519,520],{"path":867,"title":868,"description":869,"date":870,"author":6,"image":871,"readingMinutes":209,"category":637,"project":512,"tags":872,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-long-way-round","The Long Way Round","The count a method computes reaches the screen not through its return value but by being written into a collection a client subscribes to, EkoLite's channel.","2026-07-20T11:05:00.000Z","\u002Fimages\u002Fblog\u002Fthe-long-way-round-hero.jpg",[651,761,561],{"path":874,"title":875,"description":876,"date":877,"author":6,"image":514,"readingMinutes":225,"category":539,"project":540,"tags":878,"draft":507,"series":545,"seriesOrder":112},"\u002Fblog\u002Fthe-page-that-existed-only-when-someone-linked-to-it","The Page That Existed Only When Someone Linked to It","Our blog vanished in production while passing every local check. How we found the cause, fixed it twice, and how the fix gave every page a twin.","2026-08-22T16:00:00.000Z",[542,543,544],{"path":880,"title":881,"description":882,"date":883,"author":6,"image":884,"readingMinutes":255,"category":539,"project":529,"tags":885,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-previews-that-served-production","The Previews That Served Production","Two weeks after the container that was not there, the same impossible error came back. The orphans were our own pull request previews. One diagnosis was wrong.","2026-07-31T17:00:00.000Z","\u002Fimages\u002Fblog\u002Fpreviews-served-production-hero.jpg",[543,783,784,785],{"path":887,"title":888,"description":889,"date":890,"author":6,"image":891,"readingMinutes":182,"category":504,"project":638,"tags":892,"draft":185,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-release-is-the-acceptance-test","The Release Is the Acceptance Test","Not a green suite, not a good demo: a real version of a real package, live on npm, shipped by the tool. What red first tests proved, and what only reality did.","2026-07-21T17:35:00.000Z","\u002Fimages\u002Fblog\u002Frelease-acceptance-test-hero.jpg",[700,893,894,643],"dogfooding","acceptance-testing",{"path":896,"title":897,"description":898,"date":899,"author":6,"image":900,"readingMinutes":225,"category":559,"project":512,"tags":901,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-script-that-never-ran","The Script That Never Ran","Joining two abilities EkoLite already had, a method that runs and a runner that shells out, and proving the join with a nulled runner before any script exists.","2026-07-20T11:00:00.000Z","\u002Fimages\u002Fblog\u002Fthe-script-that-never-ran-hero.jpg",[520,561,902],"scriptrunner",{"path":904,"title":905,"description":906,"date":907,"author":6,"image":514,"readingMinutes":255,"category":539,"project":540,"tags":908,"draft":507,"series":545,"seriesOrder":209},"\u002Fblog\u002Fthe-small-things-the-crawler-notices","The Small Things the Crawler Notices","Speed is the part of findability you can measure on a laptop. Fonts and images we shipped without looking, an error page we did not have, and the numbers after.","2026-08-23T10:00:00.000Z",[542,543,909],"performance",{"path":911,"title":912,"description":913,"date":914,"author":6,"image":915,"readingMinutes":225,"category":559,"project":512,"tags":916,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-test-that-could-not-fail","The Test That Could Not Fail","A green test that stayed green when we deleted the behaviour it was named after, and the single blunt question that exposed it.","2026-07-20T09:00:00.000Z","\u002Fimages\u002Fblog\u002Ftest-that-could-not-fail-hero.jpg",[520,561,562],{"path":918,"title":919,"description":920,"date":921,"author":721,"image":922,"readingMinutes":209,"category":504,"project":923,"tags":924,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-website-that-told-the-same-story-twice","The Website That Told the Same Story Twice","Our home and story pages felt like one story across two screens. Rather than argue by taste, we modelled our words as data and asked a database one question.","2026-06-06","\u002Fimages\u002Fblog\u002Fwebsite-same-story-twice-hero.jpg","The Website",[533,925,674,519],"content",{"path":927,"title":928,"description":929,"date":757,"author":6,"image":930,"readingMinutes":189,"category":559,"project":512,"tags":931,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-words-we-work-in","The Words We Work In","The vocabulary we test with, Nullable, embedded stub, stub object, output tracking, each grounded in real code so a review points at a line, never at taste.","\u002Fimages\u002Fblog\u002Fthe-words-we-work-in-hero.jpg",[561,520,533,519],{"path":933,"title":934,"description":935,"date":921,"author":721,"image":936,"readingMinutes":182,"category":504,"project":923,"tags":937,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fthe-workflow-is-the-lesson","The Workflow Is the Lesson","We are moving our content out of a cloud CMS and into git. Why a school that teaches a keyboard first way of working should practise it in the open, and how.","\u002Fimages\u002Fblog\u002Fworkflow-lesson-hero.png",[938,531,519,939],"workflow","accessibility",{"path":941,"title":942,"description":943,"date":944,"author":6,"image":514,"readingMinutes":209,"category":559,"project":529,"tags":945,"draft":507,"series":752,"seriesOrder":137},"\u002Fblog\u002Ftwenty-six-more-tests-four-fewer-behaviours","Twenty Six More Tests, Four Fewer Behaviours","Removing the mocks grew the suite from 44 tests to 70 and quietly deleted four behaviours, every one of them a failure path. Test count is not coverage.","2026-08-28T09:00:00.000Z",[520,561,691,751],{"path":947,"title":948,"description":949,"date":950,"author":6,"image":951,"readingMinutes":225,"category":539,"project":529,"tags":952,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Ftwo-hands-on-one-lever","Two Hands on One Lever","Our deploy failed with a deploy already in progress while the commit it wanted to ship was going live. Anatomy of a benign collision and a trigger safe to lose.","2026-07-23T09:00:00.000Z","\u002Fimages\u002Fblog\u002Ftwo-hands-on-one-lever-hero.jpg",[543,577,953],"observability",{"path":955,"title":956,"description":957,"date":958,"author":6,"image":514,"readingMinutes":225,"category":559,"project":529,"tags":959,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Ftwo-hundred-and-fourteen-undecided","Two Hundred and Fourteen Undecided","An accessibility scanner found three faults on the dojo, we fixed them, and the next run came back clean. The clean run was the dangerous one.","2026-08-22T12:00:00.000Z",[939,520,651,533],{"path":961,"title":962,"description":963,"date":964,"author":6,"image":965,"readingMinutes":159,"category":637,"project":529,"tags":966,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Ftwo-validators-one-gate","Two Validators, One Gate","Validating one request body twice, with a Fastify schema and again with Zod, is not double safety. The second gate is unreachable. One validator; Zod for types.","2026-07-07T22:06:46.000Z","\u002Fimages\u002Fblog\u002Ftwo-validators-one-gate-hero.jpg",[651,967,968],"validation","typescript",{"path":970,"title":971,"description":972,"date":973,"author":6,"image":974,"readingMinutes":209,"category":539,"project":529,"tags":975,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fwe-are-all-doing-asynchronous-integration","We Are All Doing Asynchronous Integration","A continuous integration server is not continuous integration. The forge tests your work after you have moved on. Our build takes three minutes; no excuse.","2026-07-09T09:46:00.000Z","\u002Fimages\u002Fblog\u002Fasynchronous-integration-hero.jpg",[532,543,576],{"path":977,"title":978,"description":979,"date":980,"author":6,"image":514,"readingMinutes":225,"category":539,"project":540,"tags":981,"draft":507,"series":545,"seriesOrder":225},"\u002Fblog\u002Fwe-promised-not-to-watch-then-we-needed-to-see","We Promised Not to Watch. Then We Needed to See.","Our privacy policy said no analytics, and it was true. Adding a page counter meant reopening that promise in public: what we chose, what we refused, one commit.","2026-08-24T10:00:00.000Z",[552,543,982],"analytics",{"path":984,"title":985,"description":986,"date":987,"author":6,"image":988,"readingMinutes":209,"category":539,"project":638,"tags":989,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fwhat-one-real-release-taught-us","What One Real Release Taught Us in an Afternoon","Two crashes and one near miss within minutes of touching the real GitHub, because we had refused to fake it. The race we never met, the merge that never came.","2026-07-21T17:30:00.000Z","\u002Fimages\u002Fblog\u002Fone-real-release-hero.jpg",[700,990,561,643],"failure-modes",{"path":992,"title":993,"description":994,"date":995,"author":6,"image":514,"readingMinutes":209,"category":539,"project":540,"tags":996,"draft":507,"series":545,"seriesOrder":182},"\u002Fblog\u002Fwhat-the-link-looks-like-before-anyone-clicks","What the Link Looks Like Before Anyone Clicks","A link to any page on our site showed up as a bare rectangle of text. The share image is part of the page, and the build now puts one on every route.","2026-08-22T22:00:00.000Z",[542,543,544],{"path":998,"title":999,"description":1000,"date":1001,"author":6,"image":514,"readingMinutes":209,"category":559,"project":529,"tags":1002,"draft":507,"series":752,"seriesOrder":159},"\u002Fblog\u002Fwhat-the-nullable-gave-back","What the Nullable Gave Back","One file, seven behaviours held fixed, the database swapped for a Nullable: about 180 times less time inside the tests, and coverage flat to two decimals.","2026-08-29T09:00:00.000Z",[520,561,691,751],{"path":1004,"title":1005,"description":1006,"date":1007,"author":6,"image":514,"readingMinutes":225,"category":539,"project":540,"tags":1008,"draft":507,"series":545,"seriesOrder":255},"\u002Fblog\u002Fwhat-we-count-and-what-we-refuse-to-know","What We Count, and What We Refuse to Know","Every system that holds anything about a visitor, in one table in the privacy policy. Writing it found a form that no longer existed and one that wrote twice.","2026-08-23T14:00:00.000Z",[552,543,982],{"path":1010,"title":1011,"description":1012,"date":1013,"author":6,"image":1014,"readingMinutes":182,"category":637,"project":514,"tags":514,"draft":507,"series":514,"seriesOrder":514},"\u002Fblog\u002Fwhy-good-software-starts-with-the-right-questions","Why Good Software Starts with the Right Questions","Before writing a line of code, the most impactful thing a team can do is ask better questions. Discovery is not a phase to rush. It is the foundation.","2026-01-15T09:00:00.000Z","\u002Fimages\u002Fblog\u002Fright-questions-hero.jpg",1787861096900]