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>
123 lines
6.4 KiB
JavaScript
123 lines
6.4 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`);
|
||
}
|
||
});
|
||
|
||
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;
|