HardYards/tools/site_audit/sweep.selftest.js
type-two 9700b40a30 Wire ratingHint into the failure threshold: the ratings are real (A's ruling)
sail.js _checkFailure now fails a corner on load > hw.rating * anchor.ratingHint
(read live from the anchor — dress() mutates in place). Consequence asserted,
not the formula: on site_02's carport line (uniform rated shackles, funnel on,
12 s calm settle) a cb corner blows before any q corner — at DEFAULT tension
(t~29 s into the storm, honest posts hold) and at max. In-suite control pins
the attribution: the same rig with every hint forced to 1 holds 4/4, so the
break is the hint, not the load. Mutation-checked: unwiring the line fails both.

rigging.js weak-link (D's #7): summary.weakest reduces on rating * ratingHint
via _effRating, not bare hw.rating — uniform hardware now flags the genuinely
worst steel instead of the first click. Mutation-checked.

site_audit prices the anchor, not just the steel: tierFor(peak, hint) needs
h.rating * hint >= peak, both front-ends carry ratingHint through (browser off
world.anchors, node dump extended with the live-dumped hints), rows print the
hint. sweep.selftest asserts hints reprice without touching peaks and land
over-ceiling corners in unholdable. Mutation-checked.

Selftest 319/0/0 (315 + 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:19:01 +10:00

123 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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`);
}
});
test('pricing reads the ANCHOR, not just the steel — ratingHint reaches tierFor', () => {
// SPRINT12: sail.js fails a corner on rating × ratingHint, so the sweep must
// price hardware against the same product or it audits a nicer yard than
// ships — a fascia (0.35) or carport beam (0.22) corner would be sold a $5
// carabiner that the sim now snaps. Same synthetic yard, one anchor's hint
// dropped to 0.01: its corner's demand (peak / hint) leaves the shop's 6.5 kN
// ceiling entirely, so its tier must go to NONE while every OTHER corner and
// every PEAK stays byte-identical (the hint is a pricing fact, not physics —
// the sweep flies unbreakable audit hardware). Written to fail if the hint
// is dropped from sweep.js's tierFor call: the two runs collapse into one.
const sweep = (anchors) => auditSweep({ anchors, bed: BED, stormDef: STORM, calmDef: CALM, venturi: [] }).rows[0];
const honest = sweep(ANCHORS);
const lied = sweep(ANCHORS.map((a) => (a.id === 'a1' ? { ...a, ratingHint: 0.01, sway: a.sway } : a)));
for (let i = 0; i < honest.tiers.length; i++) {
assert(Math.abs(honest.tiers[i].peak - lied.tiers[i].peak) < 1e-9,
`corner ${honest.tiers[i].id}: ratingHint moved a PEAK (${honest.tiers[i].peak} -> ${lied.tiers[i].peak}) — ` +
'the hint is a failure threshold, it must never touch the flown loads');
}
const a1 = lied.tiers.find((c) => c.id === 'a1');
const a1honest = honest.tiers.find((c) => c.id === 'a1');
assert(a1honest.tier, 'fixture drift: a1 must be holdable at hint 1 or this test asserts nothing');
assert(!a1.tier,
`a1 at hint 0.01 (demand ${(a1.peak / 0.01 / 1000).toFixed(1)} kN vs the 6.5 kN ceiling) still got ` +
`hardware ($${a1.tier?.cost}) — sweep.js is pricing bare hw.rating, not rating × ratingHint`);
const others = lied.tiers.filter((c) => c.id !== 'a1');
assert(others.every((c) => c.tier && c.tier.cost === honest.tiers.find((h) => h.id === c.id).tier.cost),
'a hint on a1 repriced a DIFFERENT corner — hints must be per-anchor');
assert(lied.unholdable.some((c) => c.id === 'a1'),
'an over-ceiling corner must land in unholdable, or the verdict lies');
});
export const SWEEP_TESTS = TESTS;