Lane B S18 gate 1: commit(rig, {at}) carries the mid-storm clock, so seeding it cannot be forgotten

A's emergency flow is inherit -> commit -> put the dead corner down -> roll, and
it goes through session.commit(rig) by design (the public-moves discipline).
That door could not seed the sail's clock, so getting it right needed a SECOND
call after commit — a step a caller can forget, on the one night where
forgetting it understates every load ~4x and reads as "triage is easy" rather
than as a bug. opts forward straight to attach; omitted, the ordinary night is
byte-for-byte unchanged (clock at 0), pinned both ways.

Selftest 535/0/0 (522 baseline + 13).
This commit is contained in:
type-two 2026-07-25 20:03:13 +10:00
parent ae2f560ff2
commit afef9abe0b
2 changed files with 32 additions and 3 deletions

View File

@ -541,8 +541,18 @@ export class RiggingSession {
this.picks = ring.map((a) => byId.get(a.id));
}
/** Hand the finished rig to the sim. Mirrors contracts.js sailRig.attach(). */
commit(rig) {
/**
* Hand the finished rig to the sim. Mirrors contracts.js sailRig.attach().
*
* `opts` is forwarded to `attach` untouched, which exists for exactly one
* reason (SPRINT18, Lane C's finding): an emergency callout's storm is already
* 30 seconds old, and `attach({at, wind})` is what stops the cloth flying the
* wrong sixty seconds of it. Without this the only way to seed the clock would
* be a SECOND call after commit a step a caller can forget, on the one night
* where forgetting it silently understates every load by 4x. The session's
* public-moves discipline and a correct clock should not be a choice.
*/
commit(rig, opts = undefined) {
if (!this.canStart) throw new Error(`sail needs ${MAX_CORNERS} corners, have ${this.picks.length}`);
// gate 2, the promise the sprint doc names ("enforces the budget cap at
// commit"): by now the refusals above have made these states unreachable
@ -570,7 +580,7 @@ export class RiggingSession {
// shade cloth + C's 0.40 downdraft drop p1 to 1.08 kN, under a $5
// carabiner's rating, which is the $10 that closes the $90 -> $80 gap.)
if (rig.setFabric) rig.setFabric(this.fabric);
return rig.attach(this.picks.map((p) => p.anchorId), this.picks.map((p) => p.hw), this.tension);
return rig.attach(this.picks.map((p) => p.anchorId), this.picks.map((p) => p.hw), this.tension, opts);
}
/**

View File

@ -605,6 +605,25 @@ test('gate 1: ⚠️ the refund cannot pay you for a stranger\'s steel — the k
return `inherited steel refunds $0; the $${RATED.cost} the player bought refunds, and only that`;
});
test('gate 1: commit carries the mid-storm clock through to the sail', () => {
// The emergency flow is inherit -> commit -> put the dead corner down -> roll.
// If commit cannot pass `at`, seeding the clock becomes a separate call on the
// one night where forgetting it understates every load ~4x (C's finding).
const s = session();
assert(s.inherit(INHERITED).ok, 'inherit refused');
const rig = s.commit(new SailRig({ anchors: ANCHORS, gridN: 10 }), { at: 30 });
assert(rig.t === 30,
`commit({at:30}) left the cloth at t=${rig.t}. It is now flying the storm's FIRST sixty seconds ` +
'under a sky flying its last sixty, and nothing about that is visible from outside');
assert(rig.clockSkew === 0, `a freshly seeded commit already reads ${rig.clockSkew}s of skew`);
// and the ordinary night is untouched — no opts, clock at 0
const plain = session();
for (const id of ['h1', 'h3', 'p1', 'p2']) plain.rig(id);
assert(plain.commit(new SailRig({ anchors: ANCHORS, gridN: 10 })).t === 0,
'an ordinary commit no longer starts its storm at 0');
return 'commit(rig, {at:30}) seeds the cloth to 30s; a plain commit still starts at 0';
});
test('gate 1: an inherited rig still obeys the client, and refuses a rig it cannot build', () => {
const s = session();
s.setConstraints([HOUSE_BAN]);