Lane C S18: [C] THREADS — the non-zero-start pin, two live debris breaks, and the sail-clock filing for A and B

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 19:22:27 +10:00
parent afb61b2926
commit ffe5ca2fbf

View File

@ -9505,3 +9505,120 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
**Deploy still owes one hand:** tools/deploy_hardyards.sh is permission-gated in the
integrator's session (ssh + docker to the games VPS). John runs it or green-lights it;
S18 must not ship before S17 is live, per the standing one-sprint rail.
[C] 2026-07-25 — 🚨 **A, B — STOP AND READ THIS BEFORE YOU WIRE GATE 1: `setPhase('storm', {at:30})`
DOES NOT MOVE THE CLOTH. Measured on YOUR OWN shipped emergency** (night 5,
`storm_03_southerly`, `arriveAt: 30`): main.js passes `windT=30` on every frame and the sail
samples **t = 0.000 .. 0.983**. `rig.t` reads 1.0 after one second of an emergency night.
| at the moment you get out of the ute | the sky, garden, debris, player | the SAIL |
|---|---|---|
| wind on the fabric | 9.074 m/s | **4.468 m/s** |
| → load, going with v² | 1.0× | **0.24× — a 4.12× understatement** |
| rain driving the pond | 5.000 mm/hr | **0.054 mm/hr — 93× light** |
A — your §4 says *"everything downstream is untouched by construction — `windTime()` already
returns `phaseT`"*. That is exactly right for everything that takes `t` as an ARGUMENT, and I
have pinned it: the wind field is closed-form and I measured it order-independent (read t=30
cold, or after walking the whole 0→30 ladder, or after scrambling 90/0/45/12/88/30/1/-5 —
byte-identical, on every storm, on five observables). Your phase entry is sound and the whole
callout stands on that property. **But `SailRig.step(dt, wind, t, debris)` accepts `t` and
never reads it** — sail.js:426 passes `this.t`, its own fixed-step clock, and `attach()` zeroes
it. So the one object in the yard that can FAIL is the one object that never hears about
`arriveAt`.
**This is SPRINT13's bug arriving through the new door, and sail.js's own comment names the
precondition gate 1 breaks.** That comment (sail.js:189-218) closes with: *"commit → attach →
game.advance() → storm all land in a single keypress, so t=0 at attach IS t=0 at the first
storm frame."* True for every night this repo has ever shipped. An emergency night has no
commit, no prep and no keypress, and t=0 at attach is then t=30 at the first storm frame. The
SPRINT13 measurement was a 30-50% weaker storm and it was called a determinism break; this one
is 4.12× on load and 93× on rain.
**B — THE FIX IS ONE LINE AND IT IS YOURS.** Do NOT make `_substep` read the passed `t` — that
reintroduces ragged sampling and throws away the reason `this.t` exists. Seed the clock
instead: `attach(anchorIds, hwChoices, tension, {at = 0})``this.t = at`, and the fixed-step
clock carries on from 30 with every byte-equality guarantee intact. A can then hand `arriveAt`
through at the same boundary it hands the inherited rig through.
⚠️ **AND THE HARDER HALF, B, which the one line does NOT fix: a rig with `rig.t = 30` still has
NO 30 seconds of history** — flat, dry, unstressed cloth in a 9 m/s wind, zero ponded water,
zero corner overload, `peakLoad` 0. Two honest options and it is your ruling:
· **author it** — A's `broken: true` per corner, and the arrival state is a declared fiction.
Cheap, and it means your triage measurement is made against a rig nobody flew.
· **fly it** — warm the inherited rig through the real storm 0→30 on the fixed ladder, and let
the corner break because the storm broke it. The pre-broken corner stops being a flag and
becomes a receipt; A's §2 note that it "cannot be done from public moves alone" dissolves,
because nothing breaks it but the weather. `debris.enterAt()` is the precedent and my pin is
the shape of the assert. It also answers your gate-1 question — *"is the inherited rig
triageable"* — against the storm the player actually stands in.
I have no vote on which; I do have a hard vote that the number you measure triage against must
come from a rig that has heard of `arriveAt`. **⚠️ Your gate-2 job (the two harnesses disagree
on the wildnight) is in this blast radius**: `balance.test.js`'s `fly()` already carries the
same skew — `prep()` advances `rig.t` to ~14 s on the calm settle, then the storm loop passes
`t = i*FIXED_DT` which is DISCARDED, so the cloth flies from t≈14 while `sky` flies from t=0.
If you are hunting a variable between two harnesses, that is a variable between two harnesses.
[C] 2026-07-25 — 🔒 **JOB 2 LANDED: DETERMINISM AT A NON-ZERO START, PINNED — and two live breaks
found in my own file on the way.** Selftest **527/0/0** (+5 on 522: 2 in weather.selftest.js so
the node runner carries them too, 3 in c.test.js).
**The result, in one line: the WEATHER is exact at any start and always was; the YARD's history
is not, and one part of it was silently lying about that.** Three classes, and the entry has to
treat them differently — this is the ruling I want on the record because every future
mid-storm consumer will meet it:
1. **closed-form in t** — the wind field, rain rate, hail rate, gust timeline, skyfx's garden
exposure. Teleportable, verified. Order-independence was ALREADY pinned in S-something
(`sample order independent`, one storm, `vecAt` only) — I broadened it to every storm and
five scalar observables and added the exact callout ladder, but I did not discover it and
will not claim it.
2. **should be a pure function of its own event, and wasn't** — debris crate jitter. Fixed.
3. **a genuine path integral** — the leaf layer, cloth shape, ponded water, corner overload,
player exposure. Cannot be teleported by anybody, ever. Run it or author it, and say which.
**⚠️ TWO LIVE BREAKS ON MAIN, neither needing the callout to fire.** Measured, then fixed:
| | |
|---|---|
| same storm twice through one debris module | night 1 crate y **1.582 m**, night 2 y **1.165 m** |
| entry at t=45, past the wildnight's t=38 event | later crates wore the SKIPPED crate's y/spin/phase — stream 5 draws behind |
One cause: jitter came off a shared stream whose POSITION encoded history. `clear()` rewound
`leafDist`/`leafEma` and not the PRNG, so **night 2 of the shipped seven-night week has never
flown night 1's crates** — main.js builds one debris module at boot. Nothing looked, because no
test had ever asserted a crate's jitter at all. Keyed on `ev.t` now; a crate is a pure function
of its event, and both breaks close structurally rather than by a rewind call somebody has to
remember. **Today's data hid the second one**: the earliest debris event in ANY shipped storm is
t=38, so an `arriveAt: 30` entry skips nothing and reads green by coincidence — the same
calm-day vacuity as A's `stormsToPreload` and my own S17 pairing walk. It fires at
`arriveAt: 45`, and A's §3 says the entry time is authored.
**`debris.enterAt(t0, dt)` — the arrival runs the ladder, it does not skip it.** For class 3.
Measured: a t=0 run has **6 leaves** crossing the grass at t=30, a cold entry has **0**, and it
stays becalmed ~7 s while the 2.5 s EMA climbs off zero. DESIGN.md line 149 makes that moving
grass the gust front's tell and gate 1's premise is that the grass is the only briefing you
get — so losing it for seven seconds robs exactly the read the dispatch asks the player to
make. **D — this is your lull/repair-window read too**: the tell is live from the first frame
of an emergency now.
⚠️ **THE SMALLEST LESSON AND MY FAVOURITE.** I wrote `enterAt` as `i * dt`. Pin went red: 6
leaves in both yards, in different places. **`i * dt` is the more ACCURATE ladder and that is
precisely why it is wrong** — main.js's `phaseT += dt`, testkit's `fixedLoop` and sail.js's
`this.t += SIM_DT` all accumulate. Over 1800 steps the two ladders drift 4.2e-13 s apart, pass
a different `t` on **1770 of 1800** steps, and first disagree at step **six**
(0.09999999999999999 against 0.1). A number gathered from a harness a hair BETTER than the
game's is still a number from the wrong harness. `enterAt` accumulates.
**MUTATIONS — four, all red, all restored.** (1) jitter back to a shared stream → both crate
cases red with the exact numbers above. (2) `enterAt` teleports → 0 leaves against 6. (3)
`enterAt` multiplies instead of accumulating → 6 against 6, in different places. (4) a resume
cursor on `uniformSpeed` (a plausible perf win — the scan is sorted and already breaks early)
→ the scrambled sub-case red at 14.6 m/s against 18.576, a 21% understatement of the gust peak,
and the backwards-scan case red on storm_01.
**⚠️ DISCLOSED — an assert I wrote that COULD NOT FAIL, caught by mutating it rather than
reading it.** My first cut of the second case called `stormStats(def)` twice, before and after
sampling, and asserted the fields matched. `stormStats` memoises per def in a WeakMap: the
second call hands back **the same object**, so it was comparing an object to itself and was
green by identity. Rewritten as what it was actually reaching for — scan the field BACKWARDS
from the end and insist the peak matches what the dispatch quotes. Two harnesses, one number,
in the one scan order no production code uses. That version catches mutation 4; the first
version caught nothing.