Supersedes unexecuted round-5 orders. Folds in the live first-eyes review findings (silence, void floor, onboarding cliff, quiet payoffs) + John's hidden-radio idea. Contracts v6: AudioFX (THE SPEAKER - all Web Audio synthesis, no asset files), RelicState + excavate + relicFound (the optical-fossil hidden pipe: find it and the game grows a tuner - FKTRY FM, Correction propaganda subtitled by THE SCREEN's chyron, a shortwave numbers station reading hidden-seam coordinates, dead air). main.ts wires audio + first-gesture unlock; src/audio stub awaits LANE-SCREEN. Lane orders: SIM = wildlife + seams-become-real + the relic + research time; RENDER = the floor (seam glints, strata whispers), relic shimmer (no signposting), juice, break-room cabinet -> match.html; UI = mercy toast, tuner dock, SCREEN click-to-enlarge, camera hints; SCREEN = era feed + THE SPEAKER (3 audio layers, 4 stations, spectral verification); DATA = the commission-ladder tutorial, stations.json broadcast schedule, seams.json final. Ship gate: after review, redeploy partly.party/glytch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
413 lines
29 KiB
Markdown
413 lines
29 KiB
Markdown
# 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 5 (MEGA) — supersedes the earlier round-5 text
|
||
|
||
You are now two organs: THE SCREEN (sky) and **THE SPEAKER** (`src/audio/**` is
|
||
yours — replace the orchestrator stub; contracts v6 `AudioFX` is wired in main.ts
|
||
with a first-gesture `unlock()`). House law: **every sound is synthesized with Web
|
||
Audio. No asset files, ever.** A game about broken media makes its own noise.
|
||
|
||
### PART A — carried from round 5 (THE SCREEN)
|
||
1. **Era base feed**: the clean programming ages with the highest ratified era
|
||
(sepia/gate-weave reel → phosphor/interlace broadcast → rainbow-on-black disc →
|
||
current SMPTE stream → blinding fortress). Corruption stays era-agnostic; the
|
||
SUBSTRATE ages. Sterile guarantee holds per era (test one).
|
||
2. **Jammed-rot**: jammed entities accelerate boredom decay; clean idle stays gentle.
|
||
3. **Wildlife shimmer**: swarms drive regional mosquito-noise via the growth-mask
|
||
template, seeded from the swarm pos.
|
||
4. **Title card**: WIMVEE FKTRY burn-in ~2s on boot (the game never says its own
|
||
name — fix that), then NOTHING IS WRONG. First-ever shipment's awakening upgrades
|
||
from 1 frame to a ~0.5s full-canvas moment.
|
||
|
||
### PART B — THE SPEAKER (new, the big build)
|
||
5. **Layer 1 — diegetic minimum (always on, pre-discovery)**: placement thunk (low
|
||
sine + noise burst), fax dot-matrix chatter, brownout pop-crackle + power-down
|
||
whine (the codex's catastrophic underrun sound — finally), scram klaxon,
|
||
ratification sting (a clean consonant triad — The Correction resolves; you don't).
|
||
NOTHING else before discovery: the silence is the tease.
|
||
6. **Layer 2 — THE BAND (unlocks on relicFound)**: a tuner. Frequency sweep with
|
||
band-appropriate inter-station noise (AM = brown crackle, FM = white hiss, SW =
|
||
warble + heterodyne whistles near carriers); station lock via gaussian around
|
||
each station's freq. Stations from `data/stations.json` (DATA authors; schema
|
||
yours to agree in NOTES):
|
||
- **88.1 FKTRY FM** — generative music: seeded 16-step sequencer, square+triangle
|
||
voices, pentatonic minor ~92bpm, pattern mutates every 32 bars, flavored by the
|
||
current era (reel era gets wow/flutter + crackle).
|
||
- **AM 640 THE STANDARD** — Correction propaganda: ring-mod mumble cadence, and
|
||
the flagship trick: **THE SCREEN's chyron subtitles the radio** when tuned
|
||
(DATA writes the lines). The sky captions the audio. Do this one properly.
|
||
- **SW 6955 NUMBERS** — cold formant-synth digit groups; chyron shows the digits;
|
||
some groups encode coordinates of DATA's `hidden: true` bonus seams — the radio
|
||
is a treasure map for players with a pencil.
|
||
- **108.0 DEAD AIR** — room tone, breathing, one reversed-chime ghost per ~10min,
|
||
seeded.
|
||
7. **Layer 3 — world ambience (post-discovery)**: factory hum scaled by draw/gen,
|
||
tape-chunk per shipment stamp, heat adds a strained whine near scram. Subtle;
|
||
ducked under the radio.
|
||
8. **The control surface**: export a narrow `radio` API (setFrequency/setBand/
|
||
volume/mute/getState/subscribe) — UI's tuner dock drives it (sanctioned import,
|
||
document it). Master chain: gain → soft limiter; zero per-frame allocations.
|
||
|
||
**Definition of done:** spectral verification (AnalyserNode: station lock vs static
|
||
show distinct spectra; brownout pop is a real transient); pre-discovery silence
|
||
guarantee tested; subtitles verified on the chyron; era feed contact sheet; CPU
|
||
budget stated. This round, verification includes LISTENING — describe what it
|
||
sounds like in NOTES, in words.
|
||
|
||
---
|
||
|
||
## 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 + item→artifact 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-cams→freeze, static-canister→noise, mosquito-swarm→noise, explosive-hits
|
||
→mosh+burn+chroma, gamut paints graded rec709≪rec2020, 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.004–0.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 1–3 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.72→1.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): bricks↔slurry 21.9, ring↔moiré 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.003–0.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.
|