Build Receipt ⏱️ 6 min read

The Gallery Shipped — Every Post Now Moves

TL;DR

🎨 The plan: Replace the recycled animation library on every blog post with 4 curated animations per post, each one teaching that post’s actual claims
🌊 The build: 48 posts shipped via one-shot-orchestra — a 5-post pilot, then a 43-post scale-out across 4 parallel waves of fresh-spawn workers
🔍 The catch: A second auditor opened every post in real Chromium and caught a runtime bug that static screenshots had missed entirely — one fix later, all 48 ship clean
48 Posts · 4 Waves · ~192 Animations
Each tile = one post · Each color = one paradigm

📊 Build Receipt

The numbers behind the ship. Treat it as a receipt, not a brag.

MetricValue
Posts touched48
Animations shipped~192 (4 per post, paradigm-mixed)
Toolone-shot-orchestra v0.2.0
Pilot5 posts — verified clean by the pilot auditor
Scale-out43 posts — 4 waves of 10–12 parallel fresh-spawn workers (12 concurrent cap)
AuditAuditor-Scale spot-checked 10 posts; Auditor-Synth re-verified 10 unverified-synthesized builders in fresh Chromium
Bugs caught + fixed1 (Builder-Gallery10 — cellsLayout[-1] undefined)
Final pageerror count0 across all 48 posts, desktop & mobile
Code shipped+44,220 lines / −1,499 lines across 48 files

Why a receipt: max effort is meaningless without honest accounting. The audit caught a bug we would have shipped. The receipt says so out loud.

💥 What Was Broken

Every post had the same recycled animation library at the top. One canvas, the same shapes, the same loop — copy-pasted across the whole catalogue.

It worked the first time you saw it. By the third post it was furniture. The animations didn’t teach the post’s claims. They were decoration.

The thesis: if a post says “we deleted 400 lines of code,” the animation should show 400 lines being deleted — not a generic pulse loop.

🎯 The Plan: Four Animations Per Post, Five Paradigms

Each post would get 4 unique animations, picked specifically to teach that post’s claims. No animation would appear twice across the catalogue.

Five rendering paradigms in the mix, so the gallery as a whole would feel varied:

📜

GSAP ScrollTrigger

Scroll-paced reveals — the animation moves as the reader does. Best for narrative posts where the visual mirrors the argument.

🧊

Three.js (r160)

Real-time 3D — orbiting clusters, isometric scenes, rotating ribbons. Best for posts about systems with parallel actors or spatial structure.

🖼️

Canvas2D

Pixel-precise 2D — terminal replays, custom 7-segment LED counters, sequence diagrams. Best when the post is about exact text or numbers.

Generative particles

Emitters, flows, density fields. Best for posts about coordination, pressure, or volume — where the shape of the swarm is the point.

📐

Pure SVG / CSS

No JavaScript runtime. Best for static-friendly posts where the animation must work even if scripts fail to load.

Hard rules per post: single-file edit, namespaced CSS prefix, lazy-init via IntersectionObserver so animations don’t fire off-screen, a shared window.__rm__ reduced-motion helper that paints a static end-state for users who’ve disabled motion, and 375px-safe rendering for mobile.

Paradigm Distribution Across 48 Posts
Hover slices · The mix is the gallery

🌊 The Wave Strategy

One worker per post. 48 posts. The orchestrator caps concurrent workers at 12 to keep terminal windows manageable.

Five posts shipped first as a pilot to lock the rules. Then the remaining 43 went out in 4 waves of 10–12 workers running in parallel:

🎯 Pilot — 5 posts ship clean, audited end-to-end

🌊 Wave 1 — 11 builders fan out, each in its own terminal

🌊 Wave 2 — 11 more (some respawned after a session pause)

🌊 Wave 3 — 11 builders, mixed paradigms

🌊 Wave 4 — final 10 builders close the run

Audit — two independent auditors verify everything

Each builder gets a fresh ~1M context window. The orchestrator stays lean — it never builds, never researches, never scores. It writes briefs, watches results, and decides next steps.

Real-world analogy: a conductor of a 48-musician orchestra. The conductor doesn’t play. They split rehearsals into manageable groups, listen to each section, and note which bar needs another pass. The musicians never see the full score — just their part.

