Audit item 14.
THE DEPLOY VERIFIED THE ONE URL A PLAYER NEVER LOADS. Every check in deploy.sh appends
?v=$BUST — which is exactly what makes it bypass Cloudflare's edge — so all six could pass
green while every actual player was still being served the previous build from a POP. The
script's answer was a line of prose at the end asking a human to remember to purge. It now
fetches js/boot.js the way a browser does, with no query string, and byte-compares it to what
was just shipped; a stale edge fails the deploy and prints the purge steps. No credentials
needed, and it asks the question the prose was only gesturing at.
This is the same mistake as the `assets` bug and the unwinnable saliva tide: verifying the
mechanism rather than the path the player actually takes through it. Third time, so it is
written into the tool this time rather than into a comment.
QUALITY SWITCH. renderer.setPixelRatio was hardcoded to min(dpr, 2), so a retina laptop drew
four times the pixels of a 1x one with no way for its owner to decline — and this game is
fill-bound, not geometry-bound (fogged tube, additive glows, a full-screen damage layer), so
that is the lever that moves. `performance` caps it at 1. On the title beside difficulty,
saved, same reload path (the pixel ratio is set at boot, before any card exists).
save.js's two one-line setters collapsed into setOpt(key, value) now that there are two options
and a third is obvious.
Note on verification: the chip persists and boot reads it, confirmed live. The resolution
change itself is not observable in the harness — that browser pane runs at devicePixelRatio 1,
where min(1,1) and min(1,2) are the same number.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit items 9, 10 and 11 — three features whose backends were finished and whose front ends did
not exist.
DIFFICULTY WAS UNREACHABLE. levels/difficulty.js has carried three fully specified tiers since
round 2 — Endoscopy, Clinical, Terminal, with per-tier assists and a note explaining each — and
every single call site in the codebase took the default. `easy` and `hard` were a table nobody
could select. getLevel now takes the saved difficulty (?diff= still wins, so a test URL stays
absolute), and the title has the switch. Verified live: the world boots at 11 spawns on Terminal
against 8 on Clinical.
THE CAMPAIGN WAS SIX LEVELS WITH ONE WAY IN. Reaching anything but L1 meant hand-editing ?lvl=
in the address bar. The title now lists the campaign, unlocking each segment as the one before
it is filed, showing the medal you hold on each. `LS_` is C's secret prefix, so the appendix
stays a row of question marks until you have earned your way in — naming it in the menu would
have given away the one thing in this game you have to discover.
THE CAMPAIGN HAD NO ENDING. C's anal_verge gate carries `to: null` — the only gate in the game
that does, its own note calling it "the end of the campaign" — and boot's ui:continue handler
opened with `if (!e.to) return`. Finishing GUTS parked you on a medal card forever with no way
out but the address bar, and the card said "any key", the same words it uses when it has nothing
to tell you. Now the clear is filed and counted, the card says you are out, and the title carries
CANAL CLEARED.
New core/save.js is the whole persistence layer: one localStorage key, one flat object, best
time / best score / best medal kept per column (they need not come from the same run) plus an
attempt count. Every entry point is wrapped — Safari private mode throws on setItem, and a save
file must never be able to stop the game booting. Its selfcheck (`node web/js/core/save.js
--selfcheck`, now in qa.sh) covers the best-of comparisons and feeds it corrupt, future-versioned
and malformed saves.
cards.js records rather than boot: this file computes the grade, so routing it through the bus
for boot to re-derive would be two copies of the medal table waiting to disagree.
One bug found by clicking rather than reading: the first cut of the chips had no
pointer-events:auto. #ui is pointer-events:none and it inherits — the file's own header says so
— so the buttons rendered, highlighted nothing, and passed the click through to the canvas,
which on the title starts the run. It looked like a menu and behaved like wallpaper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit items 4 and 5. Both are shell behaviour a finished game is expected to have and this one
never did.
THE RUN NOW STARTS WHEN YOU START IT. cards.js has emitted `ui:start` on title dismissal since
round 1 and its own comment says nothing consumes it — so the simulation ran behind the title
card. Measured: 1.2 s of sitting on the title moved the ship and burned 1.2 s off the par clock
before the player had touched anything. L1's first checkpoint (s=20) and the opening comms line
both fired while the title was still up, and a hazard could hurt you before you started, which is
why cards.js already carries a player:damage auto-dismiss as a safety valve. Now `started` gates
step(); verified s stays at 2 across a second of real frames.
PAUSE ON BLUR. The ui:pause consumer has worked since round 2 and nothing but the pause key ever
produced for it. Alt-tab mid-fight and you came back to a dead ship. blur + visibilitychange now
produce it.
AUDIO HEARS THE PAUSE. engine.js handled eleven bus events and not that one, so the bed, drone,
grain scheduler and heartbeat ran at full level behind the pause card, and the heart slewed to
its resting 50 bpm while you read and snapped back on resume. Ducks the master rather than
suspending the context — the heartbeat's pump schedules against ctx.currentTime and would wake
up in the past.
FAILURE HAS A FACE. Every failure mode in this game presented as the same black rectangle, so a
player could not tell "loading" from "crashed" and had nothing to report:
- loadLevel did `return mod.getLevel(id)` without awaiting, so a rejection escaped its own
try/catch and killed module evaluation. A mistyped ?lvl= now falls back to the stub tube
(which names itself STUB ESOPHAGUS) instead of a dead page.
- frame() re-armed its rAF at the BOTTOM, so one throw anywhere stopped the loop forever while
the canvas kept showing the last good frame. Body is guarded; the loop always re-arms.
- bus.emit ran listeners bare, so a throw in one subscriber aborted the rest of the emit — and
player:state is emitted every frame from inside step(), so one bad frame in the HUD took
combat and audio down with it. Listeners are isolated now.
- webglcontextlost had no handler at all: a laptop sleeping left a white page with a floating
HUD.
All four now land on a FEED LOST card naming the cause, in the game's own voice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The round-2 microbe pass, vertical slice. Cross-lane (integrator call): the point was one
thread proven end to end — art pipeline in, friend and foe out.
- core/assets.js (D): the GLB loader, finally wired. GLTFLoader was vendored but never
imported and the manifest had 0 models, so enemies.js's `glb?.geometry` hook was dead code.
Now every manifest model preloads (AWAITED — pools set geometry at construction, a late load
can't retrofit an InstancedMesh) into ONE BufferGeometry normalized to a unit bounding
sphere; get('models',n).geometry lights up the hook. Optional-asset law intact: any failure
-> no geometry -> procedural fallback.
- combat/enemies.js + balance.js (B): two new archetypes.
flora — the FIRST FRIEND. Cyan (ART_BIBLE: flora reads friendly). Drifts like a
floater but its aura HEALS (sheds coat), never harms. Shootable — and shooting
it is the reputation trap: kill() gives no score, no combo, emits flora:harmed.
Verified: the aura fires player.refill({coat}); a shot flora scores 0.
spore_pod — a foe yeast cluster: the floater's drift+aura, but its OWN pool so its blobby
GLB will not reskin the food-debris floater.
Pool geometry now scales a GLB by the archetype radius (GLBs ship unit-normalized).
- levels/enemies.js (C): ARCHETYPES += flora, spore_pod; catalogue entries lacto_drifter
(Lactobacillus) + yeast_pod (Candida). L2 gets a light teach beat in the calm opening — a
friendly reef at s180, one yeast at s300, both off the racing line.
Ships playable as glowing primitives (a capsule + an icosahedron), exactly like the rest of
the round-1 roster; the Trellis/Hunyuan meshes drop in via the manifest with no code change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Textures (6 walls + normals + wet matcap, $0, ~64s on the m3ultra MPS):
- FLUX ignores "seamless tiling" (seam_error 3.7-17.9); a 4-way cosine
partition-of-unity blend of the four half-rolls fixes it exactly -> 0.87-1.32
- levels-normalised the pack to one exposure (means were 0.18-0.32); these are
detail maps multiplied into a biome tint, so per-prompt exposure read as a
broken tint rather than as dark tissue
- prompt-kit experiment rejected + recorded: PROCITY's "uniform coverage" clause
in the STEM cured centred subjects but flattened the pack; framing words now
live only in the two subject prompts that needed them
Audio (1 bed + 4 sfx, 4.2s render, 0.61/10MB, ogg+m4a dual-ship):
- bed-esophagus 24.000s, loop seam 0.32 (wrap step 3x smaller than inner step)
- cascaded lowpass poles: one-pole at 900Hz left 16kHz only ~25dB down = hiss
- rewrote the spectrogram sheet (per-clip peak, log freq) — the first one was a
saturated rectangle, and evidence you can't read is not evidence
assets.js: miss ledger + misses() — Lane A found that a drifted slug falls back
procedurally forever with no error. Each distinct miss now announces itself.
Also: shot_sink.py (canvas -> docs/shots/laneD, correctly named) and a dev
texture viewer that imports the stub world read-only for the eyeball law.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>