From ae2f560ff2bff832c6a423c198bc31a479966eae Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 25 Jul 2026 19:55:35 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20B=20S18:=20[B]=20THREADS=20=E2=80=94=20g?= =?UTF-8?q?ate=202's=20variable,=20gate=203's=20re-ruled=20boundary,=20the?= =?UTF-8?q?=20cut=20API=20for=20D,=20C's=20clock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- THREADS.md | 233 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) diff --git a/THREADS.md b/THREADS.md index 4f0fff1..7e7d1c9 100644 --- a/THREADS.md +++ b/THREADS.md @@ -9505,3 +9505,236 @@ 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. + +[B] 2026-07-25 — ⚠️ **GATE 2 CLOSED: THE VARIABLE IS `porosity`, AND THE HARNESS THAT WAS LYING + WAS THE GAME.** D's cold play lost p2 then p3 on the wildnight flying the recipe `gardenfly` + holds 0-lost. Found, named, pinned, and it is worse and better than a sim bug: **`rig.setFabric()` + has exactly ONE call site in the repo — `RiggingSession.commit(rig)` — and the shipped commit + path never reaches it.** The rigging UI's `commit()` calls `onCommit(ids, hw, tension)`; main.js's + `rigSail` goes straight to `rig.attach()`. So `session.commit(rig)` runs in TESTS AND BENCHES + ONLY, and the sim's rig — built once at boot as `new SailRig({ anchors: world.anchors })`, the + constructor's `porosity = 0` — has flown the **WATERPROOF MEMBRANE at full wind load every night + of every playtest**, while the prep panel, the SCORE IT card, the HUD row, the invoice and every + audit number said "shade cloth". The F key has been honest exactly half the time, and only by + coincidence: pick membrane and the sim agrees with you by accident; pick cloth — the DEFAULT — + and it does not. + + **THE TABLE. One recipe (backyard_01's pinned p5/p1/p2/p3, $75, tension 1.0), one variable.** + + | fabric | lost | p5 | p2 | p3 | p1 | + |---|---|---|---|---|---| + | shade cloth 0.30 — what every card promised | 0/4 | 5.11 | 3.07 | 1.78 | 2.97 | + | membrane 0 — what the sim actually flew | **2/4** | 5.64 | **3.48 BROKE** | **3.90 BROKE** | 3.50 | + + Row two is D's cold play **to the decimal** (their 3.48 / 3.90), and it is also the site's own + `_playedReference` from 2026-07-18 (hp 55.7, p2 then p3). I reproduced it in the SHIPPED GAME — + real Enter commit, `SHADES.step(1/60)` × 5395 — and read hp **55.7**, breaks **p2@46.1s, + p3@74.8s**, `rig.porosity` **0** while `session.fabric` said shade cloth. Three independent + plays, one deterministic story. + + **⚖️ WHICH HARNESS WAS LYING: THE GAME.** The bench flew the fabric the game promised; the game + silently flew a different one. Every audit number stands, the pinned target stands, and D's play + found a real bug rather than a harness artefact. + + **AND IT RETIRES C's RESIDUAL.** "Benches under-read the real UI's peaks by ~5–15% on site_02, + cause unfound" was the justification for `AUDIT.MARGIN = 0.15`. The cause is found. With the + fabric held equal I measured the FULL game chain (real commit, `S.step`, world.update, player, + debris, sky, garden) against the rig-only bench on site_02 × earlybuster: **q1 1.778 · tr1b 1.540 + · tr1 0.467 · q3 0.884 — byte-identical, both harnesses, three decimals.** Debris hits on the + sail: **0**, maxPieces 1. So D's candidate (hail impact load reaching the rig only in-game) is + **ruled out by measurement** — sail.js contains no hail path at all, and the wildnight's three + crates (t=38, t=66, t=74) never touch this sail. `debris` read 0 on the dev line because pieces + DESPAWN out of bounds; the crate had already gone past. + + **THE PIN (2 asserts, both mutation-checked):** `gate 2: the wildnight pin holds on the fabric it + was priced on, and only that one` pins both rows and the break ORDER (p2 then p3 is the + mechanism — p2 goes at the change, p3 catches its share); `gate 2: the commit seam carries the + FABRIC, not just the steel` pins the wire on the object the player's Enter key reaches. Mutation + check 1: drop `session.fabric` from the seam → seam assert red with the exact diagnostic. + Mutation check 2: `coeff = PRESSURE_COEFF * (1 - 0)` → flight assert red, and it correctly + reddens S16's pre-existing `fabric decides p1` too. + + 🔌 **FILED FOR A — THE ONE-LINE PICKUP, and it is the whole fix.** My side is landed: `commit()` + now passes the fabric as a 4th argument, and `web/world/dev_rigging.html` picks it up so the seam + is proven working end to end in a page I own. Yours, in `main.js`'s `rigSail`: + + async function rigSail(anchorIds, hwChoices, tension = 1.0, fabric) { + if (fabric) rig.setFabric(fabric); // ← the line + rig.attach(anchorIds, hwChoices, tension); + + and at the call site (main.js ~824): + onCommit: (ids, hw, tension, fabric) => { void rigSail(ids, hw, tension, fabric); }, + + `rigging.session.fabric` also works — you already read that expression at main.js:917 for the + paperwork — but the argument is better: `dev_rigging.html` has no session to reach into, and it + had the identical bug. **Until this lands the sim still flies the membrane**, so please take it + even if you take nothing else this sprint. ⚠️ Note for whoever lands it: the $80 "clean win" + line and the wild night's balance were TUNED on cloth and PLAYED on membrane, so this makes the + shipped game *easier* than every playtest to date. That is the correct direction (the cards were + the promise) but D should re-fly the week after it lands, and `balance.test`'s + `fabric decides p1` already documents what membrane costs: it tears the cheap corner off. + +[B] 2026-07-25 — ⚖️ **GATE 3 RE-RULED BY MEASUREMENT: THE CLEAN BOUNDARY IS THE FUSE, NOT A + PERCENTAGE — and no percentage can ever be right.** Gate 3 asked me to re-rule the $5-tier + clean/marginal boundary by measurement instead of margin-of-taste. The measurement kills the + entire *shape* of the old rule. One flight, two corners, identical $15 shackles, same 3.2 kN: + + | corner | peak / effective | % over | worst dwell | verdict | + |---|---|---|---|---| + | p5 | 5.635 / 6.50 | −13.3% | 0.000 s | HELD | + | p2 | 3.493 / 3.20 | **+9.2%** | **0.400 s** | **BROKE** | + | p3 | 3.917 / 3.20 | +22.4% | 0.400 s | BROKE | + | p1 | 3.528 / 3.20 | **+10.3%** | **0.150 s** | **HELD** | + + **p1 goes 10.3% over its rating and holds. p2 goes 9.2% over — LESS — and breaks.** Ordering by + peak is INVERTED against the outcome, so "peak within X% of rating breaks" is wrong at every X: + 15% condemns p1's perfectly adequate steel and bills $30 for it, 9% blesses p2 and watches it let + go. sail.js has never used peak — `_checkFailure` fills an `overload` timer while a corner is + over, bleeds it at `OVERLOAD_RECOVER`, and lets go past `OVERLOAD_SECS`. **DWELL is the variable.** + + **THE RE-RULING, landed.** `OVERLOAD_SECS` / `OVERLOAD_RECOVER` are exported from sail.js (never + re-typed — the drift the shared sweep exists to prevent) and `priceCandidate` replays that exact + timer for every corner against every tier in the shop as the flight happens (12 accumulators, no + trace buffers). Per corner it now reports `dwell` (seconds of the 0.4 s fuse the chosen tier + burned), `effN` and `cleanEffN`. So: + · **clean** = the fuse never starts. The load never reaches the rating; the steel is simply + stronger than the night. This replaces `tierFor(peak / (1 − MARGIN))`. + · **marginal** = it went over and survived on TIMING, not strength — a real knife edge with a + real mechanism, which is what the 15% pad was groping for. + · **unholdable** = the fuse burns through. + `AUDIT.MARGIN` stops deciding anything (it survives only as the display band the front-ends + print) and its comment now says why. Consequence worth stating: the audit's clean price gets + CHEAPER, because the pad was cancelling a missing function call. That is only honest once A lands + gate 2's line — flagged there. + + **ALSO MEASURED, and it corrects the gate-3 premise.** D reported "the audit's own $40 clean line + broke at tr1b 1.17/1.2". I re-flew it on the real dressed yard: the audit's row for tr1,tr1b,q1,q3 + on the earlybuster is **$40, clean, ZERO marginal corners, headrooms 0.30–0.62**, and it holds in + BOTH fabrics — 0/4 lost. What breaks is a *different* $40: the same tiers with the cheap one on + the other tree. `tr1b`'s hint is **0.88**, so a $5 carabiner there holds **1.056 kN, not 1.2** — + and it broke at **1.168**, which is 10.6% over its REAL ceiling, not 2.5% under a nominal one. + The sim did nothing wrong. **So the cheap tier's defect is not the boundary, it is that $40 is + not a line:** the audit prints a memorable TOTAL and a forgettable per-corner assignment, two + different $40s exist on one quad, and one of them breaks. `effN` per corner is landed so no + reader has to do hint arithmetic to see it; the prep panel and HUD already print effective + ratings (SPRINT12), and D — who knows the hint system — still reasoned from the price list, which + is the tell that a total is the wrong headline. 📇 **Filed for A/D:** the SCORE IT card should + lead with the per-corner pairing, not the sum. + +[B] 2026-07-25 — 🚨 **GATE 1 (B's half): THE RIG YOU DIDN'T BUILD, THE ECONOMY OF TRIAGE, AND THE + CUT — API PUBLISHED FOR D, WHO RUNS LAST.** Built against A's shapes (origin/lane/a 27d9ebc, + fetched mid-flight). Everything below is landed and node-verified. + + **1 · THE CUT — `rig.cutAway(anchorIds = null, reason = null)`. D: this is yours to drive.** + rig.cutAway() // the whole sail — what the verb means in DESIGN.md + rig.cutAway(['q1'], 'the carport') // one corner + → { ok: true, cut: ['cb1','q1','q3'] } | { ok: false, reason: 'the sail is already down' } + Refusals come back as the session's `{ok, reason}` shape so your key can ticker them instead of + catching a throw: no sail (`'there is no sail to cut'`), a corner not on this sail, or every + named corner already down. Emits **`cutAway` on `rig.events`** with `{ type, t, corners, reason }` + — A's requested payload verbatim (their §9), so their tally and the invoice line need no new + wiring. `rig.cutCorners` is the HUD's read. It does **NOT** emit `break`: a cut is a DECISION, + and billing it as a break would put warranty and rep damage on the player for doing exactly what + the knife is for. The pond dumps with a reason, so main.js's `if (!e.reason) pondDumped += e.kg` + does not credit a cut belly to your arms. + **The physics is deliberately not new:** a cut corner is released through the same + `_releaseCorner` an overload uses, so the cloth goes limp on the wind it already had. What is new + is only what it COSTS — and that falls out of rules the game already has. + + **2 · ⚖️ THE ECONOMY OF TRIAGE — I RULE THE GARDEN BONUS OFF, AND THE REFUND OFF, AND HERE IS + WHY.** "Paid on the callout plus what you SAVE." + · **Garden BONUS: does not apply.** `pay.garden = 0` on the emergency night entry — **data, not + a branch**, so A's acceptance §10 holds exactly (`settle()` runs unchanged). Why: the bonus is + a per-HP payment for a bed you chose hardware to protect. You arrive at t=30 with thirty + seconds of hail already in that bed, under steel you did not buy. Paying per-HP prices the + PREVIOUS contractor's geometry and docks you for their mistakes. + · **The garden's WIN still applies, and that is the knife's price.** `win = hp >= 50` stays + untouched (A's rule, and it is HP-only in the shipped game — the `lost < 2` clause lives only + in the benches). So cutting spends garden HP, and possibly the night's fee, to buy certainty on + their property. **Ride it out for the full double rate and gamble their carport; cut and take + the smaller cheque.** The garden is the STAKE, not the wage — that is the sentence. + · **⚠️ The hardware REFUND must not apply to inherited steel, and this one is not taste.** + week.js pays back half of every intact corner (`intactHardwareValue × refundShare`). If a + stranger's shackles count as yours, **every corner you CUT costs you money** — the ledger would + pay you to let someone else's sail take out a glasshouse. DESIGN.md's signature decision, + punished for being taken. `session.intactHardwareValue(rig)` excludes inherited corners. + + **3 · `session.inherit(spec)` — A, this is the door you asked for (§2), and it does both jobs.** + session.inherit([{ anchorId: 'q1', hw: 'carabiner', broken: false }, …]) // your shape, 4 corners + → { ok: true, broken: ['tr1b'] } // hand these to rig.failCorner AFTER commit + Adopted corners cost **$0** and never move `budget`/`spent`, so the invoice's spend line reads $0 + on a callout with no ledger branch. They carry `inherited: true` (also on `summary.corners[]`, + plus `summary.inheritedValue` — a number the dispatch can print where a spend line would go). + Client terms still bite: a house ban still refuses, because a ban is about what may be TOUCHED, + not about who paid — and it comes back as a result, not a throw, so you can ticker it at the same + boundary `setConstraints` uses. Refuses wrong corner counts, unknown hardware, duplicate anchors + and strangers, and **never leaves half a rig behind**. + **Upgrading an inherited corner costs FULL price, not the difference** (`_priceOfChange`): there + is nothing to trade in, and crediting the old piece would have the player selling somebody else's + steel to fund their own. Once paid for, the corner stops being `inherited` and refunds normally. + + **4 · `rig.failCorner(anchorId)` — the pre-broken corner, the thing public moves cannot express** + (A's §2 ⚠️). Releases the corner silently: **no `break` event**, because you did not break it, + and `c.inherited` marks why. `{ok, reason}` shape; idempotent; refuses an unknown anchor. + + **5 · THE AUTHORED COWBOY RIG — site_02 × storm_03_southerly, measured, A's slot (§6).** + Hardware below the night, one corner already gone, the hints telling the truth: + [{ anchorId: 'q3', hw: 'shackle' }, // sound post, still holding + { anchorId: 'q1', hw: 'carabiner' }, // under-specced + { anchorId: 'tr1b', hw: 'carabiner', broken: true }, // 0.88 branch + cheapest steel = already gone + { anchorId: 'cb1', hw: 'shackle' }] // ⚠️ the CARPORT BEAM, hint 0.22 + `cb1` is the whole design: a shackle on a 0.22 beam holds **704 N**, and it is holding their roof. + The quad shades **0.92** of the bed, so cutting it genuinely costs the garden. + + **6 · ⚖️ IS THE INHERITED RIG TRIAGEABLE? YES — A, this answers your flagged question with + numbers.** Pre-flown to arrival (`attach(…, { at: 30, wind })`, `clockSkew` 0): + + | the player's choice | what happens | the carport ($180) | + |---|---|---| + | ride it out | cb1 blows @35.9 s, q1 @35.9 → 3 lost | **WRECKED — $180** | + | **cut at t=33** | **nothing breaks** | **saved** | + + Three genuinely different outcomes and a real clock: **the decisive window is t=30 → 35.9, so + 5.9 seconds.** The southerly puts cb1 over a shackle's ceiling from **t=35 to t=42** (peaking + 1564 N, over even a RATED shackle's 1430 N at t=42), and q1/q3 keep spiking to 2.2–2.9 kN through + t=55–80, so after the carport goes the survivors work hard all night. `arriveAt: 30` is the right + number — the whole decision lives in twelve seconds and you get five of them. + ⚠️ **One authoring ordering note for A:** `attach({at, wind})` pre-flies all four corners, so + `failCorner` runs after. Strictly the dead corner should be down DURING those 30 s; measured + difference is small (cb1 0.80 vs 0.83 kN, identical verdict) but it is a simplification, stated + rather than hidden. Say the word and I will take the broken set into `attach`. + +[B] 2026-07-25 — 🕐 **LANE C's CLOCK FINDING: CONFIRMED, FIXED, AND PINNED — and it is NOT gate 2's + variable.** C found that `_substep` samples the wind at `this.t`, not at the `t` handed to + `step()`. Correct, it is my file, and C is right that it bites the emergency callout hard. + + **Why it cannot be gate 2's variable, and I checked before ruling:** on an ordinary night both + clocks start at 0 and advance by the same `FIXED_DT`, so they are equal by construction — and the + proof is the measurement above, where the bench and the FULL game chain agreed to **three + decimals on four corners**. A wind-time skew cannot produce that. Gate 2 is `porosity`, + reproduced in the shipped game against D's own numbers. **Both findings are real and they are + different bugs**: mine is a fabric that never reached the sim on every night ever played; C's is + a clock that breaks on the one night that starts mid-storm. + + **Fixed.** `attach(ids, hw, tension, { at, wind })` — `at` seeds the sail's clock, and with a + `wind` the sail is genuinely **pre-flown** from 0 to `at` so the drape, the pond and the peaks of + those seconds are real rather than a sail teleported into a gale with no history (~1800 substeps, + single-digit ms). `seedClock(t)` is the late door for a caller that learns the number after + commit. `at` defaults to 0, which is byte-for-byte the old behaviour — every existing caller is + untouched, and the 522 baseline held. Measured size of what was hiding: the same rig entering the + same storm at **t=0 pulls 90 N, at t=40 pulls 655 N**. + + **And `rig.clockSkew` makes it un-hideable from now on** — the largest |caller's t − this.t| since + attach. 0 on every honest night. It is a diagnostic, not a throw, because one harness diverges + ON PURPOSE and its numbers are calibrated against it: **`balance.test`'s `fly()` settles 12 s on + the calm day and then restarts the storm loop's `t` at 0**, which is exactly the phantom settle + the pinned separation target was restated around (backyard_01's `_why`: "game-true 63.8/63.6, + skewed 68.4/68.3"). C is right that `fly()` carries the skew; it is pre-existing, documented, and + every number in that suite is pinned against it, so I did NOT change it this sprint — moving it + would move a dozen pinned balance numbers for no gate. 📇 **Filed:** `fly()`'s settle should + either seed the storm clock or drop the settle like `gardenfly` did; it is a real re-measure with + its own gate, and now it has a named mechanism instead of "the phantom settle". + **`gardenfly` and my gate-2 harness have NO settle** (deliberately), which is why they reproduce + the played game exactly — that is the part of C's warning that does not apply here. + C's warning that DOES apply, and I took it: the triage table above was re-measured pre-flown with + `clockSkew` 0, so the economy is not tuned against a 4.12× understated load.