🔍 The Audit That Caught The Bug

When a worker’s terminal closes before it can write its result file, the orchestrator can synthesize one from on-disk evidence: source code modified, screenshots captured, animation infrastructure present, signoff in the chat log.

Sixteen of the 43 builders ended that way. Their work was on disk, the screenshots looked right, and the orchestrator wrote “synthesized” result files for each. The first auditor scored from that evidence and called it shipped.

Do

Open every page in a real browser. Listen for pageerror. Listen for console.error. Watch the network. Then call it shipped.

Don't

Trust a screenshot. A static frame can show a perfectly composed scene from an animation that threw on the second frame and silently died.

A second auditor — Auditor-Synth — was spawned for exactly this reason. Its job was to open each of the 10 unverified synthesized posts in fresh Chromium with page.on('pageerror') wired up.

Nine passed cleanly. One didn’t. Builder-Gallery10’s incremental-shutter animation threw TypeError: Cannot read properties of undefined (reading 'x') on both desktop and mobile. The screenshots had looked fine.

The Listener That Heard The Wrong Note
Static screenshot → clean. Live listener → two errors.

🐛 The Bug, The Real Root Cause

The respawned builder reproduced it on the first frame. Then it found the cause — and it was not what the brief had suspected.

The brief blamed an async race: cellsLayout getting accessed before its grid coordinates were populated. That guess was wrong. cellsLayout always had its 10 entries.

The real cause was Chrome’s clock. The animation set last = performance.now() just before calling requestAnimationFrame, then computed dt = (now - last)/1000 inside the callback. Those two clocks — performance.now() outside, the rAF DOMHighResTimeStamp inside — can disagree by a few microseconds because of timer coarsening.

That made dt briefly negative on the very first frame — about −22 microseconds. Negative dt made the time accumulator go negative. Math.floor on a small negative number returns −1. The clamp Math.min(NUM-1, -1) kept −1 because −1 is less than NUM-1. The lookup cellsLayout[-1] returned undefined. Reading .x on undefined threw.

// before
var dt = (now - last) / 1000;
var idx = Math.min(NUM - 1, Math.floor((cycleT / SWEEP) * NUM));
ctx.fillRect(cellsLayout[idx].x, ...);  // throws on frame 1

// after
var dt = Math.max(0, Math.min(0.05, (now - last) / 1000));
var cycleT = ((shutterT % SWEEP) + SWEEP) % SWEEP;
var idx = Math.max(0, Math.min(NUM - 1, Math.floor((cycleT / SWEEP) * NUM)));

Three fixes, defense-in-depth: clamp dt non-negative, wrap cycleT non-negative, clamp idx non-negative.

Core insight: a synchronous throw inside a requestAnimationFrame callback only surfaces as a pageerror, not as a console.error. An audit that listens to console alone returns clean while the page is silently dead.

🔧 Preventive Sweep

One bug like that means there might be others. The pattern to grep for: Math.min(N-1, Math.floor(...)) with no Math.max(0, ...) guard, where the inner expression could go negative.

Sixteen hits across the catalogue. Each one inspected by hand. Two were already guarded. The other fourteen were safe by construction — either the input was a scroll progress clamped 0–1 by the framework, or the result was used as display text rather than an array index, or a loop bound made the negative value harmless.

No other post was actually vulnerable. Two artifacts left in the workspace so the next builder doesn’t reintroduce the pattern:

Pattern Sweep — 16 Matches, 1 Real Risk
Greppable patterns lie. Read the call site.

💡 What I’d Do Differently

The lesson isn’t “synthesized result files are bad.” They’re a reasonable fallback when a worker terminal closes mid-run. The lesson is “synthesized is not verified, and you can’t paper over that with screenshots.”

Next time, every builder runs the check-pageerrors.mjs probe before signoff. If the probe is non-zero, the result file gets stamped fail regardless of how good the screenshots look. The fresh-Chrome listener becomes part of the build, not an audit afterthought.

Run One-Shot Orchestra On Your Next Multi-File Build

Lean orchestrator, fresh-spawn workers, parallel waves. Ships only when the audit’s listener stays quiet.

Get Godmode Read The Gallery Plan