Lane D S18 gate 1.3: the knife's pins — including one for the feature-detect that lied

Ten asserts in d.test.js. The reach rule (a post cleat yes, the carport beam
never, up a ladder never), the hold and its three interruptions, the read with
both halves of the trade priced through main.js's OWN collateral walk, and the
money split with a vacuity guard in both directions — the same four corners cut
cost $0 and blown cost $205, because a pin where both cases read zero is a pin
that is not working.

Two that exist because of what playing the night found. The shipped emergency's
rig is pinned as DATA — cb1 is a shackle and it is FLYING when you arrive,
because a carport corner already on the ground is a receipt and not a decision.
And every rig door main.js feature-detects on that path is asserted to EXIST:
failCorner, cutAway, seedClock, setFabric, plus attach's {at}. A feature-detect
is only safe if something checks the feature — `?.()` on a renamed method ran
silently for a whole sprint and looked exactly like a call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 21:30:05 +10:00
parent db1977f55d
commit 60771875d6

View File

@ -23,14 +23,24 @@ import { loadStorm, createWind } from '../weather.js';
// SPRINT16 gate 4 / SPRINT17 gate 0.1: the week-law pins at the bottom of this
// file read the ladder. week.js is contracts-only underneath, so this stays
// headless-safe.
import { NIGHTS, nightAt } from '../week.js';
import { NIGHTS, nightAt, REP } from '../week.js';
// SPRINT17 gate 3.2: the pool-yard pins at the bottom read the board's POOL
// (pure data, zero THREE) and loadSite (fetch + validateSite — the teeth; a
// site file this suite merely fetch()ed raw could rot invalid and stay green).
// loadSite pulls world.js which pulls THREE — browser-only, same as the GLB
// facts everywhere else in this repo; the node runner never imports this file.
import { POOL, exposureOf } from '../board.js';
import { POOL, exposureOf, EMERGENCIES } from '../board.js';
// SPRINT18 gate 1.3 — the knife's physics half, imported for ONE reason: to check that the doors
// main.js feature-detects on the emergency path exist. See that test.
import { SailRig } from '../sail.js';
import { loadSite } from '../world.js';
// SPRINT18 gate 1.3 — the knife. knife.js is headless-importable for the same reason ladder.js is
// (its GLB-free, and the panel guards on `typeof document`); the ledger halves come from main.js,
// which a.test and c.test already import, because scoreRun lives behind a WebGL context and the only
// way to assert its rulings is to have them extracted as pure functions.
import { createKnife, createVerbPanel, cuttable, KNIFE_TUNE } from '../knife.js';
import { answerableFor, collateralWalk, rigRecordFor, verdictFor } from '../main.js';
import { createWeek } from '../week.js';
const DT = FIXED_DT;
@ -1328,4 +1338,336 @@ export default async function run(t) {
assertEq(rustItem.cost, 45,
'the rust is the cheapest structure you can break — the stake E sized the gamble in');
});
// ================================================================ SPRINT18 gate 1.3 — THE KNIFE
//
// DESIGN.md line 165, and the signature decision under load: release, cut, or ride it out. B built
// `rig.cutAway`; these pin the VERB — where it reaches, what interrupts it, what the read promises,
// and (the half that would have shipped as a trap) what it costs on the paperwork.
//
// A note on the fixtures: they are the SHIPPED emergency's own geometry, not invented numbers.
// site_02's cowboy rig is q3/q1/tr1b/cb1, and cb1 is a carport BEAM — `work:'bracket'`, y=2.36,
// ratingHint 0.22, the corner that takes the $180 carport at t=35.9. It is the one corner the knife
// must refuse and the whole reason the verb is a whole-sail cut.
/** A rig shaped like sail.js's: corners with `broken`/`cut`, and a cutAway that behaves like B's. */
function fakeRig(corners) {
const cuts = [];
return {
rigged: true,
corners,
cuts,
cutAway(ids = null, reason = null) {
const want = ids ?? corners.map((c) => c.anchorId);
const cut = [];
for (const id of want) {
const c = corners.find((x) => x.anchorId === id);
if (!c || c.broken) continue;
c.broken = true; c.cut = true; cut.push(id);
}
if (!cut.length) return { ok: false, reason: 'the sail is already down' };
cuts.push({ corners: cut, reason });
return { ok: true, cut };
},
};
}
/** The corner block's real anchors, with the beam's mechanism as E baked it. */
const YARD = {
anchors: [
{ id: 'q1', type: 'post', work: 'cloth', pos: { x: -3.5, y: 3.95, z: -2.5 } },
{ id: 'q3', type: 'post', work: 'cloth', pos: { x: 2.0, y: 3.95, z: 4.0 } },
{ id: 'tr1b', type: 'tree', pos: { x: -6.0, y: 5.05, z: 3.0 } },
{ id: 'cb1', type: 'carport', work: 'bracket', pos: { x: -7.0, y: 2.36, z: -3.0 }, collateral: 'carport' },
],
gnome: { collateralValue: 25 },
anchor(id) { return this.anchors.find((a) => a.id === id) ?? null; },
collateralFor(key) { return key === 'carport' ? { cost: 180, label: 'the carport' } : null; },
};
const fourCorners = () => [
{ anchorId: 'q3', broken: false, hw: { name: 'shackle', cost: 15, rating: 3200 } },
{ anchorId: 'q1', broken: false, hw: { name: 'carabiner', cost: 5, rating: 1200 } },
{ anchorId: 'tr1b', broken: false, hw: { name: 'carabiner', cost: 5, rating: 1200 } },
{ anchorId: 'cb1', broken: false, hw: { name: 'shackle', cost: 15, rating: 3200 } },
];
const knifeAt = (p, rig, extra = {}) => createKnife({
player: p, getRig: () => rig, getWorld: () => YARD,
collateralAtRisk: collateralWalk, ...extra,
});
/** Stand on a cleat. The cleat is the anchor's x/z at ground level — see knife.js. */
const standAt = (p, id) => { const a = YARD.anchor(id); p.pos.x = a.pos.x; p.pos.z = a.pos.z; };
t.test('knife: it reaches a post cleat from the grass and NEVER the bracket', () => {
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
const corners = fourCorners();
const K = knifeAt(p, fakeRig(corners));
standAt(p, 'q3');
assertEq(K.target()?.anchorId, 'q3', 'standing at the post cleat, the knife is offered');
standAt(p, 'tr1b');
assertEq(K.target()?.anchorId, 'tr1b', 'and at the tree strop');
// THE ONE THAT MATTERS. cb1 is the corner about to take the $180 carport, and it is a bracket at
// 2.36 m — the same rule that costs a fascia repair two trips and a ladder. You cannot cut there,
// which is why the verb releases the WHOLE sail from a corner you can reach.
standAt(p, 'cb1');
assertEq(K.target(), null,
'the carport BEAM is bracket work — a knife cannot reach it, and having no time to fetch the '
+ 'ladder is the emergency\'s whole premise');
assert(!cuttable(corners[3], YARD.anchor('cb1')), 'cuttable() states the same rule directly');
// and the read says WHICH TOOL, rather than going silent — the greyed-prompt rule, S13
assert(/ladder/.test(K.read().whyNot ?? ''), 'the refusal names the tool it would need');
// out of range of everything: still a sail, still no cut
p.pos.x = 0; p.pos.z = 0;
assertEq(K.target(), null, 'mid-yard is not at a cleat');
// up the ladder: both hands are on the rungs
standAt(p, 'q3');
p.climbY = 2.35;
assertEq(K.target(), null, 'you cannot cut from up a ladder');
});
t.test('knife: hold X for the full 1.2 s and the WHOLE sail goes — once', () => {
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
const rig = fakeRig(fourCorners());
const K = knifeAt(p, rig);
standAt(p, 'q3');
// half a hold is no cut, and that is the deliberation the verb is made of
fixedLoop(KNIFE_TUNE.cutSecs * 0.5, DT, (dt, tt) => K.step(dt, tt, true));
assertEq(rig.cuts.length, 0, 'half a hold cuts nothing');
assert(K.cutting, 'and the radial is running');
fixedLoop(KNIFE_TUNE.cutSecs * 0.6, DT, (dt, tt) => K.step(dt, 1 + tt, true));
assertEq(rig.cuts.length, 1, 'the full hold cuts');
assertEq(rig.cuts[0].corners.length, 4,
'the WHOLE sail — DESIGN.md line 165, and B\'s cutAway() with no argument');
assertEq(rig.cuts[0].reason, 'the carport',
'the paperwork carries what it was FOR: the most expensive thing those corners were holding up');
// one press, one cut. Without the latch the held key re-arms and the ticker fills with refusals.
fixedLoop(3, DT, (dt, tt) => K.step(dt, 5 + tt, true));
assertEq(rig.cuts.length, 1, 'a held key must not try again every 1.2 s');
assertEq(K.read().state, 'none', 'and with no live corner there is nothing left to offer');
});
t.test('knife: letting go, walking off, or being put down all abort the cut', () => {
const start = () => {
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
const rig = fakeRig(fourCorners());
const K = knifeAt(p, rig);
standAt(p, 'q3');
fixedLoop(KNIFE_TUNE.cutSecs * 0.7, DT, (dt, tt) => K.step(dt, tt, true));
assert(K.progress > 0.5, 'mid-cut');
return { p, rig, K };
};
const a = start();
a.K.step(DT, 1, false); // let go
assertEq(a.K.progress, 0, 'releasing X abandons the cut');
const b = start();
b.p.pos.x += 4; // walk off the cleat
b.K.step(DT, 1, true);
assertEq(b.K.progress, 0, 'walking away abandons it too');
const c = start();
c.p.knockdown(1, 1, 0); // the storm votes
c.K.step(DT, 1, true);
assertEq(c.K.progress, 0, 'a gust that floors you mid-cut takes the cut with it');
assertEq(c.p.state, 'knocked',
'and the knife did NOT stomp the state the world just set — interact.js owns `busy`, '
+ 'nothing else writes it');
// ...nor can a cut START while the player is locked in something else
const d = start();
d.p.setState('busy', 2);
fixedLoop(2, DT, (dt, tt) => d.K.step(dt, 3 + tt, true));
assertEq(d.rig.cuts.length, 0, 'you cannot cut mid-crank or mid-poke');
});
t.test('knife: the read prints BOTH halves of the trade, and the saves side is priced', () => {
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
const rig = fakeRig(fourCorners());
const K = knifeAt(p, rig, { getGarden: () => ({ hp: 84 }), secsLeft: () => 52.4 });
standAt(p, 'q3');
const r = K.read();
assertEq(r.state, 'ready');
// $205: the carport 180 + the gnome 25 on the two-corner rule. The number the OFFER CARD printed
// as "at risk" that morning — same walk, so it cannot disagree with the invoice.
assertEq(r.savesTotal, 205, 'the saves side is the collateral the live corners are still holding');
assertEq(r.hp, 84, 'and the costs side is the bed');
assertEq(r.secsLeft, 52, 'for however long is left of the storm');
// The trade must survive on the glass, not just in the object — this panel is the thing that was
// missing for five sprints, so it gets an assert rather than a screenshot.
const panel = createVerbPanel({});
panel.update({ target: { id: 'rerig_0' }, usable: true, label: 're-rig corner', holding: false, progress: 0 }, r);
const text = panel.el.textContent;
assert(text.includes('[E] re-rig corner'), `the hold-E prompt reaches the glass: ${text}`);
assert(/CUT THE SAIL LOOSE/.test(text), 'so does the knife');
assert(text.includes('$205') && text.includes('the carport'), 'with what it saves, named and priced');
assert(/84%/.test(text) && /52s/.test(text), 'and what it costs, in the same breath');
panel.dispose();
});
// ---------------------------------------------------------------- the money (D's own filing, fixed)
t.test('knife (the money): a corner you CUT is not a corner you are answerable for', () => {
// The bug this pins: `_releaseCorner` sets `broken` for a cut exactly as for an overload, and
// every ledger surface reads `broken`. So the shipped tree billed the knife on five surfaces at
// once — the hardware, the $180 carport you cut the sail TO SAVE, the gnome's two-corner rule,
// the clean bonus it voided, and (in week.js) warranty plus standing.
const cut = fourCorners();
cut.forEach((c) => { c.broken = true; c.cut = true; });
const a = answerableFor(cut, null);
assertEq(a.lost.length, 4, 'the record still says four corners are down — the sail IS on the lawn');
assertEq(a.yours.length, 0, 'and none of them is a failure of yours');
assertEq(a.cut.length, 4, 'they are named as cut, so the paper can say who put them there');
assertEq(collateralWalk(a.yours, YARD).total, 0,
'THE HEADLINE: cutting the sail to save the carport does not then bill you for the carport');
// The vacuity guard, and it is the whole reason this test can be trusted: the SAME four corners
// broken by the storm bill everything. If both cases read $0 the walk is simply not working.
const blown = fourCorners();
blown.forEach((c) => { c.broken = true; });
const b = answerableFor(blown, null);
assertEq(b.yours.length, 4, 'an overload is still yours');
assertEq(collateralWalk(b.yours, YARD).total, 205, 'and it still costs the carport and the gnome');
// one cut, one blown: the ledger splits them by name, not by count
const mixed = fourCorners();
mixed[3].broken = true; // cb1 blew — the carport goes, honestly
mixed[0].broken = true; mixed[0].cut = true; // q3 cut afterwards
const m = answerableFor(mixed, null);
assertEq(m.yours.map((c) => c.anchorId).join(','), 'cb1', 'the one that failed is yours');
assertEq(m.cut.map((c) => c.anchorId).join(','), 'q3', 'the one you cut is not');
assertEq(collateralWalk(m.yours, YARD).total, 180,
'the carport bills (its corner really did let go) and the gnome does not — one corner, not two');
});
t.test('knife (the money): the walk that prices the panel is the walk that bills the invoice', () => {
// Not "they agree today" — they are the SAME FUNCTION, injected. This asserts the wire, because a
// second copy of the arithmetic is how the board came to advertise $57 against a $114 dispatch.
const p = new PlayerSim({ start: { x: 0, y: 0, z: 0 } });
const rig = fakeRig(fourCorners());
const K = knifeAt(p, rig);
standAt(p, 'q3');
const promised = K.read();
K.step(KNIFE_TUNE.cutSecs + DT, 0, true);
assertEq(rig.cuts.length, 1, 'cut');
const billed = collateralWalk(answerableFor(rig.corners, null).yours, YARD);
assertEq(promised.savesTotal, 205, 'the panel promised $205 off the table');
assertEq(billed.total, 0, 'and the invoice honoured it to the cent');
// PURE: asking the question must not wreck the yard. (The wreck stays at scoreRun's call site.)
let wrecks = 0;
collateralWalk(fourCorners(), { ...YARD, anchor: YARD.anchor.bind(YARD),
collateralFor: YARD.collateralFor, wreckStructure: () => { wrecks++; } });
assertEq(wrecks, 0, 'the walk never swaps a structure for its wreck — the HUD asks it every frame');
});
t.test('knife: the verdict says you cut it — and stands aside when something of yours failed too', () => {
const dmg = { hail: 30, rain: 4 };
const cut = fourCorners();
cut.forEach((c) => { c.broken = true; c.cut = true; });
const a = answerableFor(cut, null);
// The bug: with `lost` empty and hp under the line, verdictFor reached `uncovered` and told a
// player who had just cut the sail down that "the hail fell where your sail wasn't". That is the
// one sentence the verdict must never say here — it blames placement for a decision.
const lostBed = verdictFor({ hp: 31, lost: a.yours, cut: a.cut, win: false, dmg, pondPeak: 0, pondDumped: 0 });
assertEq(lostBed.mode, 'cut', `a cut night is its own mode, not '${lostBed.mode}'`);
assert(/CUT IT LOOSE/.test(lostBed.verdict), 'and it says so');
assert(!/sail wasn/.test(lostBed.verdict), 'never "the hail fell where your sail wasn\'t"');
assert(/didn't/.test(lostBed.verdict) || /their property/.test(lostBed.verdict),
'it names what the bed bought: their property');
const heldBed = verdictFor({ hp: 62, lost: a.yours, cut: a.cut, win: true, dmg, pondPeak: 0, pondDumped: 0 });
assertEq(heldBed.mode, 'cut-held', 'cut early enough and the bed can still make it');
// And the composition rule: a corner of YOURS on the ground is the louder fact, so the failure
// modes keep speaking and the knife stays a line on the ledger.
const mixed = fourCorners();
mixed[3].broken = true;
mixed[0].broken = true; mixed[0].cut = true;
const m = answerableFor(mixed, null);
const both = verdictFor({ hp: 20, lost: m.yours, cut: m.cut, win: false, dmg, pondPeak: 0, pondDumped: 0 });
assertEq(both.mode, 'corner', 'one corner of yours went: that verdict is true and it wins');
assert(/CB1/.test(both.verdict), 'and it names the corner that actually let go');
// no knife, no change: every pre-existing mode is untouched by the new argument
const plain = verdictFor({ hp: 31, lost: [], cut: [], win: false, dmg, pondPeak: 0, pondDumped: 0 });
assertEq(plain.mode, 'uncovered', 'a night with no cut still reads exactly as it did');
});
t.test('knife: the shipped emergency has something to CUT FOR — B\'s authored rig, not the placeholder', () => {
// The night's whole premise, as DATA, because that is where it was wrong. On the merged tree the
// rig was still A's flagged first cut with `cb1` (the carport beam) ARRIVING ALREADY DOWN — which
// makes the $180 carport `theirs`, unbillable and unwreckable, and leaves DESIGN.md line 165's
// signature decision with no object. Found by playing the night and reading the dispatch card.
const em = EMERGENCIES.find((e) => e.id === 'corner_block_emergency');
assert(em, 'the shipped emergency is still on the board');
const by = (id) => em.emergency.rig.find((c) => c.anchorId === id);
assert(by('cb1'), 'the carport beam is in the cowboy\'s rig — it is the bait the client has been '
+ 'selling since night 3');
assertEq(by('cb1').hw, 'shackle',
'a SHACKLE on the 0.22 beam: 704 N holding their roof, over its ceiling t=35..42 (B, measured). '
+ 'A carabiner there fails in the first second and there is nothing left to decide');
assert(!by('cb1').broken,
'AND IT IS FLYING WHEN YOU ARRIVE. A carport corner already on the ground is not a knife '
+ 'decision, it is a receipt — this is the assert that stops the placeholder coming back');
const down = em.emergency.rig.filter((c) => c.broken).map((c) => c.anchorId);
assertEq(down.join(','), 'tr1b',
'exactly one corner arrives gone, and it is the 0.88 branch on the cheapest steel');
// B's ruling, which is also data in A's file and also had not landed.
assertEq(em.pay?.garden, 0,
'the garden BONUS is off on a callout (B): you cannot be paid per-HP for a bed you had no prep '
+ 'phase to protect. The garden WIN stays, which is what makes the knife cost something');
});
t.test('knife: every rig door the emergency path feature-detects actually EXISTS', () => {
// ⚠️ This is the pin for the bug that cost this sprint its arrived-broken corner: main.js called
// `rig.breakCorner?.(id)` and B had named the door `failCorner`, so an optional call to a
// nonexistent method ran silently, forever, looking exactly like a call. The repo has a name for
// it — D's own `rigging.setWorld` lesson, quoted in main.js — and it still happened, because
// `?.()` makes a rename invisible. A feature-detect is only safe if something checks the feature.
for (const door of ['failCorner', 'cutAway', 'seedClock', 'setFabric']) {
assertEq(typeof SailRig.prototype[door], 'function',
`SailRig has no ${door}() — main.js feature-detects this door on the emergency path, so a `
+ 'rename here is a silent no-op, not an error');
}
// and the pre-fly argument, which is how the cloth gets into the storm it is standing in
assert(/\{\s*at\s*=\s*0/.test(SailRig.prototype.attach.toString()),
'attach() no longer takes {at} — the emergency\'s sail would fly the first second of a storm '
+ 'it arrived 30s into (measured: clockSkew 30.02)');
});
t.test('knife: warranty and standing do not chase a corner you chose to let go', () => {
const def = { baseCurve: [[0, 10]], gusts: { powBase: 5, powRamp: 5 } };
const cutRec = () => {
const cs = fourCorners();
cs.forEach((c) => { c.broken = true; c.cut = true; });
return rigRecordFor(cs);
};
assert(cutRec().every((c) => c.cut && c.brokenAtDawn),
'the record is honest: cut AND down at dawn, both true, neither hidden');
// night 1: cut the lot. night 2: the sheet must book NO warranty off it.
const w = createWeek();
const s1 = w.settle({ hp: 40, win: false, collateral: [], intactHardwareValue: 0,
rigRecord: cutRec(), cutAways: [{ t: 33, corners: ['q3', 'q1', 'tr1b', 'cb1'], reason: 'the carport' }] }, def, 0);
assertEq(s1.warranty.length, 0, 'nothing to warranty on the night you cut');
assert(!s1.repReasons.some((r) => /down at dawn/.test(r)),
`standing is not docked for the cut: ${s1.repReasons.join(' · ')}`);
assertEq(s1.repDelta, REP.CLEAN,
'a night where nothing FAILED is a clean night — the knife\'s price is the garden, not the star');
w.advance();
const s2 = w.settle({ hp: 100, win: true, collateral: [], intactHardwareValue: 0, rigRecord: [] }, def, 0);
assertEq(s2.warranty.length, 0,
'and no free callout tomorrow to go and re-hang a sail you deliberately spent');
// the vacuity guard: the same four corners BLOWN book four warranty lines and cost the standing
const w2 = createWeek();
const blownRec = () => { const cs = fourCorners(); cs.forEach((c) => { c.broken = true; }); return rigRecordFor(cs); };
const b1 = w2.settle({ hp: 40, win: false, collateral: [], intactHardwareValue: 0, rigRecord: blownRec() }, def, 0);
assert(b1.repDelta < 0, 'four blown corners DO cost standing');
w2.advance();
const b2 = w2.settle({ hp: 100, win: true, collateral: [], intactHardwareValue: 0, rigRecord: [] }, def, 0);
assertEq(b2.warranty.length, 4, 'and they DO book four warranty lines — the pin is not vacuous');
});
}