Lane B S17 gate 0.2: THE SECOND WIRE — a fully-lost sail re-arms the heal

D's S16 poison poke, filed for this lane: _checkFailure's corner loop is the
heal's only trigger and it skips broken corners, so once all four were
divergence-broken a SECOND poison had no unbroken corner left to read NaN
through — the corpse stayed NaN forever and rendered as nothing. A lost sail
is still cloth in the yard; it must LOOK lost, not vanish.

The sentinel runs ONLY on a fully-broken rig (the corner wire covers every
other case): sum pos+prev — any NaN/±Inf makes the sum non-finite (Inf−Inf is
NaN), finite cloth cannot overflow it. Branch-free O(n) adds, no mutation, so
a finite lost sail is untouched byte-for-byte.

Test replays D's gait: cascade 4/4 → 5 s finite with ZERO heals (the negative
control — the sentinel must never mistake 'lost' for 'sick') → second poison
heals in one substep, finite a full second on. Node suite 47/47.
This commit is contained in:
type-two 2026-07-21 02:45:28 +10:00
parent 75d5f9a4bf
commit aa0b36a54c
2 changed files with 65 additions and 0 deletions

View File

@ -970,6 +970,26 @@ export class SailRig {
// the spring network, and the cloth a player sees after the bang is a // the spring network, and the cloth a player sees after the bang is a
// shredded z-fighting mess, not a cloth. Heal ONLY on a divergence break — // shredded z-fighting mess, not a cloth. Heal ONLY on a divergence break —
// an honest overload break has finite state and must not be touched. // an honest overload break has finite state and must not be touched.
//
// SPRINT17 gate 0.2 — THE SECOND WIRE (D's poison poke, S16 filing). The
// loop above is the heal's only trigger and it SKIPS broken corners, so
// once all four are gone there is no corner load left to read NaN through:
// a fully-lost sail that diverged again stayed NaN forever and rendered as
// nothing. A lost sail is still cloth in the yard — it must LOOK lost, not
// vanish. The sentinel runs ONLY on a fully-broken rig (the corner wire
// covers every other case): sum the state — any NaN/±Inf makes the sum
// non-finite (InfInf is NaN), and finite cloth cannot overflow it
// (positions are metres, prev within a step of pos). Branch-free O(n)
// adds, no mutation, so a finite lost sail is untouched byte-for-byte —
// the negative control in sail.selftest.js pins that.
if (!diverged
&& this.corners[0].broken && this.corners[1].broken
&& this.corners[2].broken && this.corners[3].broken) {
const pos = this.pos, prev = this.prev;
let s = 0;
for (let i = 0; i < pos.length; i++) s += pos[i] + prev[i];
if (!Number.isFinite(s)) diverged = true;
}
if (diverged) this._healNonFinite(); if (diverged) this._healNonFinite();
if (this._dirtyRest) { this._applyRestLengths(); this._dirtyRest = false; } if (this._dirtyRest) { this._applyRestLengths(); this._dirtyRest = false; }
} }

View File

@ -327,6 +327,51 @@ test('divergence heal: negative control — honest overload breaks never trigger
return `${broke.length} honest break(s), zero heals — the heal only answers divergence`; return `${broke.length} honest break(s), zero heals — the heal only answers divergence`;
}); });
test('divergence heal: re-arms on a fully-lost sail (D\'s second poison) — and stays silent on a finite one', () => {
// SPRINT17 gate 0.2, D's S16 filing verbatim: "with all four corners already
// divergence-broken, a SECOND poison has no unbroken corner left to arm the
// heal (_checkFailure skips broken corners), so a fully-lost sail that
// diverges again stays NaN and renders as nothing." The fix is the sentinel
// in _checkFailure — it watches ONLY when every corner is broken. This test
// is D's gait replayed: cascade, then poke the corpse.
const r = new SailRig({ anchors: makeAnchors(HEIGHTS_FLAT), gridN: 10 });
r.attach(ALL_IDS, Array(4).fill(HARDWARE[2]), 1.0);
const heals = [];
r.events.on('heal', (e) => heals.push(e));
const w = makeStubWind({ stormLen: 90 });
runStorm(r, w, 2);
// First poison: the cascade D observed — all four corners let go by
// divergence inside the run, the S16 heal answers it.
r.pos[(r.N + 1) * 3 + 1] = NaN;
runStorm(r, w, 2);
assert(r.corners.every((c) => c.broken),
`first poison broke only ${r.corners.filter((c) => c.broken).length}/4 corners — D's cascade did not happen and this test is not testing the edge`);
assert(heals.length > 0, 'the first divergence never healed — S16 gate 2.1 regressed, this test cannot reach the edge');
const healsAfterCascade = heals.length;
// NEGATIVE CONTROL first: a fully-broken sail with FINITE state rides on
// with zero heals — the sentinel must never mistake "lost" for "sick".
runStorm(r, w, 5);
assert(heals.length === healsAfterCascade,
`${heals.length - healsAfterCascade} heal event(s) on a finite lost sail — the sentinel is touching healthy state`);
// The second poison: before the sentinel, this NaN had no wire to trip —
// no unbroken corner reads a load — and the corpse stayed poisoned forever.
r.pos[(r.N + 1) * 3 + 1] = NaN;
let steps = 0;
const cap = Math.round(1 / SIM_DT);
while (heals.length === healsAfterCascade && steps < cap) {
r.step(SIM_DT, w, 9 + steps * SIM_DT); steps++;
}
assert(heals.length > healsAfterCascade,
'second poison on a fully-broken sail never healed — the re-arm edge is back (_checkFailure has no wire when all four corners are broken)');
for (const v of r.pos) assert(Number.isFinite(v), 'node position still non-finite after the re-armed heal');
for (const v of r.prev) assert(Number.isFinite(v), 'node prev still non-finite after the re-armed heal');
// ...and it HOLDS: a full second of sim later the corpse is still cloth.
runStorm(r, w, 1);
for (const v of r.pos) assert(Number.isFinite(v), 'lost sail went non-finite again inside 1 s — the re-armed heal did not hold');
for (const v of r.water) assert(Number.isFinite(v), 'water non-finite after the re-armed heal');
return `cascade 4/4 → 5 s finite, no heal (control) → second poison healed in ${steps} substep(s), finite 1 s on`;
});
test('ponding: consolidation cannot stack one node past the cap (post-flow clamp)', () => { test('ponding: consolidation cannot stack one node past the cap (post-flow clamp)', () => {
// The rain-add clamp only ran WHILE raining; once rain stopped, downhill // The rain-add clamp only ran WHILE raining; once rain stopped, downhill
// flow could pile a basin node arbitrarily high and nothing trimmed it. // flow could pile a basin node arbitrarily high and nothing trimmed it.