site_audit's headless false-negative is fixed by the browser front-end (A's loadSite/createWorld landed in main — that was all it waited on), and fixing it surfaced a worse bug underneath: the sweep built its wind from the STORM def alone and never called setVenturi, which main.js:424 does at every site load. The venturi is SITE data, so every site_02 audit ever run — including the numbers quoted at C in SPRINT10 — was of a corner block with no gap. The SPRINT6 p1=7.4kN failure inverted: there the tool called a good site unriggable, here it called a mean site cheap. - sweep.js: setVenturi on both winds (the gap doesn't switch off for the settle); both front-ends pass the site's funnel and print it, so a funnelled run is legible as one. - sweep.selftest.js (new, wired into b.test.js): the auditor gets an auditor. Funnel must strictly raise every corner; a funnel-less site must be byte-identical. Verified it fails when the setVenturi calls are deleted. - rigging.js: setWorld(world) + session.setBudget(n)/setAnchors() — A's two asks. Markers are rebuilt (the anchor set changes across sites) and disposed; stale picks die with the old yard. Deletes A's "one private touch" into this module. - dev_rigging.html: dead since SPRINT10 (called createWorld without a site, which now throws) and nothing noticed, because a dev page has no selftest to go red. Fixed, dressed, and given the site switch it should have had (N = next yard). Verdict for C/A, in THREADS: site_02 PASSES with the funnel on — 64 of 66 lines affordable, cheapest $20. The funnel kills nothing, so the gain does NOT need dropping and E's tree stays put. It's a reach problem, not a gain problem: the throat's radius is 5 m and the bed centre is 6.08 m away, so the gap screams over the carport at +50% and reaches the garden at +0.0%. selftest 300/0/0 (296 + 4 new).
91 lines
4.2 KiB
JavaScript
91 lines
4.2 KiB
JavaScript
/**
|
|
* sweep.selftest.js — the audit tool audits itself. [Lane B, SPRINT11]
|
|
*
|
|
* Same shape as sail.selftest.js / rigging.selftest.js: [name, fn] pairs, so one
|
|
* set of asserts runs under both Lane A's selftest.html (via js/tests/b.test.js)
|
|
* and plain node.
|
|
*
|
|
* WHY THIS FILE EXISTS, in one sentence: site_audit shipped SPRINT9 and SPRINT10
|
|
* with no test of its own, and in SPRINT11 it turned out to have been flying the
|
|
* corner block with the funnel switched OFF.
|
|
*
|
|
* The venturi lives in the SITE json; the sweep built its wind from the STORM def
|
|
* alone and never called setVenturi, which main.js:424 does at every site load.
|
|
* So the tool whose entire job is "catch a site that lies about its difficulty"
|
|
* was itself lying about the difficulty of the only site that has a venturi —
|
|
* under-reporting every corner load on the yard whose whole personality is the
|
|
* gap. It reported PASS with $20 lines. That is the SPRINT6 p1=7.4 kN failure
|
|
* inverted: there the tool called a fine site unriggable, here it called a mean
|
|
* site cheap. A tool built to catch drift must not be the thing that drifts.
|
|
*
|
|
* The assert below is written to FAIL if the setVenturi calls are removed from
|
|
* sweep.js: with the funnel dropped, the funnelled and unfunnelled sweeps
|
|
* collapse to the same peak loads and the strict inequality goes red.
|
|
*/
|
|
|
|
import { auditSweep } from './sweep.js';
|
|
|
|
const TESTS = [];
|
|
const test = (name, fn) => TESTS.push([name, fn]);
|
|
const assert = (cond, msg) => { if (!cond) throw new Error(msg); };
|
|
|
|
/**
|
|
* A deliberately synthetic yard, not site_02: four anchors around a bed, sized to
|
|
* sit inside AUDIT.BAND so exactly one quad sweeps. Synthetic because this test
|
|
* pins the SWEEP's plumbing, not the corner block's balance — site_02's numbers
|
|
* are C's to tune and would make this assert fail every time they moved.
|
|
*/
|
|
const ANCHORS = [
|
|
{ id: 'a1', type: 'post', pos: { x: -3, y: 3.9, z: -3 } },
|
|
{ id: 'a2', type: 'post', pos: { x: 3, y: 3.9, z: -3 } },
|
|
{ id: 'a3', type: 'post', pos: { x: 3, y: 3.9, z: 3 } },
|
|
{ id: 'a4', type: 'post', pos: { x: -3, y: 3.9, z: 3 } },
|
|
].map((a) => ({ ...a, sway: () => a.pos }));
|
|
|
|
const BED = { x: 0, z: 0, w: 4, d: 4 };
|
|
|
|
/** A storm blowing dead along +Z, and a funnel whose axis matches it. */
|
|
const STORM = {
|
|
id: 'sweep_selftest_storm', duration: 8, dir: Math.PI / 2, base: 14,
|
|
gusts: { every: 3, peak: 1.6, downdraftOfTotal: 0.2 },
|
|
};
|
|
const CALM = { id: 'sweep_selftest_calm', duration: 8, dir: Math.PI / 2, base: 3, gusts: null };
|
|
|
|
/** Centred on the bed, wide enough to swallow it, aligned with the storm. */
|
|
const FUNNEL = [{ x: 0, z: 0, axis: Math.PI / 2, gain: 2.0, radius: 12, sharp: 1 }];
|
|
|
|
const peaksOf = (venturi) => {
|
|
const { rows } = auditSweep({ anchors: ANCHORS, bed: BED, stormDef: STORM, calmDef: CALM, venturi });
|
|
assert(rows.length > 0, 'sweep selftest yard produced no candidate quad — fix the fixture, not the test');
|
|
return rows[0].tiers.map((c) => c.peak);
|
|
};
|
|
|
|
test('sweep honours the SITE venturi, not just the storm', () => {
|
|
const bare = peaksOf([]);
|
|
const funnelled = peaksOf(FUNNEL);
|
|
|
|
assert(bare.length === funnelled.length, 'the two sweeps disagree about corner count');
|
|
|
|
// Every corner must pull HARDER through the funnel. Strictly — an equal read
|
|
// is the exact bug: it means setVenturi never reached the wind.
|
|
for (let i = 0; i < bare.length; i++) {
|
|
assert(funnelled[i] > bare[i],
|
|
`corner ${i}: funnelled peak ${(funnelled[i] / 1000).toFixed(2)} kN is not above bare ` +
|
|
`${(bare[i] / 1000).toFixed(2)} kN — the site's venturi is not reaching the sweep's wind ` +
|
|
`(sweep.js must call wind.setVenturi, the way main.js does at every site load)`);
|
|
}
|
|
});
|
|
|
|
test('no venturi is a no-op — a site without a funnel is untouched', () => {
|
|
// The other half of the contract: backyard_01 ships "venturi": [], and this
|
|
// fix must not have moved a single number on it. Default arg == explicit [].
|
|
const omitted = peaksOf(undefined);
|
|
const empty = peaksOf([]);
|
|
for (let i = 0; i < empty.length; i++) {
|
|
assert(Math.abs(omitted[i] - empty[i]) < 1e-9,
|
|
`corner ${i}: omitting venturi read ${omitted[i]} but [] read ${empty[i]} — a funnel-less site must be byte-identical`);
|
|
}
|
|
});
|
|
|
|
export const SWEEP_TESTS = TESTS;
|