Merge remote-tracking branch 'origin/lane/b'

# Conflicts:
#	THREADS.md
This commit is contained in:
type-two 2026-07-21 06:21:16 +10:00
commit 3717ef0d54
3 changed files with 171 additions and 0 deletions

View File

@ -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**, 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 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. 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.

View File

@ -33,6 +33,15 @@ import { createSkyFx } from '../../web/world/js/skyfx.js';
import { createGarden } from '../../web/world/js/garden.js'; import { createGarden } from '../../web/world/js/garden.js';
import { FIXED_DT } from '../../web/world/js/contracts.js'; import { FIXED_DT } from '../../web/world/js/contracts.js';
import { flyGarden, flySeparation, hardwareByName } from './gardenfly.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); }; 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) }; fabRun = { byDefault: flyFab(undefined), cloth: flyFab(0.30), membrane: flyFab(0) };
} catch (err) { fabErr = String((err && err.stack) || err); } } 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 [ 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', () => { ['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 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 — // the pinned recipe HOLDS and rigging matters by a full state's width —

View File

@ -9,6 +9,7 @@
import { RiggingSession, FABRIC, fabricNoteFor } from './rigging.js'; import { RiggingSession, FABRIC, fabricNoteFor } from './rigging.js';
import { SailRig, TENSION_MIN, TENSION_MAX } from './sail.js'; import { SailRig, TENSION_MIN, TENSION_MAX } from './sail.js';
import { HARDWARE, START_BUDGET, SPARE_COST } from './contracts.js'; import { HARDWARE, START_BUDGET, SPARE_COST } from './contracts.js';
import { factoryExtras } from './ratings.js';
const [CARABINER, SHACKLE, RATED] = HARDWARE; 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'; 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 (6601200 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 const RIGGING_TESTS = TESTS;
export function runRiggingSelftest() { export function runRiggingSelftest() {