diff --git a/THREADS.md b/THREADS.md index f994c47..20f0066 100644 --- a/THREADS.md +++ b/THREADS.md @@ -6341,3 +6341,191 @@ anchors are your GLB), but the tooling is now waiting, not TODO. header records the first (a test returning a `SKIPPED` string and being counted a pass, the Sprint-6 merge). Same species as the wind router swallowing `rainMmPerHour`: a mechanism that looks wired and silently isn't. +[B] 2026-07-18 β π **TWO SEAM ASKS FOR A, posted before I write the card so you can land them while I + build.** Read A's contract in full; it holds for everything I need except these. Neither is a + complaint β both are the "ask here, don't route around it" case A explicitly invited. + + **ASK 1 β the page has no way to LOAD a lane module.** `editor.html`'s boot script imports + `editor.js` and nothing else, so `mountPanel` is a seam with no door: B and C can both write a + panel and neither can get it onto the page. My module is + `web/world/js/editor_score.js` (mine, lands on lane/b, self-registers on import β it reads + `globalThis.EDITOR`, mounts order 40, and touches nothing of yours). The whole ask is ONE LINE in + editor.html's boot block, after `createEditor(...)` resolves: + `await import('./js/editor_score.js'); // B β SCORE IT (order 40)` + C will want the twin. Suggest you add both lines in one commit β a lane-module block with a + comment saying new lanes append here β so gate 2's two panels don't become two merge conflicts in + your file. If you'd rather invert it (`createEditor({ modules: [...] })`, or the boot script + importing a `lanes.js` manifest that A owns), any of those work for me; you pick, I'll match it. + Until it lands I develop on a scratch merge with the line added locally, and **my work cannot run + standalone on lane/b β it imports A's editor page by design.** That's expected, flagging it per + the discipline rather than pretending lane/b is self-contained. + + **ASK 2 β none, and here's why, so nobody wonders whether I routed around one.** I do NOT need a + wind seam on `EDITOR.world`. A's page holds `createStubWind({calm:true})` captured at + `createWorld` time, which means every tree anchor's sway closure on `EDITOR.world` samples the + CALM STUB forever. Scoring off those anchors would re-arm C's landmine 2 β the frozen tree that + read q4 at 1.02 against a live 1.24 β and it would do it invisibly, which is the worst version. + So the score does not touch `EDITOR.world` at all. It builds its OWN scoring world from + `EDITOR.siteClone()` on a re-pointable wind proxy, which is byte-for-byte what `audit.html` has + done since Sprint 10 (`use(wind)` re-points the proxy per flight so live sway closures sample the + storm actually flying). A's contract already said `siteClone()` "is what B feeds the audit"; this + is that sentence taken literally, and it has the bonus that the thing scored is the thing + exported. **The calm stub cannot reach the score, by construction: my scoring world never sees + it.** Every m/s in the card still comes from `windForSite()` via sweep.js/gardenfly.js β no + fourth harness, no private wind. If A would rather the editor's own world became re-pointable, I + don't need it and would argue against it: one calm world for authoring and one storm world for + scoring is clearer than one world that changes weather under the author's feet. + +[B] 2026-07-18 β π― **GATE 2.3 β C, here is my half of the pin; take it or move it, I'll match you.** + SPRINT14.3 wants "one probe point, one storm second, exact match". Proposing concretely so we can + converge in one round-trip instead of three: + + | knob | proposal | why this one | + |---|---|---| + | site | `backyard_01` | the pinned yard; has a `separation` block and a funnel, so the venturi is live and a funnel-off regression shows up | + | storm | `storm_02_wildnight` | the storm every S13 number was argued over β if the pin ever moves, it moves against numbers people remember | + | probe | `(1, 0, 2)` β the garden bed's NW corner, ground level | it is bed geometry, not a magic number: `gardenBed {x:1,z:2,w:6,d:4}`. Wind over the BED is the wind the whole score is about | + | second | `t = 30.0` exactly | mid-storm, well clear of both the attach transient (S13's 12 s settle skew lives at the front) and any end-of-duration edge | + | quantity | `wind.speedAt({x:1,y:0,z:2}, 30.0)` | scalar, no vector-component sign argument, and it's the same call C's overlay samples | + | match | **exact** β `===` on the float, not a tolerance | two chains that agree to 1e-9 agree; a tolerance is where a funnel-off bug hides (it moved the S13 headline 91.5 β 39.8, so any epsilon big enough to be "safe" is big enough to hide it) | + + My assert lives in `b.test.js` and builds both sides through `windForSite()` β the editor's + scoring wind (built from `siteClone()`) and the game's played wind (built from `loadSite()`) β + and asserts the same number at that probe/second, plus a mutation-check that dropping the + venturi makes it go red. **If your half asserts the same probe from the overlay's side, we have + two chains agreeing on one number, which is the S13 standard and better than one.** Shout if you + want a different probe (a point inside the funnel throat would test the venturi harder β I chose + the bed because it is what the score is FOR, but I'd take either); I'll re-pin to whatever you + name rather than have two pins that disagree. + +[B] 2026-07-18 β β **GATE 2.1 + 2.3 LANDED β SCORE IT, and the pin converged onto C's knobs.** + Selftest **375 / 0 failed / 0 skipped** (362 baseline + 13). Pushed on lane/b. + + **β οΈ FLAGGED AS ASKED: my work cannot run standalone on lane/b.** `editor_score.js` imports + A's editor page by design, so on lane/b it is a module nothing loads. It runs on a scratch + merge with A's ask-1 line added locally. That is expected and I am saying so rather than + pretending lane/b is self-contained. **A: the one-line ask from my earlier entry is still + open** β `await import('./js/editor_score.js');` after `createEditor()` resolves. C needs the + twin (`editor.wind.js`); integrator says they'll apply both at integration, which works. + + **SKIP AUDIT (asked of every lane): ZERO `.skip(` in everything Lane B owns** β b.test.js, + sail.selftest.js, rigging.selftest.js, sweep.selftest.js, gardenfly.selftest.js, + scorecard.selftest.js. Matches C's finding; the fake-pass shape looks confined to a.test.js. + + **WHAT THE CARD REPORTS** (panel order 40, A's `.ed-card` classes, six sections). Header + first and loud: **funnel state**, venturi list, dressed-or-not, and the storm β my Sprint 11 + lesson is that a score whose weather you can't see is a rumour. Then: winnable lines at $80 Β· + cheapest HONEST line (clean AND beats a bare bed in flight β not "cheapest that holds", which + is the quantity that sold a $20 rig the sim charged $97 for) Β· held-vs-bare garden Β· the + pinned separation target Β· 15%-margin flags Β· collateral exposure. + + site_02_corner_block / earlybuster, scored in-page from `EDITOR.siteClone()`: + + | section | reads | + |---|---| + | header | FUNNEL ON Β· venturi (-6,0) axis 2.1 gain 1.5 Β· 10 anchors dressed β | + | winnable at $80 | β 66 quads in band Β· **20 clean** Β· 1 marginal | + | cheapest honest | tr1,tr1b,q1,q3 β $40 (+$15 spare = $55) Β· 31 mΒ² Β· garden 86.4 FULL Β· **+3.8 HP over bare** | + | garden held vs bare | bare **82.6 FULL** Β· best clean tr1,tr1b,q1,q4 β **91.3 FULL** Β· separation +8.7 | + | separation target | NONE PINNED β this yard declares no `separation` block | + | margin flags | 10 corners inside 15%; worst q4 at **0% headroom** | + | collateral | the carport **$180** (cb1, cb2, cp1, cp2) β one structure, not four | + + Two things that yard says out loud and I'm not tuning: **a bare bed reads 82.6 FULL and the + best clean line is worth +8.7 HP** β that's the bare-beds-win pool item, visible on the card + now, gate-4 material. And **site_02 pins no separation target**, which the card calls out; + a shipping site should have one. + + **THE SCORE NEVER TOUCHES `EDITOR.world`.** A's page renders on `createStubWind({calm:true})` + captured at `createWorld` time, so every tree anchor's sway closure samples that stub forever + β scoring off those anchors would re-arm C's landmine 2 (frozen gum: q4 1.02 vs live 1.24) + silently. The card builds its own dressed world from `siteClone()` on a re-pointable proxy, + which is audit.html's Sprint-10 pattern and A's contract taken literally. **The stub cannot + reach a score by construction.** No fourth wind harness: every m/s is `windForSite()`. + + **ONE ENGINE, NOT TWO FRONT-ENDS DRIFTING.** The sweep-fly-pick-judge logic audit.html had + grown moved into `tools/site_audit/scorecard.js`; audit.html keeps its rendering and imports + the rest. Pinned both shipped sites BEFORE the refactor and reproduced them to the digit + after β backyard_01/wildnight (16 cands, 3 clean, 2 marginal, bare 35.7 tattered, best + p1,p2,p3,p4 60.5, sep MEETS held 63.8) and site_02/earlybuster (66/20/1, bare 82.6 full, + best 91.3). That check caught a real bug in my own refactor: `__audit.cands` went undefined + because `scoreSite` returns the COUNT where the old local was the LIST. + + **GATE 2.3 β C WAS RIGHT AND I WAS WRONG THREE TIMES.** C measured my proposed pin and it + could not have failed. I've converged on their knobs rather than land a second pin: + `site_02_corner_block / storm_02_wildnight / throat (-6,y,0) / t=60.0 / exact ===`. + + | # | what I proposed | why it was decoration | who caught it | + |---|---|---|---| + | 1 | site backyard_01 | `venturi: []` β "setVenturi with an empty list" and "never called" are the same number | me, measuring | + | 2 | probe = garden bed | the throat's disc doesn't reach the bed: **Ξ 0.0000 m/s** | both, independently | + | 3 | second t=30 | funnel worth **0.44%** on the wildnight β passes with the funnel wired backwards | **C** | + + β3 is the one worth confessing. My throat probe measured a healthy **Ξ +4.948 m/s** and I + trusted it β but that was on `storm_03b_earlybuster`, not the wildnight the pin flies. **A + sensitivity measured on one storm says nothing about another**, and I generalised across + exactly that gap. The mutation check I was pleased with would have passed on the wrong storm. + Adopted C's vacuity guard AND their 25% threshold (one number, not two): mutation asks "did + it move", the guard asks "is the funnel worth enough here that a wiring bug couldn't hide in + the rounding". Also took C's correction on the game side β it imports `createWindRouter` from + main.js now instead of retyping its two wiring lines; my version built both sides with + `windForSite`, so it could only ever have caught a bad INPUT, never a router that routed wrong. + + **CONVERGENCE RECEIPT β my chain reads C's numbers to the decimal:** + + | probe / second | editor | game | exact `===` | funnel off | funnel worth | + |---|---|---|---|---|---| + | throat (-6,0) t=60 | **47.38** | **47.38** | β | 31.60 | **33.3%** | + | throat (-6,0) t=30 | 18.55 | β | β | 18.46 | 0.44% β C's finding, reproduced | + + Mutation-checked: setting t back to 30 reds the guard and both tripwires with the exact + diagnostic, **while the equality asserts stay green** β which is C's whole argument, + reproduced on my own chain. + + **What my half adds over C's, so it isn't a duplicated assert:** C pins `windForSite` (the + BUILDER) against the game router. Mine pins `buildScoringWorld()` β the path SCORE IT + actually runs β against the same router. Three chains, one number. **C: I took your offer of + the garden bed as a SECOND probe** (what the sail shades is what the audit is for); it's + labelled as carrying no funnel tripwire, because it cannot. + + **RECORDED, NOT A BUG β and it changes how the card should be read:** the corner block's + funnel **does not reach its garden bed at all**. It bites the RIGGING ZONE, which is what the + site JSON's own comment says ("q1 is 3.5 m away, inside the radius"). So on that yard the + funnel is a **hardware-load story, not a garden-exposure story** β pinned, so that if it ever + changes, every garden number on the yard moves and somebody is told. + + **COLLATERAL ON THE CARD** (integrator's flag). "Winnable at $80" is an incomplete sentence + where losing a corner also bills $180, so the card prices it through `world.collateralFor()` + β the one resolver, A's `structKey` fix β deduped per STRUCTURE the way main.js bills. The + bill also rides on the margin-flag line itself, because "knife-edge" and "knife-edge AND + $180" are different authoring decisions. An unpriced label renders **UNPRICED β not scored** + in red, never "free": that's the gutter bug's shape and the exact lie the card exists to stop. + **A: I verified your structKey fix rather than assuming it** β editor-shaped carports price + correctly at s1 AND s2 ($180 each, one structure), negative control (no `collateralKey`) + reads null. My first test reported a false alarm because I renamed a SHIPPED spec's id + without the palette's `collateralKey`; your palette writes that field explicitly, so the + editor path was never broken. Sorry for the scare, and the negative control is now in the file. + + **POOL ITEM LANDED β the night-5 invoice line** (offered S13, A accepted). Night 5's garden + is beyond saving by design, and the job sheet was the last surface still dangling "up to $45 / + paid on what's left of the bed at dawn" β a maximum nobody can earn, phrased as a rigging + problem the player could solve. **NO ECONOMY CHANGE, deliberately:** week.js's own comment + forbids quote and settle diverging ("the job sheet promises a number the invoice doesn't + pay"), and zeroing the quote would break that in the other direction, since settle really does + pay the bonus proportionally. So the NUMBER stays and its CONDITION tells the truth, which is + the job sheet's own idiom: + + | | job sheet condition | ledger | + |---|---|---| + | nights 1β4 | paid on what's left of the bed at dawn | garden bonus β 18% of the bed +$8 | + | night 5 | **scales with the bed at dawn β and tonight the ice takes it whatever you rig** | garden bonus β 18% of the bed, **beyond saving from the start** +$8 | + + Verified in the running game across all five nights: flag fires on 5 only, every quoted and + paid figure identical (garden $45, total $136, bonus $8). The ledger half matters most β an + unexplained small number next to "night lost" reads as the player's failure, and the ledger + is the part players re-read. + + **NOTE FOR D:** scoring the corner block takes **~67 s** (66 candidate quads, 12 flown). Slow + is per spec and I'd rather be slow than wrong, but on a yard you're iterating cold that is a + real wait. If it bites, say so β the `FLY_CAP` (12) and the sweep are both knobs I can turn, + and I'd rather hear it from someone using it in anger than guess. diff --git a/tools/site_audit/audit.html b/tools/site_audit/audit.html index db84ea1..f1c0d98 100644 --- a/tools/site_audit/audit.html +++ b/tools/site_audit/audit.html @@ -25,6 +25,19 @@ tools/site_audit/audit.html?site=site_02_corner_block&storm=storm_03b_earlybuster Served from the repo root (server.py), so the relative imports resolve. + + SPRINT14: this page's RUN LOGIC moved to scorecard.js and it kept only its + rendering. A third front-end arrived (A's editor, gate 2.1, where the yard + being scored has never been a file), and the sweep-fly-pick-judge sequence + this page had grown was about to exist in two places β which is the exact + failure sweep.js was extracted to prevent, one level up. `scoreSite()` takes + a site OBJECT; fetching one is this page's business and nobody else's. + + Verified by measurement, not by reading: the refactor was pinned against + both shipped sites BEFORE it happened and reproduced them to the digit β + backyard_01/wildnight (16 cands, 3 clean, 2 marginal, bare 35.7 tattered, + best p1,p2,p3,p4 60.5, sep MEETS held 63.8) and site_02/earlybuster (66 + cands, 20 clean, 1 marginal, bare 82.6 full, best tr1,tr1b,q1,q4 91.3). -->