Gate 0: name the harness dispute's cause; make skips real (testkit)
THE CAUSE, and it is not a winner: balance.test.js built skyfx with no camera, and skyfx.step() opens `if (!camera) return;` — so the hail shadow grid was never rebuilt across all 5400 calls and every loadout was scored as though it had no sail. hp 36 IS the bare-bed number. Measured with the camera as the only variable: hailShadowOver(bed) peaks 0.000 vs 1.000, hp 36 vs 69. Neither harness was lying; one was flying a yard with no cloth in it. Camera + wind shelters added — the suite must drive what the game drives. MY ERROR, named: the hp-58 win was measured at tension 1.0 and I never declared it. At the shop's default 0.9 my own end-to-end also loses 2 corners. STILL OPEN: this suite says 2 corners lost, my end-to-end says 1 at tension 1.0. Ruled out by measurement, each worth <=0.02 kN: shelters, frozen vs live tree sway, the scripted repair. Next suspects named in the skip: session.commit() vs rigSail(), and main.js passing debris as rig.step's 4th arg (debris ADDS load, so it should break MORE here, not less — that inversion is the thread). And the find that explains how this survived a merge: Suite.test() ignored its return value, so `return "SKIPPED — ..."` was recorded as a PASS. The integrator's storm_02 skip was a fake pass; so was mine for storm_01; `skip: 0` in every report we ever printed was the tell and nobody read it, me included — I misread one of those fakes as my win reproducing. test() now honours the string, and rejects async fns outright (they hand back a Promise that cannot throw synchronously, passing forever while proving nothing). That guard immediately caught its own author: BOTH my wind-router tripwire tests were async and have asserted nothing for two sprints — including the sprint where I reported the tripwire "caught its first real omission". It had, in a hand-run probe, never in the suite. Awaits hoisted into run(); they asserted for the first time today, and pass. Selftest 261 pass / 0 fail / 2 honest skips. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
aef9958337
commit
aac1030131
@ -60,10 +60,42 @@ export class Suite {
|
||||
this.results = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* A test passes by returning, fails by throwing, and SKIPS by returning a
|
||||
* string that starts with 'SKIPPED'.
|
||||
*
|
||||
* That last clause is not sugar — it is a repair. This method used to ignore
|
||||
* its return value entirely, so a test that did
|
||||
* if (!ok) return `SKIPPED — harness dispute: ...`;
|
||||
* was recorded as a PASS. Two suites had already been written that way (the
|
||||
* integrator's storm_02 skip at the Sprint-6 merge, and a storm_01 one), which
|
||||
* means main has been reporting green on asserts that were neither passing nor
|
||||
* skipping — they returned a message and the suite called it a win. `skip: 0`
|
||||
* in every report we ever printed was the tell, and nobody read it, including
|
||||
* me: I misread one of those fake passes as my own win reproducing.
|
||||
*
|
||||
* It's the same disease as the wind router swallowing rainMmPerHour — a
|
||||
* mechanism that looks wired and silently isn't — and it lived in the harness
|
||||
* every lane trusts. A skip must be visible or it is a lie with good manners.
|
||||
*/
|
||||
test(label, fn) {
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
fn();
|
||||
const out = fn();
|
||||
if (typeof out === 'string' && out.startsWith('SKIPPED')) {
|
||||
this.results.push({
|
||||
lane: this.lane, label, status: 'skip',
|
||||
err: out.replace(/^SKIPPED\s*[—-]?\s*/, ''), ms: performance.now() - t0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (out && typeof out.then === 'function') {
|
||||
// An async fn hands back a Promise that cannot throw synchronously, so
|
||||
// this would pass forever while proving nothing. Lane B called this out
|
||||
// in balance.test.js's header; make it impossible rather than a warning.
|
||||
throw new Error(`test "${label}" is async — Suite.test() cannot await it. ` +
|
||||
`Do the awaiting in run() and assert synchronously.`);
|
||||
}
|
||||
this.results.push({ lane: this.lane, label, status: 'pass', ms: performance.now() - t0 });
|
||||
} catch (err) {
|
||||
this.results.push({
|
||||
|
||||
@ -52,8 +52,15 @@ export default async function run(t) {
|
||||
});
|
||||
|
||||
// --- the wind router -----------------------------------------------------
|
||||
// Loaded HERE, not inside t.test(). These two were written `async` and
|
||||
// Suite.test() cannot await — so they were recorded as passes while asserting
|
||||
// nothing at all, for two sprints, including the sprint where I claimed the
|
||||
// tripwire had "caught its first real omission". It had: in a hand-run probe,
|
||||
// not in the suite. runAll DOES await run(), so awaiting belongs out here.
|
||||
const realWild = createWind(await loadStorm('storm_02_wildnight'));
|
||||
const realGentle = createWind(await loadStorm('storm_01_gentle'));
|
||||
|
||||
t.test('wind router forwards EVERYTHING the real wind exposes', async () => {
|
||||
t.test('wind router forwards EVERYTHING the real wind exposes', () => {
|
||||
// This exists because the omission it catches has already shipped once.
|
||||
//
|
||||
// Lane C added rainMmPerHour/rainDepthMm for ponding; main.js's router — a
|
||||
@ -68,7 +75,7 @@ export default async function run(t) {
|
||||
// garden all over again, which is the exact thing this sprint exists to fix.
|
||||
// Diffing the two objects means the next omission is a red test that names
|
||||
// the missing member, rather than a system that quietly isn't plugged in.
|
||||
const real = createWind(await loadStorm('storm_02_wildnight'));
|
||||
const real = realWild;
|
||||
const router = createWindRouter([real]);
|
||||
|
||||
const missing = Object.keys(real).filter((k) => !(k in router));
|
||||
@ -81,11 +88,11 @@ export default async function run(t) {
|
||||
assert(wrongType.length === 0, `router has ${wrongType.join(', ')} but not as callable(s)`);
|
||||
});
|
||||
|
||||
t.test('wind router delegates live — use() re-points every consumer at once', async () => {
|
||||
t.test('wind router delegates live — use() re-points every consumer at once', () => {
|
||||
// The router's whole reason to exist: consumers bind once at construction,
|
||||
// so a storm swap has to be a re-point rather than a re-wire.
|
||||
const gentle = createWind(await loadStorm('storm_01_gentle'));
|
||||
const wild = createWind(await loadStorm('storm_02_wildnight'));
|
||||
const gentle = realGentle;
|
||||
const wild = realWild;
|
||||
const router = createWindRouter([gentle, wild]);
|
||||
const at = new THREE.Vector3(0, 0, 0);
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
* which is exactly what this file is for.
|
||||
*/
|
||||
|
||||
import * as THREE from '../../vendor/three.module.js';
|
||||
import { RiggingSession } from '../rigging.js';
|
||||
import { SailRig } from '../sail.js';
|
||||
import { createSkyFx } from '../skyfx.js';
|
||||
@ -105,7 +106,35 @@ async function fly(yard, session, stormName, { repair = false, broom = false } =
|
||||
const def = await loadStorm(stormName);
|
||||
const wind = createWind(def);
|
||||
const rig = session.commit(new SailRig({ anchors: yard.anchors, gridN: 10 }));
|
||||
const sky = createSkyFx({ wind, night: true });
|
||||
|
||||
// The camera is NOT decoration here, and leaving it out is what caused the
|
||||
// SPRINT6 harness dispute (gate 0, cause found by Lane A 2026-07-18).
|
||||
//
|
||||
// skyfx.step() opens with `if (!camera) return;` — reasonable on its face,
|
||||
// since it drives rain, the cloud dome and audio, all of which need a camera.
|
||||
// But the hail/rain SHADOW GRIDS are rebuilt inside that same step(). With no
|
||||
// camera, step() returns on all 5400 calls, the grids are never populated, and
|
||||
// gardenHailExposure() reports full exposure no matter what the sail is doing.
|
||||
//
|
||||
// Measured, same rig, same storm, camera the only variable:
|
||||
// no camera → hailShadowOver(bed) peaks at 0.000, hp 36
|
||||
// camera → hailShadowOver(bed) peaks at 1.000, hp 69
|
||||
// hp 36 is the BARE-BED number. This suite was flying every loadout as though
|
||||
// the sail did not exist, and reporting the wild night unwinnable on that.
|
||||
//
|
||||
// A headless caller silently getting zero shadow is a trap with Lane A's name
|
||||
// on it (same shape as the wind router swallowing rainMmPerHour). The real fix
|
||||
// is Lane C moving the guard below the shadow rebuild — raised in THREADS. This
|
||||
// camera is correct regardless: the suite should drive what the game drives.
|
||||
const camera = new THREE.PerspectiveCamera(60, 1.6, 0.1, 400);
|
||||
camera.position.set(0, 2, 8);
|
||||
const sky = createSkyFx({ wind, night: true, camera });
|
||||
|
||||
// main.js calls this at boot; this suite never did, and the omission is the
|
||||
// same species as the camera above — the suite must drive what the game
|
||||
// drives. The trees shade the yard from wind and COVER_QUAD hangs off t2/t2b,
|
||||
// anchors sitting inside those shadows.
|
||||
wind.setSheltersFromTrees(yard.anchors.filter((a) => a.type === 'tree'));
|
||||
|
||||
let hp = 100, pond = 0, used = 0;
|
||||
const steps = Math.round(def.duration / FIXED_DT);
|
||||
@ -173,19 +202,39 @@ export default async function run(t) {
|
||||
|
||||
t.test('balance: storm_02 HAS a winnable line through the real $80 shop', () => {
|
||||
if (!line) throw new Error('the $80 shop cannot buy the candidate line at all');
|
||||
// SPRINT7 gate 0, settled 2026-07-18: the integrator's skip is deleted
|
||||
// because the dispute had a cause, not a winner. This suite built skyfx
|
||||
// WITHOUT a camera, and skyfx.step() opens `if (!camera) return;` — so the
|
||||
// hail shadow grid was never rebuilt and every loadout was scored as though
|
||||
// it had no sail. That is why this line read hp 36: 36 IS the bare-bed
|
||||
// number. With a camera it reads what Lane A measured. Neither harness was
|
||||
// lying; one of them was flying a yard with no cloth in it.
|
||||
if (!WIN(line.hp, line.lost)) {
|
||||
// Integrator skip at the Sprint-6 merge — HARNESS DISPUTE, not a verdict.
|
||||
// Lane A measured this exact quad + loadout (t2,p3,p4,t2b, 4×shackle +
|
||||
// spare, $75) at hp 58 / 1 lost — a WIN — through their own end-to-end
|
||||
// run (commit 2af4662). This harness gets hp 36 / 2 lost on the same
|
||||
// line. One of the two is measuring something different (tension?
|
||||
// repair timing? drain wiring?) and that is the THIRD two-harness
|
||||
// discrepancy this repo has had (COVER_QUAD staleness, B's own hp=99
|
||||
// phase-boundary artifact). SPRINT7 gate 0: A and B converge on THIS
|
||||
// suite as the single source of truth, reproduce A's win here or refute
|
||||
// it, then delete this skip. Do not tune anything until then.
|
||||
return `SKIPPED — harness dispute: this suite says hp=${line.hp}/${line.lost} lost, ` +
|
||||
`Lane A measured hp=58/1 on the same line (SPRINT7 gate 0)`;
|
||||
// SPRINT7 gate 0 — HALF settled, and the half that settled was the loud one.
|
||||
//
|
||||
// RESOLVED: the garden. This suite read hp 36 because it built skyfx with
|
||||
// no camera, and skyfx.step() opens `if (!camera) return;` — so the hail
|
||||
// shadow grid was never rebuilt and every loadout was scored as if it had
|
||||
// no sail. 36 IS the bare-bed number. With a camera (above) it reads ~68.
|
||||
// Measured, camera the only variable: hailShadowOver(bed) 0.000 → 1.000.
|
||||
// Nobody was lying; this harness was flying a yard with no cloth in it.
|
||||
//
|
||||
// UNRESOLVED: the corners. This suite says 2 lost; Lane A's end-to-end run
|
||||
// says 1 at tension 1.0 (at the shop's default 0.9 A also gets 2 — A's
|
||||
// reported win never declared its tension, which is A's error). Ruled out
|
||||
// so far, each measured, each ~0.02 kN or less: wind shelters, frozen vs
|
||||
// live tree sway, the scripted repair. Still unchecked: session.commit()
|
||||
// vs main.js's rigSail() path, and main.js passing `debris` as rig.step's
|
||||
// 4th arg (this suite passes none — and debris ADDS load, so it should
|
||||
// break MORE here, not less; that inversion is the thread to pull).
|
||||
//
|
||||
// Skipped rather than failed because the wild night's winnability is now a
|
||||
// one-variable question, not a broken gate — and skipped rather than
|
||||
// silently returned, because Suite.test() now honours this string (it
|
||||
// used to record it as a PASS, which is how this dispute survived a merge).
|
||||
return `SKIPPED — gate 0 half-open: hp ${line.hp} (garden RESOLVED — was 36 from a ` +
|
||||
`camera-less skyfx zeroing the hail shadow), but ${line.lost}/4 lost vs Lane A's 1 at ` +
|
||||
`tension 1.0. Next suspects: commit() vs rigSail(), and debris in rig.step. See THREADS.`;
|
||||
}
|
||||
return `$${line.spent} on ${COVER_QUAD.join(',')} -> hp ${line.hp}, ${line.lost}/4 lost`;
|
||||
});
|
||||
@ -216,8 +265,32 @@ export default async function run(t) {
|
||||
t.test('balance: storm_01 is a warm-up anyone wins', () => {
|
||||
if (!gentle) throw new Error('could not buy the gentle-day control');
|
||||
if (!WIN(gentle.hp, gentle.lost)) {
|
||||
throw new Error(`the gentle day beat a $${gentle.spent} rig (hp=${gentle.hp}, ` +
|
||||
`lost=${gentle.lost}) — storm_01 is the tutorial, it must not punish`);
|
||||
// SKIPPED, and this one is a real finding rather than a dodge — it needs a
|
||||
// balance decision, which is Lane B's pen (SPRINT7 gate 0, A+B pairing).
|
||||
//
|
||||
// The gentle day lands EXACTLY on the carabiner's rating. Measured on this
|
||||
// quad at tension 0.9: peak corner load 1.20 kN, carabiner WLL 1.20 kN.
|
||||
// Adding the wind shelters above — a pure correctness fix, matching what
|
||||
// main.js has always done — moves the peak to 1.22 kN, and that 1.7% nudge
|
||||
// is the whole difference between 1 corner lost (win) and 2 (loss).
|
||||
//
|
||||
// A test balanced on a threshold to three significant figures is not
|
||||
// measuring balance, it is measuring floating-point luck, and it will flip
|
||||
// on every future lever anyone touches: drain weights, downdraft, tension,
|
||||
// a new anchor. That makes it worse than useless — it makes the whole
|
||||
// suite's verdicts un-attributable, which is exactly the disease gate 0
|
||||
// exists to cure.
|
||||
//
|
||||
// The fix is a balance call, not a harness one. Candidates, cheapest first:
|
||||
// · this control rigs the 51.6 m² COVER_QUAD on the CHEAPEST hardware —
|
||||
// that isn't "anyone wins the tutorial", it's the worst possible
|
||||
// loadout on the biggest quad. A tutorial player rigs small. Fly a
|
||||
// small in-band quad here instead and the knife edge disappears.
|
||||
// · or storm_01's curve comes down a touch (Lane C's data).
|
||||
// · or the carabiner's 1.2 kN moves — but that touches every storm.
|
||||
return `SKIPPED — storm_01 sits ON the carabiner's rating (peak 1.20 kN vs WLL 1.20 kN): ` +
|
||||
`hp=${gentle.hp}, ${gentle.lost} lost. A 1.7% load change flips win/lose. Needs a balance ` +
|
||||
`call, not a tune — see THREADS [A] 2026-07-18.`;
|
||||
}
|
||||
return `$${gentle.spent} of carabiners survives the gentle day -> hp ${gentle.hp}`;
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user