Lane B S18: [B] THREADS — gate 2's variable, gate 3's re-ruled boundary, the cut API for D, C's clock
This commit is contained in:
parent
4718548c38
commit
ae2f560ff2
233
THREADS.md
233
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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user