Gate 0' guard: redesign it to measure SHAPE, and prove it fires
The integrator demoted my settled-at-entry guard to a warning at the Sprint-8
merge and handed B+D the redesign. Investigating it refuted the stated cause and
found a better observable.
The ponding diagnosis does not hold: every storm's rain curve starts at [0,0], so
storm_02.rainMmPerHour(0) is 0.0 mm/h and a held clock rains nothing. The real
confound was that the guard measured in the STORM's wind while the rig settled in
CALM — and that calm->storm step is one the REAL GAME ALSO HAS (wind.use() swaps
on the phase change), so it was never a harness artifact. Their 2 s windows were
also shorter than the cloth's breath: 2 s reads a 32% trend on a settled rig
where 3 s reads 6%.
But fixing the clock isn't enough, because LOAD cannot answer this question at
all. In calm there is no transient to see — the unsettled rig is LIGHTER (0.30 kN
vs 0.47 settled) and the trend test passes at every settle length from 0 to 20 s.
A guard that cannot fail is decoration, which is precisely what StumbleBack and
the fake skips were.
So ask the physical question: has the cloth stopped MOVING. Mean node drift
separates ~6x either side, and cannot be confused by wind or water:
unsettled (0 s) -> 212 mm/s (cover quad) · 208 (miss quad)
settled (8-20 s) -> 35 mm/s worst breath, typically 4-6
Limit 100 mm/s. Proved it FAILS before trusting it to pass: sabotaged the settle
to 0 s and it fired — "cloth still drifting 212 mm/s (limit 100)".
The integrator's 600 N floor was a sound amendment to the LOAD design; it isn't
carried over because drift has no units of force and 212-vs-35 is not a judgement
call. 274/0/0, all five balance asserts green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
62005c8fd1
commit
601adedf45
@ -187,52 +187,48 @@ async function fly(yard, session, stormName, { repair = false, broom = false } =
|
||||
rig.step(FIXED_DT, settleWind, (i * FIXED_DT) % period);
|
||||
}
|
||||
}
|
||||
/** Peak corner load the instant the storm starts — the settled-at-entry guard reads this. */
|
||||
const entryPeak = Math.max(...rig.corners.map((c) => c.load || 0));
|
||||
|
||||
// D's settled-at-entry guard (SPRINT8 gate 0').
|
||||
//
|
||||
// The settle above is only load-bearing if it actually settled — if a future
|
||||
// change makes the cloth ring longer than 12 s, every number below silently
|
||||
// becomes an attach-transient measurement again, which is the bug that cost
|
||||
// two sprints. So prove it.
|
||||
//
|
||||
// It has to be a TREND test, not an instant one. Measured: with the wind held
|
||||
// constant the worst corner still oscillates 0.9 -> 1.9 -> 1.3 -> 1.7 kN,
|
||||
// because damping is deliberately light (VEL_DAMP 0.995 — the relative-wind
|
||||
// drag is meant to do the damping). There is no single settled value; the
|
||||
// cloth breathes. An instantaneous "has it stopped moving" check can never
|
||||
// pass, and would just be a flaky assert that gets deleted. Same lesson as the
|
||||
// statics assert in sail.selftest: compare time-AVERAGED windows.
|
||||
const meanLoad = (secs) => {
|
||||
let sum = 0, n = Math.round(secs / FIXED_DT);
|
||||
for (let i = 0; i < n; i++) { rig.step(FIXED_DT, wind, 0); sum += rig.maxLoad(); }
|
||||
return sum / n;
|
||||
};
|
||||
const w1 = meanLoad(2);
|
||||
const w2 = meanLoad(2);
|
||||
const trend = Math.abs(w2 - w1) / Math.max(1, w1);
|
||||
// Integrator amendment (Sprint-8 merge, D to bless): the trend only matters
|
||||
// at a scale that can move a verdict. The transient that cost two sprints was
|
||||
// kN-scale; a dry settled rig still shows a decaying ~0.2 kN tail that reads
|
||||
// as 40%+ RELATIVE while being noise against a 1.2 kN carabiner rating.
|
||||
// ⚠️ INTEGRATOR DEMOTION (Sprint-8 merge) — B+D, this guard needs a redesign,
|
||||
// not a threshold. It was authored before ponding merged, and at a held clock
|
||||
// storm_02's compressed rain (~35 kg/s on a 40 m² sail) ponds the cloth DURING
|
||||
// the guard's own windows — the "trend" it reads is water arriving, which no
|
||||
// settle length fixes (measured tonight: 105% with full-curve settle, 46%
|
||||
// dried, 73% dried-every-step; entryPeak also read a 3.04 kN water belly).
|
||||
// The guard's idea is right; its clock is wrong. Redesign: measure the trend
|
||||
// over the storm's own advancing first seconds (rain then follows the real
|
||||
// curve, mild at t=0), or dry-and-hold in a rainless probe. Until then it
|
||||
// WARNS instead of failing so the merged suite reports the balance truthfully.
|
||||
if (trend > 0.35 && Math.max(w1, w2) > 600) {
|
||||
console.warn(`yard is NOT settled at storm entry: worst-corner mean is still trending ` +
|
||||
`${(trend * 100).toFixed(0)}% between consecutive 2 s windows ` +
|
||||
`(${(w1 / 1000).toFixed(2)} -> ${(w2 / 1000).toFixed(2)} kN) after a ${12} s settle. ` +
|
||||
`Every balance number below is measuring the attach transient — lengthen the settle ` +
|
||||
`before trusting them. (Oscillation is expected and fine; a TREND is not.)`);
|
||||
}
|
||||
/**
|
||||
* D's settled-at-entry guard, REDESIGNED (SPRINT9, B+D). It now measures SHAPE, not load.
|
||||
*
|
||||
* The guard exists for one reason: a harness that rigs and storms in the same tick measures a
|
||||
* cloth still falling out of its flat _build() shape, and pretends that is a rig. That bug cost
|
||||
* two sprints, so the settle above has to prove it worked.
|
||||
*
|
||||
* The Sprint-8 version asked the question with LOAD and could not answer it. Measured tonight,
|
||||
* why every load-based form fails:
|
||||
* · in CALM (what the rig settles in) there is no transient to see at all — worst-corner mean
|
||||
* reads 0.30 kN unsettled vs 0.47 kN settled, i.e. the unsettled rig is LIGHTER, and the
|
||||
* trend test passes at every settle length from 0 s to 20 s. A guard that cannot fail is
|
||||
* decoration.
|
||||
* · in the STORM at a held t=0 it read a 105% "trend" — but that is the calm→storm step change,
|
||||
* which the REAL GAME ALSO HAS (`wind.use(to === 'storm' ? winds[stormKey] : calmWind)` on the
|
||||
* phase change). It was never a harness artifact; the suite was being faithful and the guard
|
||||
* was calling it a bug.
|
||||
* · the integrator's ponding diagnosis at the Sprint-8 merge does not hold either: every storm's
|
||||
* rain curve starts at [0, 0], so `storm_02.rainMmPerHour(0)` is 0.0 mm/h and a held clock
|
||||
* rains nothing. (Their 2 s windows were also shorter than the cloth's breath — 2 s reads a
|
||||
* 32% trend on a settled rig where 3 s reads 6%.)
|
||||
*
|
||||
* So ask the physical question instead: HAS THE CLOTH STOPPED MOVING. Mean node drift separates
|
||||
* cleanly, ~6x either side of the line, and unlike load it cannot be confused by wind or water:
|
||||
* unsettled (0 s) → 212 mm/s (cover quad) · 208 mm/s (miss quad)
|
||||
* settled (8-20 s) → 35 mm/s worst case, typically 4-6
|
||||
* The integrator's 600 N floor was a sound amendment to the LOAD design and is not carried over —
|
||||
* drift has no units of force to need a floor, and it needs no threshold on "does it matter"
|
||||
* because 212-vs-35 is not a judgement call.
|
||||
*/
|
||||
const settleDriftMmPerSec = (() => {
|
||||
const before = Float64Array.from(rig.pos);
|
||||
const secs = 2;
|
||||
for (let i = 0, n = Math.round(secs / FIXED_DT); i < n; i++) {
|
||||
rig.step(FIXED_DT, calmWind, ((12 + i * FIXED_DT) % Math.max(1, calmDef.duration)));
|
||||
}
|
||||
let sum = 0;
|
||||
for (let i = 0; i < before.length; i += 3) {
|
||||
sum += Math.hypot(rig.pos[i] - before[i], rig.pos[i + 1] - before[i + 1], rig.pos[i + 2] - before[i + 2]);
|
||||
}
|
||||
return (sum / (before.length / 3)) * 1000 / secs;
|
||||
})();
|
||||
|
||||
let hp = 100, pond = 0, used = 0;
|
||||
const steps = Math.round(def.duration / FIXED_DT);
|
||||
@ -264,8 +260,8 @@ async function fly(yard, session, stormName, { repair = false, broom = false } =
|
||||
lost: rig.corners.filter((c) => c.broken).length,
|
||||
spent: START_BUDGET - session.budget,
|
||||
pond: Math.round(pond),
|
||||
/** kN on the worst corner at storm entry — the settled-at-entry guard's number. */
|
||||
entryPeak: entryPeak / 1000,
|
||||
/** mm/s the cloth was still drifting when the storm started — the settled-at-entry guard. */
|
||||
settleDrift: settleDriftMmPerSec,
|
||||
};
|
||||
}
|
||||
|
||||
@ -347,22 +343,26 @@ export default async function run(t) {
|
||||
});
|
||||
|
||||
/**
|
||||
* D's settled-at-entry guard (SPRINT8 §Lane D). The suite must enter the storm with the yard in
|
||||
* the state a player hands it: rigged, breathing on the calm day, corners loaded by nothing worse
|
||||
* than their own tension. It does not decide storm_02's verdict — measured, it doesn't — but a
|
||||
* harness silently measuring the attach transient is how three of them disagreed for two sprints,
|
||||
* and the transient IS worth 2.7× on tree corners in the moment it exists.
|
||||
* D's settled-at-entry guard, redesigned to measure SHAPE (SPRINT9, B+D). Full reasoning at the
|
||||
* measurement site in fly(); the short version is that load could not answer this question and
|
||||
* node drift can, ~6x clear either side of the line.
|
||||
*
|
||||
* It does NOT decide storm_02's verdict — measured twice now, the settle never moves the corner
|
||||
* count. It exists because a harness silently flying a cloth that is still falling is how three
|
||||
* of them disagreed for two sprints, and the next one should trip a wire instead of an argument.
|
||||
*/
|
||||
t.test('harness: the yard is SETTLED when the storm starts', () => {
|
||||
t.test('harness: the cloth has STOPPED MOVING when the storm starts', () => {
|
||||
const LIMIT = 100; // mm/s. Measured: 212 unsettled, 35 worst settled breath.
|
||||
const runs = [['line', line], ['cheap', cheap], ['gentle', gentle]].filter(([, r]) => r);
|
||||
for (const [name, r] of runs) {
|
||||
if (!(r.entryPeak < 1.0)) {
|
||||
if (!(r.settleDrift < LIMIT)) {
|
||||
throw new Error(
|
||||
`${name} entered the storm carrying ${r.entryPeak.toFixed(2)} kN — that is an attach ` +
|
||||
'transient, not a rig. Settle on the calm day with a running clock, as main.js does.');
|
||||
`${name} entered the storm with the cloth still drifting ${r.settleDrift.toFixed(0)} mm/s ` +
|
||||
`(limit ${LIMIT}). That is a sail falling into shape, not a rig — every number below is ` +
|
||||
'measuring the attach transient. Lengthen the settle in fly().');
|
||||
}
|
||||
}
|
||||
return `entry loads: ${runs.map(([n, r]) => `${n} ${r.entryPeak.toFixed(2)} kN`).join(' · ')}`;
|
||||
return `settle drift: ${runs.map(([n, r]) => `${n} ${r.settleDrift.toFixed(1)} mm/s`).join(' · ')}`;
|
||||
});
|
||||
|
||||
t.test('balance: storm_02 punishes a cheap rig on the same quad', () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user