Writing & build logs.
Essays on software development, web builds, and AI tooling. Plus build notes from what I'm actually working on right now.
Eight clusters triaged, zero published: why autonomous content cadence fails silently
The UK Calculators content pipeline ran three clean cron exits and published nothing for three weekdays. No errors, no alerts, just absence. Three weeks of daily runs showed the failure mode is not bad drafts. It is a gate that rejects everything differently each day, and a cadence monitor that asks the wrong question.
The cron kept drafting mortgage because gov.uk wasn't in the source set yet
For five days, the cron produced news-cluster drafts and operators shipped gov.uk regulatory pillars by hand. Day 6 changed one thing: gov.uk Search API sources entered the polling loop. Two data-pegged pillars from a single cycle followed immediately.
The in-chapter TOC needed live JavaScript. It still got a mockup first.
The captainrandom learning hub navigation retrofit ran five phases. Every one went through the UI gate — interactive approval mockup before production code. The most interesting case was P4: the subsystem's only client-side component, with a live IntersectionObserver scroll-spy in its mockup, still approved before a line of React was written.
The privacy work isn't on the /privacy/ page
The /privacy/ page went live in June and covers what it covers. The actual privacy work on this site happened in a data-locality choice, a brand-separation rule, and a username leak in published MDX. Only one of the three announced itself.
10 of 13 sentences made it. The verifier caught the rest before they reached a pillar page.
The June 30th cron produced a mortgage rate-spike draft grounded in April 2026 data. Ten of thirteen sentences passed. The three that didn't never reached a pillar page — and the next two days showed why that distinction matters at publishing scale.
14 days of daily publishing: the three failure modes I hadn't designed for
A 14-day publish cadence on UK Calculators surfaced three failure modes: title-mangling produces permanent ugly slugs, zero-draft mornings expose the absence of a fallback queue, and triage without memory republishes the same cluster it shipped the day before.
AdSense low-value rejection: thin pages, empty hubs, and six SVG diagrams
UK Calculators was rejected for low-value content on 10 July — ads.txt was authorised, so this was purely a content-quality judgement. Three concrete moves addressed the specific inventory-value criteria: expanding thin calculator landings, adding SVG diagrams across the pillar estate, and populating empty hub pages the rejection identified.
The E2E suite billed 1,130 minutes to test a page that never existed
A Playwright suite on a private repo ran 69 times in three weeks, failed 61 of them, and billed roughly a thousand minutes doing it. The root cause was three mistakes stacked on top of each other, and the deepest one was mine: I did not know GitHub Actions had a bill.
The backlog called it a content gap. It was navigation.
Completing an eight-chapter course on the learning hub felt like progress. Then I looked at what had actually moved the needle — track sections, prerequisite chips, and a JSON-LD breadcrumb. The backlog had been mis-filing the bottleneck for months.
The search index built in three seconds. Five phases made that possible.
Pagefind took an afternoon to wire up. The navigation phases before it took a week. A search index returns what’s there — and what needed to be there first was structure.
AdSense readiness: the checklist caught one gap, the reviewer found another, the sweep found a third
Seven out of eight on the automated readiness audit, then rejected for low value content anyway. The fix required three rounds: media diversity closed before submission, hollow hub listings after rejection, and a site-wide sweep that stripped fabricated trust signals the structural checklist had no way to catch.
Google's AdSense reviewer checklist returned 7 of 8 green. Format diversity was the one that counted.
Running Google's reviewer checklist against UK Calculators returned 7 of 8 green. The one red item was format diversity: zero of 38 articles had any embedded visual content. Eight days after submission, rejected for low value content — a checklist score doesn't carry item weights, and format diversity was the one Google's reviewer weighted most.
The cron produced zero drafts. That was the correct answer.
Four mornings into a 14-day operator-publish sprint on UK Calculators, the pipeline ranked 150 clusters, fetched candidates, and produced nothing. Not a failure. A signal that the ground-truth layer, not the LLM, is the binding constraint in a pipeline built on live gov.uk sources.
Fifteen claim blocks and a zero-draft cron: the cost of grounding a content pipeline in gov.uk
Hand-curated gov.uk claim blocks stopped the ukcalculators drafter fabricating figures. Three days after the first clean pillar shipped, the cron fired on time, ranked 150 clusters, put mortgage at the top — and produced nothing. That was the gate working correctly.
The cheapest phase in a seven-phase plan was also the highest-leverage one
Prerequisite chips in the learning hub — three words, one arrow, no JavaScript — outperformed every other navigation phase by the only metric that matters at the chapter level: whether a reader knows what to read first. The evidence is what the other six phases cost by comparison.
The eight-pillar map closed in three days. ga-telemetry and gsc-telemetry are why.
Phase 1.6 shipped two observability skills on 20 June: ga-telemetry and gsc-telemetry. Six days later the eight-pillar topic-authority map was complete. The link is an operator loop built on next-morning indexation signal.
`ignoreBuildErrors: true` kept the type checker quiet. Turning it off found twelve product bugs.
UK Calculators had `strict: true` in tsconfig and `ignoreBuildErrors: true` in next.config.mjs. The two settings coexisted for months. One afternoon removing the flag surfaced twelve masked type errors on a YMYL money site where wrong numbers carry real consequences.
Scope the MySQL user first: the newsletter DDL as a security boundary on cPanel
On shared hosting, you don't control the firewall or the infrastructure. What you do control is which privileges the MySQL user holds. Scoping it to the newsletter schema alone made a compromised endpoint's blast radius structurally finite — not as a mitigation layered on top, but as a property of the design.
Zero drafts at 07:00: build the gate, not the prompt
The 29 June cron ran every stage correctly and produced nothing. Haiku underfired on claim-extraction, the pipeline accepted the shortfall without raising an error, and the log showed a clean exit. The fix was a corrective stage that detects the count failure, reruns with constrained parameters, and marks the run failed if the second attempt also falls short. Six pillars shipped in the three days that followed.
Ten cycles, one ready draft: the pipeline was consuming its failures
Between 30 June and 13 July, ten cron cycles produced one ready draft and zero auto-publishes while the skipped counter climbed from 2 to 7. The drafter was not stalling. It was treating failed attempts as permanent completions, and only heartbeat visibility on ready-draft count made the dead zone visible.
The cron exited clean. There were no drafts.
Two zero-draft mornings out of four cron firings across 29 June to 2 July, both exits clean. The failure was Haiku's claim extraction underfiring one stage before draft: structurally invisible, no error, no warning, cron returns success.
The cron fired at 08:22 and drafted nothing — scheduling is the easy part
On 2 July, the pipeline ran on time, ranked 150 clusters, fetched candidates, and produced zero drafts. The scheduler worked. The grounding step didn't. Automating gov.uk pillar articles forces a distinction that took a zero-drafts morning to make explicit: cron reliability and content freshness are different problems, and only one of them is solved.
Six navigation phases, one client island: why the keyboard audit took half an hour
The learning hub's navigation plan ran six phases. Five shipped as pure RSC; the server knew the URL, and that was enough. The in-chapter TOC with IntersectionObserver scroll-spy was the only genuine client requirement, and keeping the client surface at one component made the accessible-by-default story apply to everything else.
Three weekdays, zero publishes: draft.py was consuming its own failures
Between 7 and 9 July the UK Calculators cron fired on time, triaged candidates, and assembled source packs. It published nothing. Across ten cycles, skipped climbed from 2 to 7 before the cause was found: draft.py was writing failed-* markers to the same index it used for successful consumption, permanently locking out any topic that had ever failed a draft attempt.
AdSense said 'low value content'. Mapping that to actual pages took a day.
The ads.txt was authorised. The rejection still came back 'low value content' on 10 July — a content quality judgement that maps to three specific gaps: thin surfaces, empty hub listings, and zero images across 38 articles. Passing the re-review required treating Google's inventory-value policy as a content brief, not a configuration checklist.
Five days, ten operator publishes, two cron drafts: where the ukcalculators pipeline actually breaks
Running a 14-day pillar schedule with a parallel operator-publish path made the bottleneck legible: the cron fires on schedule, triage completes, 150 clusters rank — then the ground-and-extract stage drops everything silently. The schedule is not the problem.
Ten of thirteen: why verification without correction is just expensive triage
The morning cron produced a draft with 10 of 13 sentences passing the verifier. Without a corrective step, that's a triage ticket, not a publish. Four days of cron output showed the rescue pattern fires once in four — and what covers the other three.
150 clusters ranked, zero drafted: ground attrition is the load-bearing constraint
Five days of autonomous publishing logs made the pattern clear. The cron fired on schedule every morning. Ground attrition killed the drafts before the drafter ran. One usable cron draft across five days; ten articles published via a different route entirely.
Seven navigation phases. The prerequisite chips encoded the one thing the others couldn't.
The learning navigation spec ran seven phases from foundational structure to site-wide search. P5 — prerequisite chips, a static 'Read first' link at each chapter head — was the smallest implementation. It was also the highest return: every other phase encoded page structure, and P5 encoded reader intent.
Six PHP classes, no Composer: a cPanel newsletter backend with enumeration-resistant auth
The captainrandom.co.uk newsletter backend runs on a cPanel shared host with zero framework and no Composer. Six small PHP classes, around 600 lines, and an eight-stage subscribe pipeline that tells the caller nothing about whether an email address already exists.
Pagefind shipped last. The prerequisite chips had already closed the harder problem.
The learning hub's navigation plan ran seven phases and ended with site-wide Pagefind search. The highest-leverage phase was five: a three-word chip at each chapter head pointing to the one thing to read first. By the time search shipped, most readers who needed it no longer did.
The cron said zero twice and ten-of-thirteen once. Every answer was correct.
Five mornings of cron firing produced two zero-draft rejections, one partial-pass, and six operator-path publishes. Zero drafts is not a pipeline failure. It is the verifier doing its job when the inputs do not warrant a draft.
The navigation plan shipped on Pagefind. Prerequisite chips were the higher-leverage fix.
Six phases of navigation work shipped on the learning hub in July. Pagefind — site-wide search — closed the programme. Prerequisite chips, one phase earlier, delivered more navigability per line of code by firing at orientation time rather than recovery time.
The read-first chip outperformed the TOC, track sections, and site-wide search
The learning hub navigation programme ran six phases across two days: track sections, a scroll-spy TOC, prerequisite chips, and site-wide Pagefind search. The smallest phase shipped fifth and delivered more navigability than all the rest combined. The structural reason was there in the spec before the build started.
AdSense rejected the site. The ads.txt was fine.
The Google AdSense verdict for UK Calculators arrived 10 July: rejected, “Low value content.” The ads.txt was authorised. The review had nothing to do with technical setup — Google was auditing editorial depth, and the /resources hub pages didn’t pass.
I hand-wrote the newsletter backend in PHP. The threat model is the code.
The captainrandom newsletter subscription backend is 600 lines of PHP, six classes, no framework, no Composer. Every security decision — rate limits, honeypots, time-traps, token hygiene — lives in the endpoint files, readable end-to-end in one sitting. That's the trade: no ecosystem, but a threat model you can hold in your head.
Seven phases in. The chip at the chapter head was still the highest-leverage fix.
The learning hub navigation programme shipped complete last week: seven phases, P0 through P6, from breadcrumb to site-wide Pagefind search. Improving on the earlier post that identified the prerequisite chip as the spec's highest-value navigability fix — having shipped P4's IntersectionObserver scroll-spy and P6's full search index, the designation holds. Here's what shipping the full programme makes visible.
The navigation spec pre-ranked prerequisite chips over search. Six phases later, that held.
The conventions.md backlog called prerequisite chips at the chapter head the single highest-value navigability fix before any code shipped. Pagefind search and an IntersectionObserver right-rail TOC both landed in the same programme. The ranking held. This is why.
Prerequisite chips, scroll-spy TOC, and Pagefind: the trio that closed the learning hub nav spec
Seven phases of navigation work landed on the learning hub over two days. Phases P0 through P4 were infrastructure. P5 and P6 changed what the property felt like to use — and both were in the spec from day one.
The cron ran clean and drafted nothing: eight pillars in five days named the extraction bottleneck
Five days into a 14-day operator-publish sprint, the cron fired on-time every day and produced two useful drafts — one of which recycled yesterday's ground. Improving on the quality-gate diagnosis from the inheritance tax piece, this sprint isolated an earlier failure: claim extraction stalls before the drafter gets a turn.
The highest-leverage navigation fix was a conditional on a frontmatter field
conventions.md ranked the N1 prerequisite chip as the single highest-value navigability fix before any phase of the learning-hub navigation plan shipped. After a scroll-spy TOC, hub track sections, and site-wide Pagefind search, the ranking holds. Reader-orientation at the chapter entry-point outperforms retrieval mechanics for an audience that doesn't know what it doesn't know.
A static site has no server to search. So I indexed the output instead.
There is no query endpoint on a statically-exported site, no database, nothing to ask at request time. Full-text search still works, because you index the HTML the build already produced.
GA4 saw nothing for four days. The fix was gated behind the consent it was meant to make optional.
The Consent Mode v2 default that was supposed to fire for every visitor lived inside a function that only ran after someone clicked Accept. So first-time visitors loaded no analytics at all, and GA4 recorded nothing.
GA4 went dark for four days. Both fixes were right. One was in the wrong place.
Two PRs, each diagnosing a real fault, and GA4 still flat four days later. The problem wasn’t whether the consent signal was present — it was where in the document it lived relative to gtag.js.
One hardcoded album was fine. The tenth would have broken the page. So /creative stopped hardcoding anything.
The first version of the creative hub had all ten NAPA tracks written directly into the page component. That works for one album. It falls apart at ten. The rebuild made the page a reader of files, not a holder of content.
Prerequisite chips were the highest-leverage fix. They shipped fifth.
The navigation spec for captainrandom’s learning hub named the N1 prerequisite chips the single highest-value browsability improvement. They shipped in Phase 5 of 7. Course sidebar, in-chapter TOC, prerequisite chips, and site-wide Pagefind each close a different kind of lost — and the order they can ship in is a dependency graph, not a priority list.
The confirm link token was in five places before the subscriber clicked it
Building newsletter confirm and unsubscribe handlers revealed a leak surface most developers overlook: tokens passed via GET end up in browser history, Referer headers, and server access logs before any mutation happens. The fix is an interstitial — show a page on GET, mutate only on POST.
The roadmap was silently gone: four documentation layers and why none of them overlap
Six weeks after captainrandom's first devlog entry, a session to document the /learning/ work surfaced three problems. The most instructive: the roadmap had gone missing without raising an error anywhere. That is what four layers is for.
The tag on 73 of 74 posts told me nothing about what relates to what.
Building a 'related posts' block surfaced a problem that had nothing to do with rendering: the tag on nearly every article carried no signal, and letting it count made everything related to everything.
The video spec had a voice-clone bake-off. The same day, I cut generated voice entirely.
The one open decision in the video-pipeline spec was which synthetic voice would narrate: a clone of mine, or a premium API voice, picked by ear. I deleted the question instead of answering it.
Every route but the blog previewed blank on WhatsApp. The og:image default was missing.
Blog posts set their own share image, so links to them looked fine. That masked the fact that every other route on the site emitted no og:image at all — invisible until someone shared one.
Four cron zeroes in five mornings. Eight articles shipped by hand. The pipeline problem isn't the scheduler.
Five days into a fourteen-day publishing sprint on UK tax calculators, the scheduler fires within a minute of its window every morning. Automated drafts produced: two, one a duplicate cluster. Gov.uk cooperates. The bottleneck is claim extraction and cluster survival.
What shipped on 2026-06-25 wasn't T-004: the verifier is the pipeline's real editor
T-004 was next in the topic-authority backlog — NRB and RNRB explainer, score 11. Two operator-driven publishes on 2026-06-25 shipped IHT rates and property tax reform instead. The backlog had an opinion; the verifier had authority.
The voice-check agents passed two chapters. A three-line recount proved they couldn’t count.
An ultracode workflow’s voice-check agents passed two course chapters as within the em-dash budget. A three-line Python recount found both over. A model can’t reliably count its own tells.
The process changed. The docs didn’t. The fix was a folder.
Three times in the captainrandom repo, documentation drifted from actual practice. Each time the fix was the same: a dedicated folder with its own PRD, roadmap entry, and devlog trail — not a patch, but enclosure.
The back-nav went to Accounts. It should have gone to the calendar.
Weeks of Accounts build work passed SwiftUI preview on every check. The first hands-on device install found gaps the simulator had never reported. Round two found a navigation bug — Back from Day view landed in Accounts, not the calendar — that the simulator had never seen.
The spring panorama has winter underneath it: audio-driven temporal layering
Two panoramas, identical dimensions, stacked front-to-back. The spring layer carries a radial cutout whose radius is driven by bass amplitude. When bass hits, winter shows through. The background is not a backdrop — it is a temporal device indexed to audio rather than time.
The ultracode audit was clean. Google still hadn't found the pages. The fix was six in-body links.
Five legal pages and /stamp-duty-calculator were returning HTTP 200, index/follow, self-canonical — and still showing 'URL is unknown to Google' in Search Console. A multi-agent ultracode review ruled out every technical blocker. The answer was the link graph: footer-only pages don't get crawled.
Three words and an arrow: the learning spec's highest-value navigability fix
Seven phases of navigation work shipped across the learning hub. The spec rated P5 — a row of prerequisite chips at the chapter head — highest-value for navigability. It was the only phase designed to answer a question the reader didn’t know they had.
A hole in spring: the bass-masked radial portal that skipped the shader
Two panoramas at identical dimensions, one layered in front of the other. Cut a radial hole in the front layer, drive its radius with bass energy, and winter is visible through spring. No shaders, no post-processing pipeline — just winding-order arithmetic on a canvas clip path.
Bass as a window: the low end punches a hole in spring and shows you winter
Adding a winter panorama behind the spring layer did nothing until the spring canvas got a radial alpha hole driven by bass frequency. The moment that connection landed, the low end stopped being a brightness signal and became a literal window between seasons.
Four lines in a published article leaked my macOS username to the live site
PR #17 was a four-line diff across two files. One of those lines was in the body of a published article, rendering a real macOS username in the browser. The fix was substitution — but the pattern it revealed was wider: three surfaces in the same audit window, the same failure.
We captured thirteen GCP screenshots in one sitting. The OAuth console had changed while we weren't looking.
Chapter 2 of the GSC + GA4 automation course needed thirteen screenshots of the Google Cloud project setup flow. The session that produced them found that the OAuth consent screen had been redesigned since the prose was written — and showed why that kind of discovery only happens in a live browser, not a mockup.
Chapter 2 had thirteen screenshots and one OAuth rewrite. The three architecture decisions explain both.
The Google Cloud setup chapter of the GSC+GA4 automation guide forced three architectural calls before any screenshot was captured. Then Google redesigned the Auth Platform UI and the OAuth section had to be rewritten. This is what the decisions looked like at runtime.
Six YMYL pages were unknown to Google. The technical audit came back clean.
An ultracode review of five legal pages and the stamp duty calculator found no technical blockers — HTTP 200, correct directives, canonical self. The pages weren't stuck because of markup. They were stuck because Googlebot had no editorial reason to visit.
The first visible change to /learning/ arrived after the tests, the breadcrumb, and the JSON-LD
P2 of the learning-hub navigation was the first thing a visitor could see. By the time it shipped, the repo already had 37 passing fixture tests, a semantic breadcrumb, and a data model — none of it visible. The spec changed what shipped first, which changed what was easy to skip.
Bass amplitude into a radial alpha mask: audio reactivity as compositing, not particle spawning
Most audio visualisers route frequency data into particle spawn rates. Routing bass amplitude into a radial alpha mask between two panoramas is a different model — one where the music reveals a scene rather than decorating it. This is audio reactivity as a compositing problem.
I fixed four pipeline bugs in three days. The success rate was 20%.
On the morning of 2026-06-13, three articles were blocked or in trouble before 09:00. Working backwards from a 20% autonomous success rate across five slots revealed something individual fix sessions don't surface: every fix had left the adjacent failure mode intact.
I patched the same privacy leak four times. The lesson isn't to be more careful.
Private repo names in a language stats page. A macOS username in a published plist example. A missing privacy policy when the newsletter went live. Four separate fixes, each reactive, each after deployment. Four is enough to name the pattern: the publish boundary has no scrubber.
145 words shipped on inheritance tax. 949 words fixed it. The gate measured the wrong thing.
The operator-publish gate cleared 145 words on UK Inheritance Tax because word count was above minimum and the verifier passed. IHT is YMYL content. A same-day ultracode rescue to 949 words proved the gate needs a depth signal, not just a length floor.
Eight pillars published. AdSense said low value content. The bottleneck was never volume.
Closing the 8-pillar map on 23 June was the point at which Phase 1.6's publish-volume theory ran out of runway. The AdSense low-value-content gate was already in place; more articles hadn't moved it. The real bottleneck is internal-link density and topic-cluster cohesion.
The check-in button opened a transaction form: how three device-QA bugs made bb-finance's Home gamification navigable
The three Home gamification blocks in bb-finance — Quests, Level, Streak — were pulling live data and doing nothing else. Tapping the Streak check-in opened the transaction form instead. Three bugs from the AnF device-QA round forced Phase N, which built the detail screens and routing that turned the blocks from decoration into navigation.
The substitute pool stole from future slots because slots didn't exist as rows
Phase B's autonomous publishing pipeline sat at twenty percent success. The substitute pool was recursing through future-scheduled rows to breach — and nothing in the data model could stop it because slots had no identity. Making the slot a first-class entity with explicit state transitions turned six log events into six queryable rows.
Back from the day view landed on Accounts — the simulator had nothing to say about it
Phase O added a full-screen month calendar inside the Accounts tab, taking the navigation to three levels. Round-2 device QA found A8: tapping Back from a day view vaulted over the calendar entirely and returned to the Accounts screen. The simulator had approved the same flow without complaint.
Correct answers, wrong questions: four DVLAW scope errors in 72 hours
Between 2026-06-10 and 2026-06-13, four DVLAW bugs caused cascading slot failures, auto-published style violations, and a near-duplicate article. None threw an exception. All of them were answering the wrong question correctly.
The bass mask belongs on the front layer: that's what turns the background into a different season
Moving the radial bass mask from a dark overlay to the front panorama layer changes what the effect communicates. Previously the bass revealed texture through a luminosity window. Here it cuts a hole in spring to show winter behind it — a temporal portal, not an illumination effect.
The identifiers I didn't know I'd published
Five fixes across /learning, /now, a published article, the privacy policy page, and a global pre-commit hook — all tracing back to the same root cause. Not tracking, not data harvesting. Just private names in places I'd stopped looking.
Before the first word: three architecture decisions in an 8-chapter GSC and GA4 course
An 8-chapter guide to Google Search Console and GA4 automation forced three architecture decisions before any writing started: a throwaway GCP project per reader, Steps components over prose, and inline chapter links from day one. I made the first two immediately. The third I retrofitted.
Calendar → Day → Back Landed on Accounts. The Stack Was Correct; the Model Wasn't.
A8 in the round-2 device QA pass: tapping Back from the day view skipped the full-screen calendar and landed on Accounts. The NavigationStack had done nothing wrong. The navigation model had no concept of levels.
My canonical tags were deindexing my own articles. GA4 had been right all along.
GA4 showed 2 events per seven days across a site with 70+ articles. The first instinct was broken tracking. The real cause was canonical tags instructing Google to ignore the pages meant to rank — a discovery collapse that looks identical to a measurement failure until GSC gives it away.
The privacy policy was correct. The article body wasn't.
A production audit found the macOS username in a published plist example and private project names on two public pages. Neither would have been caught by a policy document. The fix was a pre-commit hook and an aggregation change — pipeline enforcement that runs whether the author remembers or not.
Five Phase B fixes in 48 hours. They were all patching the same missing type.
Twenty percent autonomous success across five scheduled slots. The same breach shape surfacing twice from different audit trails. Five fixes shipped in 48 hours — and when the sixth investigation opened, the pattern became unmistakable: the slot had never been an entity.
Not a fade, a life: energy decay and perpendicular wobble in a music visualiser
The onset particles were launching correctly and dying identically. Tying an exponential energy decay to the perpendicular wobble amplitude was the difference between particles that travel and particles that live.
One audit finding, eight chapters: closing content-5 with a hub instead of inline links
The SEO audit said to retrofit inline links across seventy articles. I built a guide hub instead, and the link-graph problem stopped being about retrofitting and started being about new direction.
The bass portal doubled as a scene swap. The hole geometry already owned the reveal.
A bass-driven radial mask opens a hole in a spring panorama to reveal winter behind it. When the track hits its midpoint, the same mask — scaled to frame-covering size — swaps the two scenes without a cut. The reveal mechanism was already running from frame one.
The diff check passed forever. That was the bug.
A test that checked whether verify.py was modified by a wiring PR became trivially true the moment PR #43 squash-merged. Phase X.8 replaced it with replay hashes, pre-registered gates, and hermetic sabotage cycles — tests designed to fail when they should.
Bass as a seasonal portal: routing the kick into a radial alpha mask
The captain-random visualiser stacks two panoramas: spring in front, winter behind. The spring layer carries a radial alpha mask whose radius tracks bass energy. When the kick hits, the mask opens and winter shows through; when the energy decays, spring closes back over it.
One PR, three game systems: why pure generators kept the ledger honest
Shipping streaks, quests, and levels as a single PR forced a specific architecture: pure generators with no side effects, persisted targets decoupled from quest logic, and celebration overlays computed after state transitions. This is the constraint that produced those decisions.
Real quests, broken buttons: how Phase C and N closed bb finance's behaviour loop
Phase C replaced mockQuests with a generator that reads real transaction history. Phase N found that the quest blocks were non-interactive labels and the streak's Check In button opened the wrong screen. Both sprints were necessary. Only together did they close the loop.
Spring became winter on the bass hit. That was the effect that mattered.
Three reactive effects were already live — bloom, particles, waveform — and the visualiser still read as a showcase. The portal changed that: a bass-driven radial mask cuts from spring into winter, and reactive opacity does something reactive motion cannot.
Six pages Google couldn't find: telemetry ruled out the technical before we touched any code
The GSC digest showed six pages stuck at “URL is unknown to Google.” That specific status — not crawled and filtered, just never discovered — changed what we looked for first. Two stdlib-only telemetry skills made that diagnostic distinction available before we opened a single file.
The accessibility gate that made stacking five motion effects into one PR survivable
Phase D of the Cursed Energy DNA landed sigil rotation, energy pulse, claim spark, level-up celebration, and streak milestones in a single PR alongside three other features. Accessibility review was a non-event — not because the PR was small, but because every motion effect owned its own gate.
The centre of the portal was flat. Removing the glow layer fixed it.
A spring-to-winter temporal portal with a halfway scene swap had one visual problem: the centre read as dull against everything around it. The instinct was to add a brighter overlay. The fix was removing the one already there.
The sabotage tests passed. The live cycle still found the bug.
Phase X.8 shipped with two hermetic sabotage cycles green. The first live run produced no article — not because the pipeline crashed, but because calculator_scores in ground.py was wrong in a way the fixtures had never exercised. Hermetic tests prove the logic path. Live harnesses prove the integration.
Five animations, five accessibility gates: what Cursed Energy Phase D cost to ship correctly
Phase D of the Cursed Energy design system added five animated effects to bb finance: sigil rotation, energy pulse, claim spark, level-up celebration, and streak milestone overlays. Every one is gated by the SwiftUI Reduce Motion environment key. Gating from the start rather than retrofitting later cost extra environment reads on every animated view. What it bought was a WCAG 2.1 AA clean bill before any user encountered the animations.
Phase 9 claimed self-driving overnight. The first five slots returned twenty percent.
Phase 9 described BBBrain's auto-pipeline as self-driving overnight delivery. Five scheduled slots later, the success rate was twenty percent — four cancellations, two architectural gaps in the publisher, and one duplicate that should never have queued. The rebuild was around explicit failure modes, not the happy path.
The publisher cancelled the 12:00 slot. My recovery logic called the publisher again.
Queue id=37 was scheduled for 12:00 BST on 13 June. schedule_sweep fired publisher at 11:00:08Z, publisher cancelled the row, and Fix B (the SLA recovery layer) called publisher again. Recovery logic without a model of who holds the lock cannot distinguish a concluded failure from a recoverable one.
Three effects, one envelope: how the bass signal became the nervous system of this visualiser
The neon bloom formula was written for one layer. Two days later, the same bass envelope was driving particle wobble and a radial time-portal. That convergence wasn't designed — it was pulled out by the kick pattern of a specific track.
Auto-mode opened the PR. It didn't merge it. That was gap one.
Phase 9's 'self-driving overnight' was overstated by two wiring gaps. A 09:00 BST live run on 11 June found both. The afternoon of fixes taught more about auto-mode's real edges than any pre-flight design session.
I had the Accounts calendar scoped as one level. Mid-design I deepened it to three.
Phase K shipped a horizontally-scrollable daily-activity strip on the Accounts screen. Phase O was meant to replace it with a calendar — I had a compact, single-level surface already scoped. Mid-design I deepened the ask to full-screen plus day mode, and a back-nav regression in round-2 device QA explains why that was the right call.
Phase B was hitting 20% autonomous success. The slot had never existed in the schema.
Five queue rows tried and rejected in three seconds. Three days of SLA fixes, substitute logic, and alarms, all patching around the same absence. The 20% pipeline success rate wasn't a scheduling problem — there was no schema entity for the thing being scheduled.
The Accounts screen took four iterations. Each one bought fidelity by spending scope.
Sprint 2 shipped a SwiftUI list with CRUD. Phase K rebuilt it into a module stack with a trend chart and activity strip. Phase O turned the strip into a three-level calendar flow. Round-2 device QA on AnF found the back-nav bug that came with it.
Eight cannibalisation pairs, one prefix strip, one PR
A numeric prefix in every calculator slug had created eight cannibalisation pairs — Google couldn't determine which URL was authoritative, so neither ranked with confidence. The fix required one atomic operation across content, routing, and the internal link graph: universal prefix strip, 23 permanent redirects, every MDX cross-link repointed. Not three PRs. One.
The 4-bit LLM hallucinated my Halifax statement. A band-based parser fixed it.
bb finance's Phase H spike wired a 4-bit quantised Llama 3.2 1B as the primary PDF extraction route. On real Halifax statements it hallucinated at a destructive rate — page 3 has 34 transactions; the model returned something else. PF-002 disabled the LLM path and replaced it with a deterministic band-based parser that lands 54 of 54.
The publishing queue ran at 20% success rate. Fixing it took three changes that only work together.
Four autonomous publishing slots expired before the pattern became clear. Sibling cancellation, slot fallback, and a missed-slot alarm are a set — without all three, the queue degrades silently.
20% in 72 hours: rewriting the slot scheduler's SLA recovery
Twenty percent autonomous success across five scheduled slots in 72 hours. The substitute pool was running but consuming the wrong inventory — draining future windows to cover present failures. Three mechanisms and a first-class slot state machine fixed it.
Four quest types. None of them read your transactions.
An iOS budget app shipped a gamification layer backed by a hardcoded mock array. Phase C replaced it with QuestGenerator — a pure function that derives up to four quests per day from real transaction history. What changed wasn’t the number of quests. It was whether the quests knew anything about the user.
I treated publication slots as timer events. Three days of SLA breaches say otherwise.
Between 11 and 13 June, the 09:00 BST publication slot fired and produced nothing three times in a row. Each incident had a different surface cause. All of them trace back to the same modelling mistake.
The pipeline was wrong before the model ever ran
PR #53 was the first article the X.8 editorial pipeline shipped since H-1/H-2/H-3 lifted, unblocked by a single bug fix in ground.py. Not a prompt change. Not a model upgrade. A function called calculator_scores had a bug, and the model had been synthesising from whatever it returned.
accessibilityReduceMotion should be a contract. Phase D made it one.
Phase D of an iOS budget app Cursed Energy feature adds five animated layers across sigil, energy, claim, level-up, and streak milestone surfaces. Every one checks accessibilityReduceMotion before firing — a hard gate, not a hint. The uniformity was a rule established on the first addition and held through to the fifth.
Four expired slots and the SLA recovery rule we learned in sequence
Between 12 and 14 June, four scheduled DVLAW slots expired without autonomous recovery. Each failure confirmed the same thing: sibling cancel, substitute fallback, and a missed-slot alarm don't work in isolation. They're one system.
The Accounts calendar strip passed every preview check. On my test iPhone, it answered the wrong question.
The Phase K Accounts screen shipped with a horizontally-scrollable calendar strip. Device QA on my test iPhone showed it was answering the wrong question, so mid-design I deepened Phase O to a three-level navigation flow: inline calendar, full-screen month view, day mode. SwiftUI preview had nothing to say about any of it — the back-nav regression that followed only surfaced under a real thumb.
The sibling-rehash rule: wired in the prompt, enforced in the filter
The picker generates three thesis variants per upgrade decision. Multiple upgrade taps on the same topic, same source material, different angles: the pipeline could queue two articles that were really one. Fixing that took two layers — one in the drafter prompt, one in the substitute filter — because prompts are probabilistic and filters are not.
25 PRs, a rescued BoE fetcher, and a composer taught restraint: the editorial pipeline ships
PR #53 merged on 12 June: the first article through the full X.8 grounded editorial pipeline. The morning sweep showed three gaps that had been quietly open — a stale PR that would have deleted the BoE rates fetcher, a composer inferring beyond its sources, and a design sprint that completed the day after. All three closed within 72 hours.
Five sprints inside one PR: what an iOS budget app's side-branch pattern reveals about tracking
PR #10 on an iOS budget app landed five complete features in a single merge. Each devlog entry reads as a self-contained sprint brief with its own type, its own boundary, its own completion signal. PR-level granularity and feature-level granularity are not the same number.
The dedup layer had three hash schemes. CDImportSource replaced them — and four banks validated with zero per-bank code.
The an iOS budget app import layer had three coexisting hash schemes with no shared contract between them. Collapsing them into CDImportSource — a unified source-attribution model — was the structural prerequisite for universal CSV and PDF parsers that validated across Monzo, Halifax, Revolut, and HSBC.
The S-4 cohort made two editorial pipeline decisions. I shipped them.
Two weeks of UK Calculators editorial pipeline sprints ended with two decisions that came from the cohort data, not preference: live Bank of England rate fetches and a constrained LLM composer. The pipeline silently stalled for four days after shipping.
Cursed Energy Phase C: wiring an iOS budget app's quest system to real transaction history
The an iOS budget app gamification layer had quest cards, a claim button, and a streak counter. None of it read from actual spending. Replacing a hardcoded array with a transaction-aware QuestGenerator turned decorative chrome into a feedback loop that surfaces real patterns.
Device QA found three regressions. The simulator had approved them all.
Round-2 device QA on bb finance flagged a navigation stack popping two levels instead of one, ProMotion locked at 60Hz on a 120Hz panel, and category rules over-reaching on shared word fragments. None of them showed in the simulator.
Eight calculators were cannibalising each other. One prefix strip fixed it in 72 hours.
A site-wide SEO audit against ukcalculator.uk surfaced 8 routes competing with themselves for the same search intent. The fix — strip the NN- prefix, issue 308 redirects across 23 routes, repoint every internal cross-link — had to land as a single atomic operation or it would introduce crawl noise between deployments. Three days later, Google had started indexing.
Five slots, twenty percent: the substitute pool had no editorial policy
Five scheduled publishing slots, four expirations, and a breach chain that exhausted six candidates in three seconds. The scheduler fired on time every run. The substitute pool was offering articles the drafter’s citation rules were going to refuse.
GOOGLE plus CLOUD matched SoundCloud. Why the fuzzy categoriser had to go.
Phase Q's fuzzy categoriser propagated category matches silently. Device QA on Phase R's part-based builder immediately surfaced an ANY-match failure: GOOGLE plus CLOUD matched SoundCloud. Version two adds exclusions and per-row overrides; the difference is that the user can now see what the matcher is doing.
Google ranks URLs, not intentions: the NN- prefix strip that resolved eight cannibalisation pairs on ukcalculator.uk
A site-wide SEO audit of ukcalculator.uk surfaced eight cannibalisation pairs before a single meta-tag was touched. The common thread: a numeric sort prefix on every affected slug, one that had never been intended as a ranking signal. Fixing the URLs outweighed everything else in Phase 1.
Five features, one PR: how an iOS budget app ships concurrent phase work without a merge queue
On 2026-06-14, five feat entries landed in an iOS budget app across Phases C, D, and E — all tagged 'same PR #10.' Treating commits as the concurrency primitive instead of branches let three phases advance in parallel without a merge queue or conflict chain.
I could have built this twice. ModuleOrderStore is why I didn't.
Making Home and Accounts modules user-rearrangeable could have meant two separate order stores, one per screen. ModuleOrderStore — a @MainActor singleton mirroring an existing PaletteStore pattern — is the reason it's one. The device QA round on AnF confirmed the ordering engine held; six other issues confirmed the QA was necessary.
One parser for Monzo, Halifax, Revolut and HSBC: how heuristic matching beat four bespoke importers
The on-device LLM for PDF extraction hallucinated on a real Halifax statement. That failure produced a band-based parser that hits 54/54 transactions. The same heuristic approach on the CSV side means one importer, four banks, zero per-bank code paths.
SoundCloud showed up under my Google Cloud rule on device. That's when part-based matching replaced the algorithm.
Phase Q shipped smart category propagation powered by a fuzzy matcher. Device QA of the Phase R match builder found the structural over-reach immediately: ANY-match on GOOGLE and CLOUD pulled SoundCloud in via 'cloud' and two Google One rows in via 'google'. Phase R v2 added exclude words and per-row select-all to close the gap — because a rule you can read is a rule you can fix.
An iOS budget app: five rounds of device QA found the same defect every time
Three of the Home screen's gamification blocks were non-interactive labels. The Streak block's Check In button opened the transaction form. Five device-QA sweeps — Phases L through Q — kept finding the same thing: the interface was describing things users expected to control.
Five features, one PR: the two shared-state promotions that had to come first
PR #10 for an iOS budget app bundled a palette picker, a motion layer, a streak milestone overlay, a category-budget quest, and a save-target preference into a single side branch. Getting them to co-exist required promoting BBPalette.current and UserProfileStore before the features could be wired.
Two correct cancellations, two SLA breaches, four manual ships
On 13 June, the 09:00 and 12:00 BST editorial slots both failed in the same day. G-L3 cancelled the queued articles correctly each time. Manual recovery covered every slot. The experience made the gap between row-level correctness and slot-level reliability impossible to ignore.
Four sprints shipped before the installer resolved to the right directory
The idea park soak cron installer used a directory convention copied from the article pipeline, correct in the article pipeline's layout and wrong in the idea park-soak's co-located layout. Two more post-merge fixes followed the same shape: a sanitiser wired into one code path but not the other, and a caller convention documented but never enforced.
Retries must be stateless, or the loop learns to cheat
The first live probe of the calibration gate against the BoE MMC article produced a 9,340-character draft that blocked correctly. Getting to a pass from there without teaching the loop to game its own gate took two days of doctrine.
The prompt that made Haiku hallucinate, every single time
Every iteration of the UK Calculators redraft loop produced the same failure: Haiku returning plausible-sounding figures with no real source. The fix wasn't better prompting. It was removing the question entirely.
Three newsletter hotfixes. The events table found all of them.
The newsletter form went live on 9 June. I subscribed from my own address and got nothing. No row, no email, no error. Three consecutive bugs followed, all found the same way: reading the events log, not the code.
No Composer, no autoloader: the newsletter backend cPanel rewarded
The deployment target was a cPanel shared host with FTP access. Composer wants SSH or a committed vendor directory. Neither fits cleanly on shared hosting. The alternative was require_once, six PHP classes, and a 190-line schema that doubles as documentation. Six sprints in, the backend is completely understood.
One osascript harness, two consumers — how skipping Archon kept the article pipeline stack composable
After the variant-picker shipped, the next call was architectural. Route thesis generation through Archon, or build a project-owned harness and own the primitive. The harness won. What emerged was a two-consumer pattern — thesis generation and voice rewriting off the same base — that Archon would have made awkward.
Scheduling articles through Telegram taught me that cron is the wrong model
The article pipeline pipeline could draft and voice-fix articles but had no scheduling layer. Adding one took six sprints, not because the cron was hard but because scheduling through Telegram needs to capture intent, not timestamps. The grammar parser, the 2×2 keyboard, and the 48-hour grace policy are the same design decision expressed three different ways.
Three articles cleared every gate. None of them cleared the style guide.
Phase 9 of the article pipeline auto-pipeline shipped on 2026-06-11. Within 24 hours I was manually patching three articles that had cleared every automated check but violated the style guide's em-dash budget on all three. The scrubber had the right rule. It was checking the wrong scope.
Every post-merge the idea park fix was the same fix
Provenance sanitisation, SCRIPT_DIR co-location, BB_CALLER lint — three fixes shipped after the idea park merged. They look like different problems. They’re not. All three are the audit trail recording what it could infer rather than what actually happened.
Stop retrying the prompt: what the brief ledger taught the image harness
Four sessions into generating hero illustrations, I was still fixing the same structural mistakes on different posts. Each bad render was a lesson. The retry loop wasn't keeping them.
Both voice iterations failed the Cash ISA test. I shipped the original.
Two iterations of the uk-calculator-voice skill (analytical v2.0.0 and warmth-corrected v2.1.0) both failed the same blind A/B on Cash ISA drafts. The original v1.0.0 won both rounds. The ship was v1.1.0: a reset to the passing baseline with the recovery path formalised, not a third iteration attempt.
The inbox filter model drops strangers silently. I built a queue instead.
Inbox filters drop what they don't recognise. I wanted a record instead: every unknown sender parked, every blocked arrival logged, nothing silently gone. Five tables, four sync branches, and a historyId cursor later, the editorial pipeline's Inbox Hub does exactly that.
Archon was burning quota. The fix was a hook, not a scheduler.
Three Archon runs in flight simultaneously, two of them fired without a deliberate decision. The instinct was to build a smarter scheduler. What worked was a global PreToolUse hook that intercepts archon workflow run and forces a pause before anything spawns.
Making Gmail tractable in a day. Four branches and one partial index.
The Inbox Hub shipped in five sprints, all on the same day. The classifier is four comparisons against a rules table. The speed comes from a partial index on pending senders, not from the sync logic. Neither changed how I read email.
Sender reputation first: why the Inbox Hub has no messages table
Building an inbox triage surface meant deciding what goes in the primary lookup table. The answer wasn't messages — it was senders. Five SQLite tables, a partial index on pending unknowns, and a 60-second badge later, the schema design is the product.
I built six sprints of article scheduling. For four of them, nothing fired.
Four sprints of Telegram button wiring, queue state machines, and free-text time parsing — all recording intent into `article_queue` without dispatching a single article. Sprint 5 was the first one that actually fired. Separating mechanism-to-record from mechanism-to-execute isn't a new idea; applying it deliberately at the sprint level is.
The schedule queue took four sprints to remember. One to act.
After four sprints of B-schedule, a scheduled article existed as a database row with a correct timestamp. No article had published. Sprint 5 added the sweep daemon and publisher_lock. That is when scheduling became real.
A missed slot at 09:00 should not silently vanish: building past-due grace into the article pipeline publisher
The `schedule_sweep` runs on a laptop. Laptops sleep. Without a grace policy, an article scheduled for 09:00 silently strands when the machine wakes at 09:07. Sprint 6 of B-schedule — past-due grace plus 48-hour hard-expire — turns that silent drop into a recoverable state with a reason, an audit trail, and a path back to publish.
The classifier is the product: how four branches replaced an inbox
Inbox Hub is five SQLite tables, twelve route handlers, four Shadow DOM components, and a launchd cron. Strip those away and what remains is a four-branch decision tree that runs on every message. Everything else is plumbing around that core.
monkeypatch lies to you when your tests run in subprocesses
The fixture looked correct. The patch was applied. The subprocess ignored all of it and wrote to the real registry. Three days of roadmap work taught me that monkeypatch only rebinds the parent process — env-var overrides are the only isolation that survives a fork.
One input to rule the dashboard: how the editorial pipeline's chat surface replaced three UIs
The editorial pipeline's chat interface started as a conversational wrapper. Two days of commits later it had absorbed the Archon SPA, the dashboard nav, and a 16-command palette. The unifying mechanism is simpler than it looks.
Ten AI-prose tells, one shell script, no more guessing
The em-dash density in my own published articles was running at four times the normal rate for edited prose. That was a measurable problem. So I made it measurable: codified ten AI-prose tells into STYLE-GUIDE.md and a pre-publish shell audit that catches them mechanically.
The backtick that could run anything: hardening AppleScript shell escaping
A Sprint 2.11 review flagged a shell command-substitution gap in a single Python helper. Backticks and dollar signs were passing through unescaped into a double-quoted `do script` string, meaning a crafted issue title could execute arbitrary code. The fix was four lines. Understanding why it mattered took longer.
Two assumptions the pipeline held until it shipped
B8b's first end-to-end Telegram ship-tap exposed two bugs that had been invisible in every prior test. Neither was logic. Both were assumptions the pipeline had been making silently since the day the code was written.
Telegram 409 Conflict: two bots, one token, infinite errors — why a bot registry is infrastructure
Retrofitting the editorial pipeline's Telegram modules revealed that the tunnel bot and the brain server had been polling the same token for weeks, throwing 409 conflicts that neither log named clearly. The fix wasn't a config tweak. It was accepting that one-bot-per-purpose needs to be enforced at registration time, not discovered at incident time.
Why the editorial pipeline's rewrite layer runs on a project-owned harness, not Archon
The Opus rewrite layer inside the editorial pipeline's de-AI-ish stack runs through a project-owned osascript Terminal harness, not a general orchestration framework. That choice was deliberate and narrow. Here's what it cost and what it bought.
Audits find what's wrong, not what's missing
A page-by-page audit ran for three days catching deprecated ARIA roles, fabricated copy, and missing hover-pause behaviour. Then I opened the site on my phone and the nav menu wasn't there. Not wrong. Just not there. That's a class of bug audits can't catch, and the fix isn't to audit harder.
macOS won't let your cron read your work: three days inside TCC
A daily launchd job that reads from /Volumes/b/ looked trivial. Three days, four wrong approaches, and one AppleScript .app bundle later, I'd learned why granting Full Disk Access to a shell script is a UI fiction.
An MDX content pipeline grows three kinds of complexity
Naïve MDX setups (one file in, one HTML out) hit three walls as they scale. Routing complexity. Authoring complexity. Citation complexity. Each grows non-linearly with post count; each has a single high-leverage fix.
I built this with Claude Code. The bugs weren't where I expected.
Three days into a page-by-page audit of my own site, I realised the bugs Claude Code had left behind weren't logic errors. They were hypotheses it had inferred from context, presented as facts. The reflex I needed wasn't suspicion. It was verification.
Static Next.js on cPanel in 2025
GitHub Actions to FTP to cPanel. Simpler than it sounds, with a few real gotchas worth documenting.





















































































































































