glytch/fktry/lanes/LANE-SCREEN.md
type-two 32ff942fd5 [screen] round 6: repairs ease the ledger + green-tick placards; hospital-white mite sterility creep
- repaired events decrement the ledger by one unit of the item (theft, not
  boredom-decay) and stamp a small perfect green-tick placard — The Correction
  signs its work; kept distinct from the crooked chip-coloured shipment stamp
- parity-mite activity grows a hospital-white sterile patch via growthMask
  (its 3rd/4th customer): desaturate + lift to clinical white + faint tile grid,
  the inverse of corruption and meant to feel worse
- DEV hook: .mite() .repair() plus .frame()/.canvas() pixel-verification drivers
- +3 repair-semantics tests (screen suite 33); order-3 STANDARD lines are DATA's
  copy, my chyron wraps them (verified live, no clipping)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 18:34:32 +10:00

587 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LANE-SCREEN — THE SCREEN compositor & glitch shaders
You are an Opus 4.8 executor on the SCREEN lane of FKTRY. Read `../MASTERPLAN.md` and
`../CONTRACTS.md` first; world bible `../../docs/FKTRY_LORE.md` (§1 THE SCREEN, §3
products, §8 style). Also read `../../index.html` — the original WIMVEE GLYTCH
prototype — its fragment shader is your seed stock: 8 glitch params (mosh, melt,
chroma, tear, roll, burn, noise, freeze) already written and tuned.
**Mission:** the payoff object. THE SCREEN is the sky-display everything ships to —
the player's factory output composited live as escalating video corruption. When
someone screenshots this game, they screenshot your canvas. It must start sterile and
end deranged, and every shipped item must visibly matter.
**Owned paths:** `src/screen/**`.
**Never touch:** contracts, main.ts, other lanes, the GLYTCH prototype (read-only
seed stock — port, don't move).
**Standing rules (all rounds):**
- WebGL (raw or three.js — your call, note it) on the provided 640×360 canvas.
Budget: <2ms/frame; THE SCREEN is garnish, not the meal.
- Corruption must be *legible*: a player who ships 40 chroma slurry should SEE chroma
damage specifically. Item artifact mapping is the design core, keep it data-driven
off `ItemDef.id`.
- Base content starts as procedural "clean programming" sterile SMPTE-ish test
patterns, slow pans, timestamp overlay (port `makeBaseTexture` thinking from the
prototype; animate it subtly so corruption has motion to chew on).
---
## CURRENT ORDERS — Round 6 (goal: the sky notices the enemy)
Your chyron now wraps (orchestrator fix 29/30 STANDARD lines were clipping at ~51
chars since round 5; two-line wrap verified live).
1. **`repaired` events on the sky** (SIM lands them): each repair eases total
corruption by its item's contribution AND stamps a small, perfect, green-tick
placard The Correction signs its work. Boredom and repairs must stay
distinguishable (rot vs theft).
2. **Mite shimmer**: reuse the growth-mask for a faint hospital-white sterile patch
creeping around heavy mite activity the inverse of corruption, and it should
feel WORSE.
3. **THE STANDARD, new pool**: coordinate with DATA the propaganda acknowledges
the mites now ("Units are assisting you." / "Inventory discrepancies are being
resolved in your favor."). Chyron-only, mumble untouched (standing constraint).
**DoD:** a mite wave is visible on the sky as creeping cleanliness + tick stamps;
pixel-verified; budget holds.
---
## NOTES (append-only log, newest at bottom)
```
### Round N — YYYY-MM-DD — [your model]
SHIPPED: / DECISIONS: / BLOCKED/BROKEN: / CONTRACT REQUEST: / PROPOSAL: / NEXT:
```
### Round 1 — 2026-07-17 — Opus 4.8
SHIPPED: all 5 orders done and verified on the dev build.
- `params.ts` the 8 params + frame-rate-independent `ease()`.
- `glitchStack.ts` GLYTCH shader stack ported. Raw WebGL, NOT three.js (order 1 said my
call): it's one fullscreen triangle + one texture; three.js would cost more than the
whole effect is allowed to. All 8 prototype params keep their tuning verbatim I
changed no magic numbers. Added 3 uniforms of my own: `u_drift`, `u_brownout`, `u_flash`.
- `baseTexture.ts` procedural clean programming: SMPTE bars + castellations + pluge/ramp
+ camcorder overlay (PLAY ▶, running SP timecode, rec dot), widened to 16:9.
- `corruptionMap.ts` the ledger + itemartifact table + escalation curve.
- `index.ts` compositor: ledger, easing, wobble, brownout, awakening.
- `corruptionMap.test.ts` 24 tests, all green.
DECISIONS:
- **Mix vs. total are separate axes.** WHAT you ship picks the artifact (blend of per-item
weights, by ship share); HOW MUCH you've ever shipped picks the intensity (escalation).
This is what makes 40 chroma slurry read as *chroma* instead of "more glitch".
- Escalation is `log1p(units/6)/log1p(100/6)`, so 10 ships 34% corruption and 100 100%.
Front-loaded hard, per orders there's a test pinning early-vs-late slope.
- Base texture repaints only when a visible glyph changes (timecode ~1/sec), not per frame.
Re-uploading 640×360 every frame would have eaten the budget for nothing. Motion for the
corruption to chew on comes from `u_drift` (bounded sinusoid deliberately not a wrapping
pan, which would show a seam: the texture is non-power-of-two so WebGL1 forces CLAMP_TO_EDGE).
- Wobble above 0.8 total is CPU-side, 8 distinct slow rates, ±0.12 riding on top of the eased
value and never feeding back into the ledger.
- **I extended the item table well past the orders' examples.** LANE-DATA landed the full
40-item codex mid-round, so I mapped every §2 resource, §3 product and §5 wildlife to a
signature (judder-camsfreeze, static-canisternoise, mosquito-swarmnoise, explosive-hits
mosh+burn+chroma, gamut paints graded rec709rec2020, chroma subsampling graded
420>422>slurry). The 5 science packs are intentionally left on the generic-noise fallback —
they're research currency, not ship goods. Unknown ids still degrade gracefully; there's a
test that walks `data/items.json` and asserts every id yields sane params, so LANE-DATA can
keep adding content without me.
VERIFIED (dev build, driving the real module on the real canvas, reading GL pixels — not vibes):
- Starts sterile: bars + "NOTHING IS WRONG" chyron, `idle:true`, all params 0.
- 40× chroma-slurry → `chroma 0.684`, every other param 0 except a 0.082 burn trace. Screenshot
shows channel-split fringing on bars and doubled glyphs. Item-specific: confirmed.
- Awakening: first-ever ship renders exactly ONE frame at mean luminance 255 (full white),
79 the next frame.
- Mature: 120× melt → total 1.0, melt 1.0 / mosh 0.451; screenshot shows the bars dripping
into black. Above 0.8, params drift ~0.095 between frames — alive, not maxed-out static.
- Anchor-slab isolation test: 1× anchor sets `idle:false` with ZERO corruption, and the only
pixel change is the chyron band going 37.1→100.8 luminance (dark overlay removed, bars
revealed). Chyron logic and sanitation both confirmed in one shot.
- Brownout: 1.0 when on, 0.0 when lifted, freeze+dropout+judder all driven off it.
- **Frame budget: 0.0040.013 ms/frame CPU** (rolling avg, `__fktryScreen.stats().msPerFrame`).
Budget was <2ms. GPU side is one fullscreen triangle, no readbacks in the hot path.
BLOCKED/BROKEN: nothing blocking me. Two things worth knowing:
- LANE-SIM emits no `shipped` events yet, so THE SCREEN can't be driven by the real chain.
I shipped a debug hook instead: `__fktryScreen.ship('melt',40)`, `.brownout(true)`,
`.reset()`, `.stats()`. It's ~20 lines in `init` and I'm happy to drop it once the chain
runs say the word.
- `npm test` currently has 2 red in `src/sim/sim.test.ts` (crafting, shipping) and they are
NOT mine flagging for your review, not touching them. My 24 pass; tsc is clean repo-wide.
NOTE ON A THING I TOUCHED OUTSIDE MY LANE: I briefly created `.claude/launch.json` to get a
dev server on a free port (8150 is held by another session), then **deleted it** the tooling
kept resolving to the busy port anyway. Net diff outside `src/screen/**` is zero. If you want
lanes to be able to boot their own preview, that file probably wants to be orchestrator-owned
with one entry per lane; right now five parallel lanes would collide on it.
CONTRACT REQUEST: none. `ScreenFX` + `SimEvent` gave me everything I needed.
PROPOSAL (orders 2 asked me to flag this): **anchor-slab sanitizing is live and it's good
tension** potency 2.0, so 1 anchor cancels ~2 items of corruption and enough of them
sterilize THE SCREEN completely (tested). Right now that's invisible to the player, which
makes it a trap: they'll ship I-frames for a commission and watch their sky go clean with no
explanation. Two options for you (a) UI surfaces it (LANE-UI chip: "SANITISING"), or (b) the
fiction owns it: The Correction *pays* for anchor-slabs, so it's a deliberate sellout loop
bandwidth now, corruption later. I'd build (b); it's the codex's whole conflict in one item.
Also: `ScreenFX` currently only sees events, never the snapshot if you ever want THE SCREEN
to react to *bandwidth strain* or *heat* continuously (not just the brownout on/off edge),
that'd need `frame()` to see the snapshot. Not asking yet; noting the seam.
NEXT (my read, not self-assigning): item-specific artifacts that the 8 prototype params can't
express moiré needs a real interference lattice, ringing needs edge-detected halos,
macroblock bricks want the 8×8 grid to be *legible*. That's new shader passes, not new
weights. Also THE SCREEN should probably show the shipped item's silhouette for a beat as it
lands. Say the word on round 2.
### Round 2 — 2026-07-17 — Opus 4.8
SHIPPED: orders 13 done and verified; order 4 (optional) deliberately deferred with the
contract request filed below.
- `params.ts` PARAMS extended 8 11 (`block`, `ring`, `moire`). The original eight keep
their prototype tuning untouched. Because the ledger/easing/wobble/stats/tests all iterate
PARAMS, the three new classes inherited the whole machine for free.
- `glitchStack.ts` three new passes + the two overlays.
- `overlays.ts` (new) stamp placard + scram bar painters.
- `index.ts` stamp queue/lifecycle, scram trigger, debug hook now behind `import.meta.env.DEV`.
- `corruptionMap.test.ts` 30 tests green (was 24).
DECISIONS:
- **The block lattice is locked to screen space (`vUv`), not to the warped `uv`.** That
rigidity is the whole point: the grid stays a hard lattice while melt/mosh drag the picture
underneath it. Quantized in a 80×45 grid = true 8×8 source pixels at 640×360.
- **Ringing needed a second attempt.** v1 sampled the gradient at ±1.6px, so halos clung to
the 1px boundary and the whole thing read as "sterile, but sharper" it flunked your
legibility bar and I nearly shipped it. Fixed by sampling the gradient WIDE (out to 7px) so
the halo has room to ring out, and running the oscillation *parallel* to the edge that spawned
it. Mean abs pixel diff vs sterile went 16.5 25.5, and it now reads as haloing at a glance.
- Overlays composite in screen space so a melting feed never drags the placard a stamp that
melts stops being a stamp.
- Stamp queue caps at 2 pending. One stamp per EVENT, 0.4s each, size scaled by count
(0.721.0 across 1..40 units) with an overshoot on impact. A burst reads as a couple of
stamps, never a strobe.
- Scram bar position is derived from `entity`+`tick`, not `Math.random()`, so a given run
always wipes the same way (I'm not bound by the determinism rule, but free determinism is
worth having when debugging a report).
- `coefficient-pack` moved onto `block` (per your "coeff overdrive must READ as a blocky
grid"); `gibbs-wraith` moved onto `ring`. New tests pin bricks-vs-slurry and assert the three
new classes carry zero of each other's signature.
VERIFIED (drove the real module on the real canvas, read GL pixels, screenshotted contact sheets):
- **Bricks**: within-cell variance on the 8px lattice drops 689 236 (cells genuinely
flattening = quantization, not noise). Reads as an unmistakable quilted posterized grid.
- **Ringing**: bright oscillating ghost rings hugging every bar edge at 3.2× zoom; flat sterile
edges for comparison. MAD vs sterile 25.5.
- **Moiré**: diagonal rainbow interference lattice over the whole feed. MAD vs sterile 35.3.
- **Distinctness** (mean abs pixel diff): bricksslurry 21.9, ringmoiré 29.5, moiréslurry 34.3.
Nothing looks like anything else.
- **Stamps**: MELT ×40 punches a big orange placard, fades by 0.33s, gone by 0.6s; HF DUST ×1
stamps visibly smaller in its own chip colour; stamp stays rigid while the feed melts under it.
- **Scram**: torn white TRACKING LOST bar on exactly one frame, absent the next.
- **Budget: 0.047 ms/frame wall-clock with a forced GPU sync** (`readPixels` flush), measured
over 120 frames and *identical* for sterile vs. every artifact class lit at once plus
brownout. The new passes are free at this resolution. CPU-only rolling avg: 0.0030.02 ms.
BLOCKED/BROKEN: nothing. Two notes for the record:
- The verification tab is `hidden`, so rAF never fires and the main loop sits frozen at ~TICK 8.
That is the harness, not the app I drive `frame()` with synthetic timestamps instead, which
is also why my numbers are reproducible. Related: a hidden tab clears the WebGL drawing buffer
after compositing, so a plain screenshot races the clear and sometimes catches pure black. It
cost me a while to prove that was the harness and not a moiré bug. Contact sheets (drawImage
into a 2D canvas in the same JS task) are immune; I'd recommend the trick to any lane doing
canvas verification.
- Mid-round I saw reds in `src/sim/sim.test.ts` and `src/render/index.ts` tsc; both cleared as
the lanes landed. HEAD is clean repo-wide, 30/30 mine.
CONTRACT REQUEST: **`frame(timeMs: number, snap?: SimSnapshot)`** taking you up on the v3
offer. Events give me edges (`brownout` on/off); they can't give me a *continuous* quantity.
Signal strain wants THE SCREEN to tremble as `bandwidth.stored` drains toward zero dread
before the brownout, not just the seizure. Same seam would cover reacting to aggregate `heat`.
Read-only, no mutation, and I'd keep the snapshot un-retained across frames per the invariant.
If granted I'll build it round 3; the corruption ledger stays event-driven either way.
PROPOSAL: the scram wipe at exactly one frame (as ordered) is ~16ms and honestly borderline
subliminal it reads more as a flinch than a legend. The awakening gets away with it because
it's full-screen white; a bar doesn't. Suggest 3 frames (~100ms) so TRACKING LOST is actually
readable. Trivial change, your call shipped at one frame as ordered.
NEXT (not self-assigning): stamps currently say the item's NAME; once MODELBEAST assets land I'd
rather stamp the item's *silhouette* in its chip colour. Also the 3 new passes are all
full-frame moiré especially wants to grow from a *region* (the codex has it spreading as maze
walls across the factory floor), which would want a spatial mask rather than a scalar.
### Round 3 — 2026-07-17 — Opus 4.8
SHIPPED: all 4 orders. Thank you for the v3 grant `frame(timeMs, snap?)` is exactly the seam
strain needed.
- `strain.ts` (new) pure, snapshot-shaped, no DOM/GL. The whole point: order 4's sterile
guarantee is now a unit test, not a promise.
- `strain.test.ts` (new) 14 tests. Screen suite is 44 green.
- `glitchStack.ts` `growthMask()` + tremor/fever warps + regional moiré + fever tint.
- `index.ts` snapshot peek, strain easing, deterministic moiré seed, 3-frame scram.
DECISIONS:
- **Tremor is measured in SECONDS, not units.** Your v3 units ruling made this: `stored` is
bandwidth-seconds, `draw-gen` is bandwidth/second, so `stored/(draw-gen)` is literally *how
long until the lights go out*. Dread ramps over the last 8 seconds of reserve. This falls out
correct at every scale for free a megafactory bleeding 1000/s with 4000 banked feels exactly
as doomed as a shack bleeding 10/s with 40 banked, because both have 4 seconds. There's a test
pinning that equivalence. Raw `stored` would have made big factories look permanently calm.
- **Surplus is always calm, whatever the reserve.** If `draw <= gen` nothing is draining, so
there is nothing to dread which is *also* what makes the boot state (gen=draw=stored=0)
sterile by construction rather than by special-case. Order 4 fell out of the maths.
- **Fever averages only the machines that run hot** (heat > 0.01). Averaging across every entity
would drown one glowing decoder in a sea of cold belts, and belts are most of a factory.
- Tremor ≈ ¼ of the brownout's judder amplitude, and fever is a smooth sine haze while tremor is
random judder — so dread, fever and seizure are three distinguishable readings, not one dial.
Strain stays weather: MAD vs sterile is ~6.8, against ~35 for moiré. Subordinate as ordered.
- **Regional moiré separates AREA from INTENSITY.** Intensity saturates early (`u_moire*2.2`
clamped) while the mask's radius keeps growing with tally. That's what makes a big shipment
read as "the maze has spread" instead of "everything got slightly shimmery" — which is the
whole difference between regional and a scalar fade.
- `growthMask(p, seed, grow)` is written as the **reusable spatial-artifact template** you asked
me to note: an fbm-ragged growth front, aspect-corrected, creeping outward from a seed. Any
future artifact that should grow from a PLACE multiplies its amount by it. The raggedness is
load-bearing — a clean circle reads as a spotlight, not as spreading crystal.
- Moiré seeds from the tick of the first moire-weighted shipment, so a given run always grows the
same maze while different runs grow it elsewhere. Debug-hook ships default to tick 0.
VERIFIED (fixture snapshots + pixel reads + contact sheets):
- **Order 4, at the pixel level**: rendered no-snapshot / boot / healthy at an *identical*
timebase — boot and healthy are **byte-for-byte identical** to no-snapshot. Strain cannot leak
into the NOTHING IS WRONG era. (First attempt showed MAD ~1-2 and that was my harness: each run
advanced the clock, so the *timecode digits* differed. Fixed the test, not the code.)
- **Tremor ramp** (deficit 10/s): 20s left → 0, 8s (the horizon) → 0, 4s → 0.50, 2s → 0.75,
empty → 1.00. Fever 0.837 from hot decoders with tremor still 0.
- **Regional moiré growth**, as % of frame taken over: 0 → 20.1% → 44.7% → 74.5% → 84.5% → 85.8%
across 0/4/10/25/60/150 crystals. It spreads; it does not switch. The front is visibly ragged.
- **Seed**: same tick → same seed twice over; different tick → different seed. Deterministic.
- **Scram**: read the bar's row per frame — `BAR,BAR,BAR,-,-,-`. Exactly 3 frames, readable now.
- **Budget: 0.072 ms/frame** wall-clock with a forced GPU sync, on a 2000-entity factory with
every artifact class lit — `computeStrain`'s per-frame entity walk is not measurable at that
size. CPU-only 0.043 ms. Still ~28× under.
BLOCKED/BROKEN: nothing outstanding.
- **I found and fixed a real bug in my own code while chasing a false alarm.** My `dt` was
`Math.min(0.1, ...)` — ceiling clamped, floor not. A *negative* dt (clock rewind) turns
`ease()`'s `1-exp(-dt/tau)` into a huge negative multiplier and threw strain to **-3614** before
it crawled back. Production can't hit it (`performance.now()` is monotonic, and main.ts passes
it straight through), so this was only reachable from my harness — but an eased value that can
explode on a clock hiccup is a landmine, so `dt` is now clamped at both ends. Worth other lanes
checking their own dt handling if they ease anything.
- Full suite went 215/216 once mid-run, then 216/216 twice — another lane mid-write, not a real
red. HEAD is clean repo-wide, tsc included.
CONTRACT REQUEST: none. v3 covers everything I need.
PROPOSAL: `SimSnapshot.bandwidth` has no *capacity* field, so THE SCREEN can express "seconds
until empty" but not "the tanks are 20% full". Seconds is the better dread metric and I'd keep it
either way — but if buffer capacity ever lands in the snapshot cheaply, a second reading ("reserve
low" vs "reserve draining fast") would let strain distinguish a factory that's coasting on fumes
from one that's falling off a cliff. Not asking; noting the seam while it's fresh.
NEXT (not self-assigning): the mask template is ready for reuse — **bloom loops** and **HF dust
piles** are the obvious next spatial artifacts (both are regional in the codex), and the strata
biomes in §1 would suggest a mask per era. Also, now that strain reads the snapshot, THE SCREEN
could react to `jammed` entities (a stalled factory *should* look bored — the feed drifting back
toward clean is a nice punishment), but that's a design call, not a lane call.
### Round 4 — 2026-07-17 — Opus 4.8
SHIPPED: all 4 orders. Thanks for granting capacity unasked.
- `boredom.ts` + `boredom.test.ts` (new) — pure freshness curve, 7 tests.
- `strain.ts` — second dread axis; `strain.test.ts` now 20 tests. Screen suite 57 green.
- `overlays.ts` — ratification card painter + era→standard table.
- `glitchStack.ts` — pallor pass (desaturate + vignette), card composite.
- `index.ts` — boredom wiring, card lifecycle, `researched` handling, pallor easing.
DESIGN INTENT, one line each (as ordered):
- **tremor = "how many seconds until dark?"** ACUTE. The cliff edge. Only exists while something
is actually draining, and it spikes as the runway runs out. Expressed as motion: judder.
- **pallor = "how much cushion do I have at all?"** CHRONIC. Sits there in surplus. Expressed as
colour: the saturation drains out and the corners close in.
A full tank draining fast is a jolt; a near-empty tank coasting is a way of life. Different
questions, different senses, so they never mush into one dial.
DECISIONS:
- **Pallor drains saturation on purpose.** Codex §8: "ALL saturation belongs to media/artifacts.
Color = value = literally the resource." A factory with no reserve should therefore literally
have less colour. The vignette does the rest. It fits the existing strain/fever/seizure
hierarchy as the quiet one: chronic, slow tau (2.0s), never louder than the corruption.
- **The capacity trap**: `1 - stored/capacity` with no tanks built reads as "totally empty" and
would have painted the boot state grey — blowing up round 3's sterile guarantee on day one.
No tanks = no cushion to miss = no pallor. That's not a special case, it's the honest reading:
you cannot be low on a reserve you never built. Two tests pin it (capacity 0 AND undefined).
- **Boredom decays the DISPLAY, never the ledger.** `fresh` scales the targets; the tally
underneath is untouched. Verified explicitly: after 4 idle minutes the sky is clean but
`units: 72, tally: {melt: 60}, total: 0.893` are all exactly as earned.
- **Boredom horizon: 60s grace, then 180s decay (4 minutes to clean).** You asked for minutes,
not seconds. A full minute of silence changes literally nothing, so a jam you notice, diagnose
and fix is free. Recovery rides the normal 1.2s shipment ease — against a 4-minute decay that
reads as a snap, and it reuses the wave feel rather than inventing a second motion.
- **The card is timed in SECONDS, not frames** — it's a broadcast interruption and must be one
second at any refresh rate. Verified 30 frames @30fps AND 60 frames @60fps. (Deliberate
contrast with the scram wipe, which stays frame-counted: that one is a tape artifact, and tape
artifacts are quantized to frames. Two different clocks for two different fictions.)
- Card composites dead last in screen space, so not one pixel of it is corrupted — that IS the
joke. Hard cut on exit, no fade: the corruption crashes back rather than seeping, because a
dissolve would make The Correction look negotiable.
- Card says the STANDARD, not the tech id (codex §1 names: OPTICAL FILM, NTSC / PAL ANALOG,
MPEG-2 / DVD, H.264 / HEVC / AV1, DCP) with the green tick from §8's Correction uniform. The
tech id is in the small print where a compliance notice would put it.
VERIFIED (fixture snapshots, pixel reads, contact sheet):
- **Sterile guarantee survives the new axis**: boot AND a tankless factory in surplus both render
byte-for-byte identical to no-snapshot, pallor 0.
- **Two dreads are separable at the pixel level**: full-tank-draining-fast → tremor 0.50, pallor
0, saturation 0.621 and corners 96 both *unchanged from healthy*. Empty-tank-coasting → tremor
0, pallor 0.972, saturation 0.500, corners 26.6. MAD between the two: **43.7**. They do not
look like the same emergency.
- **Boredom**: untouched at 60s → fresh 0.834 at 90s → 0.5 at 150s → 0 at 240s → stays 0. Ledger
intact throughout. Resume: freshness 1 instantly, picture home in ~1.5s.
- **Card**: 30 frames @30fps, 60 @60fps = 1.00s both. Mean luminance 83 → 235 → 83.
- **Budget: 0.078 ms/frame** with GPU sync, 2000 entities, every artifact + both dreads + fever +
brownout + card compositing. ~26× under.
- Note for the record: **tremor is invisible in a still** — it's motion, so the contact-sheet tile
looks like HEALTHY. I pinned it numerically instead (0.50, MAD 6.8 vs sterile). A still frame is
the wrong instrument for a judder and I didn't want that filed as "verified by screenshot".
BLOCKED/BROKEN: nothing in my lane. **One thing the fleet needs to know:**
- `npm test` currently shows 6 red in `src/ui/live.test.ts` ("reference factory should contain a
software decoder", commission/entity-count assertions). **Not mine, and not LANE-UI's fault
either**: LANE-DATA has uncommitted edits to all five `data/*.json` in the shared tree, and
UI's live tests build a reference factory out of that data. I confirmed by stashing *only*
`src/screen/` — the same 6 still fail with my lane entirely absent.
- **Warning about the shared working tree**: my first instinct was `git stash`, which stashed
every lane's uncommitted work, not just mine — and made the UI tests "pass", which nearly had me
believing I'd broken them. `git stash push -- src/screen/` is the only safe form here. Any lane
bisecting a red test in this tree can be badly misled by a bare stash. Worth a line in
MASTERPLAN if you agree.
CONTRACT REQUEST: none. v4 gave me everything; fixtures now carry `commissionQueue` and
`research` ahead of the v5 hardening (order 4 done).
PROPOSAL: boredom currently only listens for `shipped`. A factory that is *jammed solid* and one
that is *idle because the player is redesigning it* decay identically. You approved the stalled-
factory idea as design — if you want the distinction, `jammed` entities in the snapshot could make
the sky decay noticeably faster (the work is rotting, not resting) while a clean idle stays
gentle. That's a one-line read of data I already have. Say the word and it's a small round-5 item.
NEXT (not self-assigning): the era→standard table means THE SCREEN now knows about eras — the §1
strata skins (sepia Reel, phosphor Broadcast, rainbow-on-black Disc) could drive the *base feed's*
palette per the player's current era, so the clean programming itself ages as they tech up. That
plus the mask template would give era-flavoured biomes on the sky.
### Round 5 (MEGA) — 2026-07-18 — Opus 4.8
SHIPPED: all 8 orders, both organs. THE SPEAKER exists.
- PART A: `eras.ts` + `eras.test.ts` (new), `baseTexture.ts` (era substrate + general chyron),
`glitchStack.ts` (wildlife pass, title composite), `boredom.ts` (jammed-rot), `index.ts`.
- PART B: `graph.ts`, `sfx.ts`, `stations.ts`, `radio.ts`, `ambience.ts`, `subtitleBus.ts`,
`band.test.ts` (all new), and `index.ts` replacing the stub.
- 78 tests in my two lanes, all green. Repo-wide 443/443, tsc clean.
**A NOTE ON "VERIFICATION INCLUDES LISTENING": I cannot hear.** I have no ears, and I won't
pretend a waveform is a listening session. What I did instead is render every sound through an
`OfflineAudioContext` and measure it — spectra, peak/RMS, attack times, onset rates — and the
description below is derived from those measurements plus the synthesis I wrote. It is an
honest account of what the numbers say the speaker is doing, not of what it feels like in a
room. **Please put headphones on and tell me what's wrong** — mix balance and "is this
annoying after ten minutes" are exactly what measurement can't answer, and I'd rather get that
from you than guess.
WHAT IT SOUNDS LIKE (measured, then described):
- **Brownout** — the important one. A 0.29 ms attack: a genuine broadband click, not a fade.
Then ~0.5 s of bandpassed brown crackle and a sawtooth whine sliding 420→40 Hz under a
closing lowpass. It should read as a rack losing its rails: snap, spit, sag.
- **Placement thunk** — 2.3 ms attack, a sine dropping 150→55 Hz (the mass) under a 1.4 kHz
noise slap (the contact). Detuned per placement so a row of belts isn't a machine gun.
- **Scram klaxon** — 620/440 Hz square alternating four times, lowpassed at 1.8 kHz. Peak 0.29:
deliberately not the loudest thing, because it fires often.
- **Ratification sting** — 85 ms attack, FAC sines with an octave shimmer. The *only*
consonant, in-tune, untroubled sound in the game. Everything the player makes is filth; the
enemy sounds lovely. That's the joke, and it's why its attack is slow when everything else
is percussive.
- **FKTRY FM 88.1** — 4.9 note onsets/sec against 6.13 sixteenths at 92 bpm, i.e. the ~34%
rests are audible as space rather than a wall. Square lead over triangle bass, pentatonic
minor, pattern mutating every 32 bars. Spectrum is bass-dominant (60 Hz: 0.49) with nothing
above 4 kHz.
- **THE STANDARD 640** — 2.9 syllables/sec, which is real speech cadence. Ring-modulated
carrier through two moving formants, lowpassed at 4.5 kHz to AM bandwidth. Sits at 500 Hz
and nowhere else. It is deliberately just short of intelligible — you can hear that it's
speech and not what it says. **THE SCREEN tells you what it says.** That gap is the trick.
- **NUMBERS 6955** — 4.0 digits/sec, a formant voice with a per-digit vowel shape so numerals
are learnable by ear, through a narrow SW bandpass (sharp 2 kHz peak, 0.50). No affect, no
melody, no acknowledgement.
- **DEAD AIR 108.0** — lowpassed brown room tone breathing at 0.21 Hz (projector pace), plus
one reversed chime — swelling up out of nothing then cut dead — every ten minutes, seeded.
DECISIONS:
- **Everything takes `(ctx, dest, t0)` and nothing assumes a live context.** That one choice is
why the whole speaker can be rendered in an `OfflineAudioContext` and measured deterministically
— same code the player hears, no test-only path. Noise buffers use a seeded PRNG for the same
reason: identical spectra every run.
- **The limiter is a `tanh` WaveShaper, not a DynamicsCompressor.** A compressor's makeup gain
would pump the entire mix every time the klaxon fires. tanh just refuses to exceed 1, which is
what "don't clip" actually means, and it adds the saturation a blown factory PA should have.
- **Pre-discovery silence is structural, not a volume setting.** The radio's master gain is 0
and ambience is never started until `relicFound`. Layer 1 is all one-shots, so a quiet factory
is *actually* quiet between events rather than humming under a noise floor.
- **Inter-station noise is band-specific** because that's what makes a band feel like a place:
AM brown crackle, FM white hiss, SW a wandering bandpass plus a heterodyne whistle whose pitch
is the beat frequency — so it swoops *down to nothing* as you zero in. Hunting is the mechanic.
- **The era ages the substrate, never the corruption.** Same shader, same params; only what
they're painted onto changes. The contact sheet shows identical melt drips across all five eras.
- **Boot era is `reel`.** With nothing ratified the world starts in the deepest stratum and
climbs to the surface (stream) and beyond (fortress). The old SMPTE look is now mid-game.
- Title timed in seconds (1.53 s hold, gone by 2.07 s), awakening 0.47 s. Both are *moments*;
the scram wipe stays frame-counted because it's a tape artifact. Three clocks, three fictions.
VERIFIED (offline render + pixel reads + contact sheets):
- **Pre-discovery silence: peak 0.0 and RMS 0.0, exactly zero**, with the dial swept across all
three bands and ambience asked for max load+fever. Same graph post-discovery: peak 0.285.
- **Station lock vs static are distinct spectra, and correctly so per band.** AM static has
**70× the 60 Hz energy of AM locked** (1.40 vs 0.02 — the brown crackle). FM *inverts*: locked
is bass-dominant (0.49 @60 Hz, nothing @14 k), static is treble-only hiss (0.10 @14 k, nothing
@60 Hz). SW locked shows a 2 kHz formant peak (0.50) that static (0.09) lacks.
- **Subtitles end-to-end**: tuned AM → The Correction's line on the chyron; off by 4 kHz (lock
0.906) → still captioned; off station → caption clears; SW → `5 3 0 7 9` on the sky, which is
the real coordinate for the nitrate vein. FM and DEAD AIR caption nothing (no words). Screenshot
in the sheet.
- **Era contact sheet**: reel (sepia + sprocket holes), broadcast (phosphor + interlace), disc
(rainbow-on-black gloss), stream (classic SMPTE), fortress (blinding white) — all under the
same 20× melt.
- **Budget: SCREEN 0.040 ms/frame** (GPU sync, 2000 entities, every artifact + wildlife + strain
+ brownout). **SPEAKER 0.0063 ms/frame** on the worst case (SW numbers + heterodyne + ambience)
— the per-frame path only writes AudioParams; Web Audio does synthesis on its own thread.
BLOCKED/BROKEN: nothing blocking.
- **I caught a balance bug by measuring rather than hearing**: DEAD AIR rendered at RMS 0.072,
*louder than the music station* (0.032), which made "dead air" the loudest thing on the dial —
exactly backwards. Dropped its tone gain 0.32→0.10 and the breath depth with it; it now sits at
0.023, under the music. This is the clearest argument for you actually listening: I only found
it because I happened to print RMS side by side.
- Harness gotcha, mine not the product's: importing a module with a `?v=` cache-buster creates a
**second module instance**, so my first subtitle test wrote to a different singleton than THE
SCREEN reads and the chyron looked broken. Verified `busQ !== busC`. Re-ran canonically and it
works. Same family as round 3's clock rewind: the harness lied, not the code.
FINDING FOR LANE-DATA (not touching their file): `stations.json`'s `_doc` says the decoys
"decode to empty ground", but **two of them decode onto real seams**`08127` → (-24,-20) lands
in `reel-beds-deep`, and `27503` → (-5,18) lands in `broadcast-shelf-south`. Both are visible,
non-hidden seams so no secret leaks and the treasure hunt is intact — it's a doc/data drift, not
a break. I deliberately did **not** red-light my suite over another lane's content (round 4's
lesson); my test asserts the invariant that actually matters — *no decoy ever leaks a HIDDEN
seam* — which passes. Their three planted groups all decode exactly right, and I have a test
pinning that against `seams.json` so the map can never drift.
CONTRACT REQUEST: none. v6 covered everything.
SANCTIONED IMPORTS (documenting as ordered): LANE-UI's tuner dock should
`import { getRadio } from '../audio'` — a narrow surface of setBand/setFrequency/setVolume/
setMuted/getState/subscribe/listStations, nothing else reachable. Returns `null` before the first
user gesture builds the graph, so the dock must handle that (and should probably render disabled
until `getState().unlocked`). Internally, `src/screen` reads `src/audio/subtitleBus` — both are my
lane, and it's a zero-dependency module precisely so neither organ couples to the other's guts.
STATIONS SCHEMA: I conformed to LANE-DATA's file exactly as written rather than proposing my own
— it's better than what I'd have specified (the `eraFlavor` table and the coordinate encoding
especially). Nothing to agree; it's already right.
PROPOSAL: THE STANDARD renders at RMS 0.0037 against FKTRY FM's 0.032 — ~9× quieter. That's
*correct* for gated speech (its peak, 0.10, matches the other stations; the gap is all the silence
between syllables) but it may still feel too quiet in practice next to continuous music. If it
does when you listen, the fix is a slow compressor on that station alone rather than a gain bump,
which would just make the syllables shout. Flagging rather than pre-emptively "fixing" something
the numbers say is fine.
NEXT (not self-assigning): the tuner has no UI yet — LANE-UI's dock is what makes the band
findable at all, and until then the radio is only reachable from the console. Also `growthMask`
now has two customers (moiré, wildlife) and is ready for a third: HF dust piles are regional in
the codex and `dust-pile` wildlife already arrives in the same event I'm consuming.
### Round 6 — 2026-07-20 — Opus 4.8
SHIPPED: all 3 orders. The sky now notices The Correction. `growthMask` gained its third and
fourth customers (order 3 round-3 groundwork paying off — no new shader machinery, new weights).
- `overlays.ts``paintRepair()` + `repairCanvas` (224×112). The Correction's uniform: hospital
white, faction green tick, machined square. Deliberately the inverse of the shipment stamp
(hand-thumped, crooked, chip-coloured, "SHIPPED / NO QUESTIONS").
- `glitchStack.ts` — two additions: the **mite sterility** pass (`u_mite`/`u_miteSeed`, reusing
`growthMask`) and the **repair placard** composite (`u_repair`, texture unit 5). The mite pass
sits on TOP of the corruption stack because it is *erasure*, not another artifact.
- `index.ts``repaired` events ease the ledger and queue a placard; parity-mite tracking drives
the sterile creep (snapshot truth / event fallback, same pattern as the mosquito swarms); repair
placard lifecycle in its own slot. DEV hook gained `.mite()`, `.repair()`, and — new — `.frame()`
+ `.canvas()` drivers (see the harness note below).
- `corruptionMap.test.ts` — +3 tests pinning the repair semantics. Screen suite 33 (was 30);
my two lanes together 81 green (screen 72 + audio band 9).
DECISIONS:
- **Repairs are THEFT; boredom is ROT — kept structurally distinct, per the order.** A `repaired`
event decrements the *ledger* by exactly one unit of the item (units drop by that item's
potency — pixel-verified: melt ×20 → repair → units 24.0→22.8, tally 20→19). Boredom only fades
the *display* and leaves the tally intact (recoverable). So a factory being eaten by mites
permanently loses corruption it can never fade back, while an idle factory snaps straight back
the moment it ships. I deliberately do NOT refresh the boredom clock on a repair — a factory
being quietly corrected is not a factory that is producing.
- **The mite patch is the inverse of corruption and it reads WORSE, as ordered.** Where the
mosquito shimmer *darkens* a region with crawling noise, the mite patch *desaturates and lifts
toward clinical white* with a faint manufactured tile grid. Pixel-verified against a heavy melt
feed: the bloom region went luminance 22.9→168.8 and saturation 0.695→0.025. A dead, too-clean
hole in your rot is more unsettling than more rot — the codex's whole point about The Correction.
- **The repair placard parks low-right (vUv 0.80,0.80), small (0.15×0.082 half-size), 0.55s** so it
never fights the centre shipment stamp — a burst of ships and a burst of corrections can co-occur
and stay legible as two separate voices.
- Both new spatial effects reuse `growthMask` verbatim (round-3 template): moiré, mosquito, AND
now mite sterility all share one fbm growth front. Zero new shader machinery for order 2.
VERIFIED (drove the REAL module on the REAL 640×360 canvas, synthetic timebase, GL pixel reads —
the harness tab is `hidden` so rAF is throttled, the round-2/3 gotcha; I pump `frame()` and
`drawImage` in the same JS task):
- **Repair**: units eased by exactly melt's potency (1.2); placard region flips corrupted
(lum 68) → hospital-white placard (rgb 228/232/228, lum 230). repair flag + tally decrement both
confirmed.
- **Mite creep**: localized bloom (grid cell 3,4 — NOT the whole frame), lum +146, saturation
→0.025. `mite` eases to 0.83 from size 0.85. Both source-of-truth paths exercised: event
fallback (`.mite()`) AND `snapshot.wildlife` parity-mite.
- **Chyron / order 3**: DATA landed 8 mite-acknowledging STANDARD lines (30→38). Pixel-checked the
live wrap: short lines 1 row, the 96-char lines wrap to exactly 2 rows, rightmost glyph at x613
inside the 640 canvas — no clipping. Word-wrap (orchestrator's fix) intact; I did not touch it.
The mumble is untouched — I never edited `amPropaganda`; it derives syllables from the line text.
- **Budget: 0.034 ms/frame** worst case — 2000 entities, corruption total 1.0, strain 0.94 +
fever 0.57 + pallor 0.82 + mosquito 0.89 + mite 0.89 + brownout, every param lit. ~59× under the
2ms budget. The two new shader branches and one new texture are free at this resolution.
BLOCKED/BROKEN: nothing in my lane.
- **Transient full-suite red, NOT mine (round-4 lesson holds).** A combined `npm test` briefly
showed 3 reds — `band.test.ts` (STANDARD line >96 chars), `sim/mite.test.ts`, and
`sim/reference.test.ts`. All three are the shared-tree race: DATA was mid-save on
`data/stations.json` (I caught it at exactly 96 chars, its own budget) and SIM has uncommitted
`mite.test.ts` + `constants.ts`/`index.ts` edits in flight. Re-running JUST my lanes: 81/81
green. I did NOT bare-`git stash` to chase it (my own round-4 near-miss).
- HEAD advanced under me twice mid-round (a `[ui] round 6` commit landed while I worked). Re-probed
before writing this; all claims above are against the tree as it stands now.
HARNESS NOTE: I added DEV-only `.frame(ms, snap)` and `.canvas()` to `__fktryScreen`. Round 2/3
both lamented having no clean way to pump the real module in a throttled hidden tab; this makes
pixel verification a two-line console call for any lane. Stripped from prod (`import.meta.env.DEV`).
CONTRACT REQUEST: none. `repaired` + `parity-mite` (v8 contracts) gave me everything.
COORDINATION (order 3, resolved): DATA owns the STANDARD copy; I own its rendering. The 8 new
lines are theirs and are already in `data/stations.json`. My only ask of them is a standing one:
please keep STANDARD lines ≤96 chars — `band.test.ts` asserts it and the chyron wraps to 2 lines
at that budget; a longer line spills to a 3rd line (the font steps down to 13px) but starts to
crowd the feed. The newest line is exactly 96, so we're at the edge.
PROPOSAL: the repair placard and the ratification card both wear the green tick — that's
intentional (one faction, one uniform), but a player seeing their first repair might read the
small placard as "another commission done" rather than "something took my work". If that lands
ambiguous in playtest, the fix is a one-word change to the placard legend ("CORRECTED" is already
doing the work, but "REVERTED" or "RECLAIMED" is colder). Flagging, not pre-emptively changing.