diff --git a/THREADS.md b/THREADS.md index eaf2f43..08a50d0 100644 --- a/THREADS.md +++ b/THREADS.md @@ -9194,3 +9194,49 @@ anchors are your GLB), but the tooling is now waiting, not TODO. scratch merge judged the wider tree (main + the newest lane/b + lane/c) at **517/0/0**, per-lane sum verified, nothing lost in the merge β€” flagged for the integrator as the number to reproduce when b/c's newest commits and this pool yard all land together. + +[B] 2026-07-21 β€” πŸ¦€ **GATE 3 LANDED: THE CORRODED TIER PRICES HONESTLY IN THE CARD β€” the piece the + 0.2/0.3/2 pass deferred, now pinned. Plus: I re-ran 0.2/0.3/2 independently and they STAND.** + A recovered/parallel session (this one) rebuilt gates 0.2/0.3/2 from 75d5f9a before finding the + landed work on origin/lane/b. Rather than overwrite three pushed, receipted, mutation-checked + commits, I **reset to the landed versions and added only gate 3** β€” the one thing the earlier + pass explicitly filed as DEFERRED ("gate 3 support is armed and DEFERRED as briefed… Corroded + steel prices through ratingHint, which every sweep already reads live"). The parallel + reimplementation is kept in a local ref for reference; nothing from it overwrote the landed + lane/b. selftest still green (node sail 47/47, rigging 29/29 = 27 + the 2 gate-3 pins). + + **What gate 3 pins**, off E's REAL resolver, not a hand-typed 0.55: + Β· **The STEEL half** (`rigging.selftest`): `factoryExtras('corroded_post')` β†’ hint 0.55 + + collateral key, and `_effRating` on a corroded corner is `rating Γ— 0.55` β€” so a load a + carabiner holds on a sound post (1200 N) is OVER a carabiner on a corroded one (660 N). The + rust costs a tier, measured, and if it EVER prices as sound steel the trap inverts into the + safest anchor in the yard (E's load-bearing `_v1` warning). + Β· **The COLLATERAL half** (`gardenfly.selftest`, a real dressed world, ~40 ms): a node-less + corroded placement (E's "plain entry, no node field" case β€” the harder one, rating and + collateral both from the factory) β†’ `world.collateralFor('corroded_post')` = $45 "the + corroded post", summed by `collateralExposure` β€” NOT the baked GLB value read directly, the + shortcut that priced the carport to null for five sprints. + **Mutations** (run on the scratch merge where the tier is live): `collateralFor` drops the + price β†’ card reads null; `factoryExtras` returns hint 1 β†’ the steel pin reddens. Both caught. + + ⚠️ **CROSS-LANE SEAM, disclosed.** The corroded tier (0.55, the GLB, the gen entry) is on + lane/e. On bare lane/b `factoryExtras('corroded_post')` is null, so **both gate-3 pins + SKIP-WITH-DISCLOSURE here** and go live at integration β€” the "build against the shape as + pushed" pattern. Measured GREEN on a scratch merge of lane/a + lane/e (hint 0.55, collateral + $45). Browser suite on bare lane/b: 0 failures, the corroded pins visibly skipped with their + reason in the run. + + **D β€” your pool yard's corroded corners price right the moment you place them**, node or + node-less; the card names the $45 collateral and the 0.55 steel with no round-trip to me. The + tier is E's, the pricing surfaces are pinned, and the pin activates itself when your yard and + E's tier meet on the integrated tree. + + **On 0.2/0.3/2 (the landed pass): reviewed, sound, nothing to change.** One optional + strengthening I found while reimplementing, filed NOT forced β€” the 0.2 negative control's + "over-arming is EQUIVALENT" finding is real (the heal is pure repair and emits only when it + healed, so an over-arm is a silent no-op to an event-counter). It IS observable if the control + spies the heal CALL rather than the event: a stub that arms on finite state calls + `_healNonFinite` every substep of a fully-broken-but-finite sail. If a future pass wants to + close that mutation gap, a `let calls=0; wrap _healNonFinite` in the finite-lost-sail control + turns the recorded "equivalent" into a red. Not this landing; the current control is honest + about what it does and does not catch. diff --git a/tools/site_audit/gardenfly.selftest.js b/tools/site_audit/gardenfly.selftest.js index f2b8ff3..2791ba5 100644 --- a/tools/site_audit/gardenfly.selftest.js +++ b/tools/site_audit/gardenfly.selftest.js @@ -33,6 +33,15 @@ import { createSkyFx } from '../../web/world/js/skyfx.js'; import { createGarden } from '../../web/world/js/garden.js'; import { FIXED_DT } from '../../web/world/js/contracts.js'; import { flyGarden, flySeparation, hardwareByName } from './gardenfly.js'; +import { buildScoringWorld, collateralExposure } from './scorecard.js'; +import { factoryExtras } from '../../web/world/js/ratings.js'; + +// SPRINT17 gate 3 [B] β€” CROSS-LANE SEAM. The corroded tier (rating_hint 0.55, +// the GLB, the gen entry) lives on lane/e. On a bare lane/b it is absent, so +// this pin SKIPS WITH A DISCLOSURE and goes live at integration β€” the same +// "build against the shape as pushed" seam the sprint runs on. Measured GREEN +// on the scratch merge (lane/a + lane/e): hint 0.55, collateral $45. +const CORRODED_TIER_LIVE = !!(factoryExtras('corroded_post')); const assert = (cond, msg) => { if (!cond) throw new Error(msg); }; @@ -156,7 +165,62 @@ export async function buildGardenflyTests() { fabRun = { byDefault: flyFab(undefined), cloth: flyFab(0.30), membrane: flyFab(0) }; } catch (err) { fabErr = String((err && err.stack) || err); } + // SPRINT17 gate 3 support [B]: the corroded tier priced through the CARD'S + // OWN path, in a real dressed world. E landed the post; the 0.2/0.3/2 pass + // deferred gate 3; this pins that a corroded anchor prices honestly the + // moment it is placed. Node-less placement (E's THREADS: "place with a plain + // entry and no node field and the rating still lands") β€” the harder case, + // where the rating and the collateral both come from the factory, not a + // dress. buildScoringWorld only DRESSES (no sweep), so this is ~40 ms. + let corRun = null, corErr = null; + try { + if (!CORRODED_TIER_LIVE) throw { skip: true }; + const corSite = await loadSite('backyard_01'); + corSite.structures = [...(corSite.structures ?? []), { + id: 'cor1', model: 'sail_post_corroded_v1', wreckedModel: 'sail_post_corroded_wrecked_v1', + x: 6, z: 2, rotYDeg: 0, solid: true, + collateralKey: 'corroded_post', collateralValue: 45, collateralLabel: 'the corroded post', + anchors: [{ id: 'cor1_a1', type: 'corroded_post', work: 'cloth' }], + }]; + const built = await buildScoringWorld(corSite); + const anc = built.anchors.find((a) => a.id === 'cor1_a1'); + const col = {}; + for (const a of built.anchors) { + if (!a.collateral) continue; + const p = built.world.collateralFor(a.collateral); + col[a.id] = p ? { key: a.collateral, cost: p.cost, label: p.label } + : { key: a.collateral, cost: null, label: a.collateral, unpriced: true }; + } + corRun = { anchor: anc, priced: built.world.collateralFor('corroded_post'), + exposure: collateralExposure({ collateral: col }), dressed: built.dressed, dressError: built.dressError }; + } catch (err) { if (err && err.skip) corErr = 'skip'; else corErr = String((err && err.stack) || err); } + return [ + ['gate 3: a corroded post prices honestly in the card β€” 0.55 hint, $45 collateral, dressed', () => { + if (!CORRODED_TIER_LIVE) return 'SKIPPED β€” corroded tier lands with lane/e; measured GREEN on ' + + 'the scratch merge (hint 0.55, collateral $45 through collateralFor)'; + if (corErr) throw new Error(`corroded pricing died: ${corErr}`); + assert(corRun.dressed, `the corroded world did not dress (${corRun.dressError}) β€” GLB missing or broke`); + const a = corRun.anchor; + assert(a, 'the corroded anchor cor1_a1 was not created'); + // The STEEL half, node-less: factoryExtras had to reach it, or the tier + // math prices this trap as the best steel in the game (adoptAnchor's + // `?? 1`, E's load-bearing _v1 warning). + assert(a.ratingHint === 0.55, `corroded anchor rated ${a.ratingHint}, not 0.55 β€” a node-less placement ` + + 'lost E\'s factory hint and the card would price it as sound steel'); + assert(a.collateral === 'corroded_post', `corroded anchor collateral "${a.collateral}", not "corroded_post"`); + // The COLLATERAL half, through the card's OWN resolver and exposure sum β€” + // not the baked GLB value read directly (the carport-to-null shortcut). + assert(corRun.priced && corRun.priced.cost === 45, + `world.collateralFor('corroded_post') gave ${JSON.stringify(corRun.priced)}, expected $45`); + assert(corRun.priced.label === 'the corroded post', `collateral label "${corRun.priced.label}"`); + const line = corRun.exposure.priced.find((e) => e.key === 'corroded_post'); + assert(line && line.cost === 45, + `collateralExposure has no $45 corroded line (${JSON.stringify(corRun.exposure.priced.map((e) => [e.key, e.cost]))})`); + assert(!corRun.exposure.unpriced.some((e) => e.key === 'corroded_post'), + 'the corroded post landed in UNPRICED β€” the exposure total silently drops its $45'); + return `corroded_post: hint ${a.ratingHint}, collateral $${corRun.priced.cost} "${corRun.priced.label}", in the exposure sum`; + }], ['gardenfly: the pinned separation line, flown in BOTH chains β€” the 12s-skew disagreement, pinned', () => { // ⚠ THE STATE OF PLAY (2026-07-18, [B] THREADS, receipts in the entry): // the pinned recipe HOLDS and rigging matters by a full state's width β€” diff --git a/web/world/js/rigging.selftest.js b/web/world/js/rigging.selftest.js index 5e27f07..a02c9bb 100644 --- a/web/world/js/rigging.selftest.js +++ b/web/world/js/rigging.selftest.js @@ -9,6 +9,7 @@ import { RiggingSession, FABRIC, fabricNoteFor } from './rigging.js'; import { SailRig, TENSION_MIN, TENSION_MAX } from './sail.js'; import { HARDWARE, START_BUDGET, SPARE_COST } from './contracts.js'; +import { factoryExtras } from './ratings.js'; const [CARABINER, SHACKLE, RATED] = HARDWARE; @@ -494,6 +495,66 @@ test('gate 2: the constraint door is a checked enum, and the terms survive reset return 'five malformed shapes thrown out at the door; terms survive reset, cleared only explicitly'; }); +// --- SPRINT17 gate 3 support: THE CORRODED TIER PRICES HONESTLY ------------- +// E landed the corroded post (rating_hint 0.55, its own ANCHOR_TYPE word, a +// $45 collateral); the gates-0.2/0.3/2 pass DEFERRED gate 3, and this is it. +// B owns the pricing surfaces the card reads, so the number the shop charges +// for a corroded corner is pinned HERE, off E's real resolver β€” not a +// hand-typed 0.55. The collateral half ($45 through world.collateralFor β†’ the +// exposure line) is pinned in gardenfly.selftest, where a dressed world exists; +// this is the STEEL half: a corroded post is weaker, so the same load costs +// MORE steel to hold, which is what "prices honestly" means and is B's tier +// math (_effRating), not E's mesh. +// +// ⚠️ CROSS-LANE SEAM. The corroded tier lives on lane/e; on lane/b in +// isolation `factoryExtras('corroded_post')` is null (no gen entry, no GLB). +// These pins SKIP WITH A DISCLOSURE on a bare lane/b and go live the moment +// lane/e merges β€” the "build against the shape as pushed" seam the sprint runs +// on. Measured GREEN on the scratch merge (lane/a + lane/e): hint 0.55. +const CORRODED_TIER_LIVE = !!(factoryExtras('corroded_post')); +const CORRODED_SKIP = 'SKIPPED β€” corroded tier is on lane/e, absent on bare lane/b; this pin is ' + + 'inert here and goes live at integration (measured GREEN on the scratch merge, hint 0.55)'; + +test('gate 3: the corroded tier is 0.55, read off E\'s resolver not a literal', () => { + if (!CORRODED_TIER_LIVE) return CORRODED_SKIP; + const ex = factoryExtras('corroded_post'); + assert(ex && ex.ratingHint === 0.55, + `factoryExtras('corroded_post') gave ${JSON.stringify(ex)} β€” the card reads this for a node-less ` + + 'corroded placement, and if E re-rules the number it must move here, not be re-typed'); + assert(ex.collateral === 'corroded_post', + 'the corroded anchor carries no collateral key β€” the exposure line would read "not scored" over a $45 trap'); + return `corroded_post β†’ hint ${ex.ratingHint}, collateral "${ex.collateral}"`; +}); + +test('gate 3: a corroded corner costs MORE steel than a sound post at the same load', () => { + if (!CORRODED_TIER_LIVE) return CORRODED_SKIP; + // _effRating is what the panel's arrow and the audit's tier both price + // against: hardware.rating Γ— the anchor's ratingHint. A corroded post fails + // at 0.55 of the steel's spec, so a load a carabiner holds on a sound post + // may need a shackle on a corroded one. Built off E's real hint. + const hint = factoryExtras('corroded_post').ratingHint; + const SOUND = { id: 'ps', type: 'post', pos: { x: -6.4, y: 3.9, z: 7.4 }, sway: () => ({ x: -6.4, y: 3.9, z: 7.4 }) }; + const COR = { id: 'pc', type: 'corroded_post', pos: { x: 5.3, y: 3.9, z: 8 }, ratingHint: hint, + sway: () => ({ x: 5.3, y: 3.9, z: 8 }) }; + const s = new RiggingSession({ anchors: [SOUND, COR, ...ANCHORS] }); + s.rig('ps'); s.rig('pc'); s.rig('h1'); s.rig('t1'); + const sound = s._effRating(s.pickOf('ps')); + const cor = s._effRating(s.pickOf('pc')); + // Both start on carabiners (rating 1200): the sound post holds 1200 N, the + // corroded holds 1200 Γ— 0.55 = 660 N. Same steel, less strength β€” measured. + assert(sound === CARABINER.rating, `sound post effRating ${sound}, expected the carabiner's ${CARABINER.rating}`); + assert(Math.abs(cor - CARABINER.rating * hint) < 1e-9, + `corroded effRating ${cor}, expected ${CARABINER.rating} Γ— ${hint} = ${CARABINER.rating * hint}`); + assert(cor < sound, + 'a corroded post held as much as a sound one β€” the trap is priced as safe steel, which inverts it'); + // The tier consequence: a load in the corroded gap (660–1200 N) is HELD by a + // carabiner on the sound post and OVER a carabiner on the corroded one. + const load = 900; + assert(load < sound && load > cor, + `pick a load in the corroded gap (${cor}..${sound} N) so the tier difference is real`); + return `same carabiner: sound holds ${sound} N, corroded holds ${cor} N β€” the rust costs a tier`; +}); + export const RIGGING_TESTS = TESTS; export function runRiggingSelftest() {