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:
parent
75d5f9a4bf
commit
aa0b36a54c
@ -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 (Inf−Inf 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user