Email me

Updates

What changed recently

Short progress summaries across all my projects, newest first. The detail lives in each project's articles.

Updates are bullet-point summaries generated with AI from my commits and build notes, then checked by me. Project articles and learning notes are written by me.

  1. · Automating Garage Dreams AI summary

    • The site I was verifying was not the site that is deployed. garagedreams.net is still parked at the registrar, answering 410 from someone else’s server with the registrar’s nameservers; the Pages project deploys to garagedreams.pages.dev and always had. Three documents said “Cloudflare is wired”, which is true of the project and false of the domain.
    • A draft: true post 404s in production, and drafts are absent from the sitemap and the feed. The whole burn-in safety argument rests on that and it had never been checked. The first pass reported a false positive: /<slug>/ 404s for everything, live guides included, because the route is /blog/<slug>/. A 404 test with no live control proves nothing.
    • public/robots.txt had been pointing at jdmgems.com/sitemap-index.xml since the fork, so the real sitemap was never advertised. A static asset has no schema and no test, so the build gate cannot catch this class of bug at all. Only looking at the deployed site finds it.
    • --dry-run returns before the build gate, and the harvest driver passes --dry-run on every run. So astro build had never once run against a model-generated article, across five batches and sixty-plus drafts. Every “published” count in the evidence that set the originality thresholds is an upper bound.
    • Re-analysed all five archived batches directly instead of trusting their summaries. Claude has passed 58 of 58 Gemma 4 articles. On the shipping model its verdict has never changed an outcome, and the two-reviewer gate is functionally “Codex must pass”. It is not a rubber stamp in general: it rejected 9 of 14 drafts from the weaker model.
    • Codex’s mean score is higher than Claude’s while Codex is the one demanding revisions. A threshold fitted to the numeric score would have been fitted to the wrong signal entirely.
    • The code’s own docstring denied a real bug. A failed push leaves the commit in place, so the tree is clean and every precondition passes, which is how a job stacks unpushed commits night after night while looking healthy. The comment claimed the next run’s clean-tree check catches it. Fixed with an explicit upstream comparison, made deliberately asymmetric so that being merely behind the other peer does not abort the night.
    Active
  2. · Automating Garage Dreams AI summary

    • Built the originality gate in three deliberate stages: record the draft on every outcome, then measure and enforce nothing for three batches, then arm it. Shipping a threshold before there were numbers to set it from would have repeated the dead-knob bug from the 12th.
    • The measurement could not have been taken at all. The run log only stored prose on rejection, so the batch where the 26B published 20 of 20 left twenty-one records containing no article text whatsoever. The one batch that most needed examining had thrown away its evidence.
    • Three models, 60 drafts, the same 20 sections. Gemma 4 26B copies steadily: median 29% five-gram share, worst case 72%, and that worst case is a straight reflow of a bulleted list into paragraphs that both reviewers passed, because every claim in it is perfectly supported. Gemma 3 4B is bimodal: best median at 15%, and two runs that echoed the source almost whole, 95% and 99%, one with a 577-word unbroken verbatim run.
    • Those two catastrophic runs were caught only because a long source pushed them over the word limit. A shorter section would have produced a verbatim copy inside the cap and both reviewers would have passed it. The only thing between total transcription and publication was a rule that happened to correlate.
    • The plan predicted the small model would be unpublishable, and it inverted. Gemma 4 E4B, the only one of the three that fits an 8 GB Pi, copies less than the 26B, has no catastrophic tail at all, and runs three times faster. Copying is closer to what a capable model does when told to stay faithful to a source than to what a weak one does.
    • Armed at a 30-word verbatim run and a 45% five-gram share, plus a separate fatal rule at 90% that skips rather than revising. Overlap fired in 3 of 20 runs and revision cleared it 3 for 3. The live risk was a fourth rule the model could never satisfy, and that is now retired with evidence rather than hope.
    Active
  3. · Automating Garage Dreams AI summary

    • MAX_REVIEW_REVISIONS did nothing: the verdict function still compared against a literal 1. It now takes the budget as a parameter, with tests at 0, 1 and 2 rounds.
    • Qwen3.6 35B-A3B timed out after 241 s because thinking is on by default. Switched the size experiment to Gemma 4, and noted that its <|think|> token isn’t stripped by the <think> filter.
    • Descriptions were cut off at the first “z”: JavaScript has no \Z anchor, so with the i flag it matched a literal z. Fixed the regex, with a test fixture using car names that contain z.
    • Read the six drafts the reviewers split on. Codex was right every time, so the plan to loosen it was dropped, along with publishing on a single pass.
    • Across three batches (62 runs) the publish count moved 2, 3, 1, which is noise. Lint failures by rule are the useful metric; description failures went from 7 to 0 after the parser fix.
    • Gemma 4 26B-A4B published 20 of 20, but shared 41% of its five-word sequences with the source guide. The next step is a deterministic cap on verbatim overlap.
    • See What the review gate taught me.
    Active
  4. · Self-balancing robot AI summary

    • Confirmed the MPU6050 interrupt is on PA12, not PB12. EXTI line 12 is shared across ports and the port is chosen in GPIO_EXTILineConfig. PB12 is the PS2 controller’s chip select. Cross-checked by extracting the netlist from the board schematic PDF.
    • Withdrew a finding that the 1300 motor deadzone needed rescaling. The vendored reference uses the same 2880 timer period, so the deadzone and the PID gains carry over directly.
    • Capped the FIFO drain loop at 3 reads (about 2.5 ms) instead of 32 (about 27 ms on the bit-banged bus). Past the cap it zeroes PWM, resets the FIFO and counts the event.
    • Found that a failed IMU read leaves the last PWM command latched on the motors, with nothing expiring it. A stale-data motor cutoff has to land before the gains go up.
    • Added hysteresis to the tilt fault: it trips at 40° and clears at 10°, so the loop only restarts near upright.
    Active
  5. · Automating Garage Dreams AI summary

    • Dry runs didn’t consume a section and didn’t write a run log, so twenty test runs measured one section and kept nothing. Dry runs now write to a gitignored harvest folder, and a driver walks the real selection order.
    • First baseline over 22 runs: 2 published, 5 died before review. Claude passed 47% and Codex 18%, and neither ever rejected outright. Most objections were claims the source doesn’t support.
    • Raised the minimum source section from 200 to 400 words. Eligible sections fell from 1,413 to 438, and the runway from about 3.9 years to 1.2.
    • See The 400-word source floor.
    Active
  6. · Where the Action Actually Happens at F1 GPs AI summary

    • The one-year circuit can’t fail a denominator test, since dividing by one changes nothing. Only the three-year circuit now counts as evidence for averaging checks.
    • Replaced the comparison script. It never compared race_phases, sorted lists the code deliberately orders, and keyed grandstands by the non-unique code. The new one compares every field and requires accepted differences to be declared.
    • Froze the old pipeline’s output in baseline/, pinned to a commit, so validation doesn’t end up comparing the database with itself after cutover. Widened validation from 195 to 525 keys and confirmed the checks can fail.
    • Resolved a suspected unzoned incident (zones 7/7/7 against a total of 8). The zones overlap, so nothing was wrong.
    • Added a source_incidents table: the circuit summary counts spreadsheet rows, not row and section pairs, and one 2023 row with no section could not be recovered from incidents.
    • See Five verification failures.
    Active
  7. · Self-balancing robot AI summary

    • Worked out the real control loop period from the telemetry log: about 12 ms against a nominal 5 ms, from 23-sample blocks of repeated angles overflowing a 32-packet FIFO.
    • Blocking serial output cost about 3.47 ms per 40-character line at 115200 baud, roughly 70% of a 5 ms tick. Printing one tick in twenty brought that down to about 3%.
    • Switched the UART wait from TC to TXE because it is the correct flag. Measured no speedup, since the baud rate is the limit.
    • Confirmed the feedback sign was already correct. The suspected inversion came from describing wheel rotation without recording which side of the car it was viewed from.
    • See Prove the motor mapping and feedback sign.
    Active
  8. · Automating Garage Dreams AI summary

    • The NZ spelling rule /colou?red/ matched the correct “coloured” and told the revision loop to change it to itself. Split the patterns and added a test that every suggestion passes the linter, which found five more broken rules.
    • A run that published nothing left its log uncommitted, and the dirty-tree check would then have blocked every later run. Every outcome now writes and commits its log, except the dirty-tree abort itself.
    • The revision prompt asked for the smallest possible edit, so a 718-word draft came back unchanged. The prompt now branches: too long means delete sentences or a section, too short means expand from the source only.
    Active
  9. · Where the Action Actually Happens at F1 GPs AI summary

    • Rewrote the global rankings as three SQL queries plus a Python pass. 31 corners and 14 straights matched the old output exactly. The only grandstand differences were nine floating point artefacts the old code had been shipping, such as 14.399999999999999.
    • ORDER BY g.grandstand_id, gv.rowid reproduced the old list ordering with zero differences across 29 grandstands.
    • Grandstand lookups were keyed by code, which is only unique per circuit. Forcing a collision merged two grandstands (28 instead of 29). Now keyed by grandstand_id, changed in a single commit, because the halfway state deleted a grandstand instead.
    • The circuit page, still on the old pipeline, listed two spreadsheet typos as straights that the new rankings page correctly dropped. Fixed the typos in the source workbook rather than rushing the migration.
    • Those typos were cells showing 9-10 that held dates under a d\-m number format. Set them to strings with Text format; database validation went to 0 mismatches.
    • See Migrating by predicted diffs.
    Active
  10. · Automating Garage Dreams AI summary

    • The plan said “split each guide on ##”. 71 of the 85 guides contained no ## at all, and had looked perfect in a browser since the day they were converted: Word’s heading styles had been flattened to bold paragraphs, and four guides from the old WordPress site carried raw <h2> HTML that Markdown passes straight through. Fixed the corpus rather than teaching the pipeline to recognise bold-as-heading.
    • 4,100 bold pseudo-headings promoted on a four-condition heuristic (entirely bold, standalone, no terminal punctuation, 14 words or fewer), each condition tuned against counts from the actual corpus. The no-terminal-punctuation rule alone excluded 630 emphasised sentences. The script asserts the stripped-tag prose is byte-identical before and after, and caught nothing on the real run, which is the point.
    • I had made the same mistake in the opposite direction an hour earlier: grepped for Markdown pipe tables, found none, and reported that the conversion had lost the spec tables. 75 of 85 files had them as raw HTML. Grepping source syntax tells you nothing about the rendered page.
    • Adding a generated cover failed the build on missing alt text, which revealed that a fallback written minutes earlier, to synthesise alt text for exactly that case, could never run. The schema already forbade the state it existed to handle. Deleted it: a fallback that cannot fire is worse than none, because it reads as a guarantee.
    • Two of the 85 “guides” were not guides. One was a classified ad for a Volvo 940 that came with two rims and some seat material. Everything in the source folder had been converted indiscriminately.
    • Found a guide titled as an EP3 Civic Type R whose body is entirely about the EK9, down to the part numbers. Flagged, not fixed, but it matters, because the pipeline derives articles from these, so a wrong guide becomes wrong articles. Nobody has ever proofread the 1.01 million words.
    Active