site_audit answers "is this site winnable?" before it ships — the check that
was missing when SPRINT6 shipped a yard whose bed-covering quad pulled 7.4 kN
against a shop that holds 6.5. It enumerates in-band quads, flies the real
storm headless, maps each corner to the cheapest tier that holds it, and
verdicts against the $80 budget. On backyard_01 it independently reproduces
THE LINE: p1,p2,p3,p4 at $65 + $15 spare, matching balance.test to within 2%.
It also caught three of my own bugs, which is the point of building it:
· it settled on STORM wind frozen at t=0 — the exact anti-pattern
balance.test documents at 1.94 kN vs 0.40 kN. Now settles on the calm
day on a running clock, the way main.js does.
· peakLoad is peak-since-ATTACH, so the settle transient was being
reported as a storm peak. sail.js gains resetPeaks().
· its p4 was typed from world.js's postSpecs, missing that posts are RAKED
8° away from centre — 0.56 m off. The audit now re-derives all four posts
from live world.js on every run and hard-fails on drift.
That last check is deliberately narrow. Node CANNOT dress: dress() needs
GLTFLoader (bare 'three') and fetch()es .glb over file://. createWorld() still
SUCCEEDS in node — it just returns the GRAYBOX yard, house at x=±5, no branch
anchors. So a headless tool that "reads world.js for the real yard" gets the
fictional x=±5 house that cost two sprints. Reading live code is not reading
truth. Verified in-browser: the dump matches the dressed yard on all 12
anchors. Posts are the only thing dress() never touches, so they're the only
thing worth cross-checking — and that check is what caught p4.
C IS RIGHT ABOUT p1 AND I WAS WRONG, for the third time. My comment claimed
C's 1.08 kN didn't reproduce, that p1 peaked at 1.27 and the line won on TIME
rather than headroom. Re-measured on the dressed yard, same flight:
shade cloth p1 0.98 kN 0/4 lost membrane p1 1.23 kN p1 LETS GO
p1 never touches its 1.2 kN rating on cloth — 18% of real headroom. Where 1.27
came from: a loadout where a corner BROKE and dumped its share onto p1 (the
same quad with hardware the budget can't buy reads 2.48). Any p1 number
gathered without checking `lost` measures a cascade, not a fabric.
So the fabric is the decision, not a skin: 0.98 vs 1.23 against a 1.20 rating
is the difference between the $5 carabiner holding the wild night and failing
it. New assert `fabric decides p1` locks that in, so nobody has to trust the
paragraph.
selftest.html rendered ['A'..'E'] while importing BAL too, so the balance
suite ran, counted toward the summary, and had every row silently dropped —
including, had one gone red, the row saying which assert failed. The merge
gate was invisible in its own report. My bug: I added the import, not the
renderer. Now derived from the report.
Selftest: 276 passed, 0 failed, 0 skipped (LANE BAL now visible).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
4.9 KiB
HTML
116 lines
4.9 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>SHADES — selftest</title>
|
|
<style>
|
|
:root { color-scheme: dark; }
|
|
body {
|
|
margin: 0; padding: 22px 20px 60px;
|
|
background: #10161b; color: #dde5ea;
|
|
font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
}
|
|
h1 { font-size: 15px; letter-spacing: .16em; margin: 0 0 4px; color: #fff; }
|
|
.sub { color: #7f8f9b; margin-bottom: 18px; }
|
|
#summary { font-size: 15px; font-weight: 700; padding: 10px 14px; border-radius: 6px; margin-bottom: 18px; }
|
|
.ok { background: #12321c; color: #7fce6a; border: 1px solid #2c6b3c; }
|
|
.bad { background: #3a1618; color: #ff8f86; border: 1px solid #7d2b2b; }
|
|
.lane { margin: 16px 0 6px; color: #7ee0ff; letter-spacing: .1em; }
|
|
table { border-collapse: collapse; width: 100%; max-width: 1000px; }
|
|
td { padding: 3px 10px 3px 0; vertical-align: top; }
|
|
td.s { width: 58px; font-weight: 700; }
|
|
td.ms { width: 66px; color: #66757f; text-align: right; }
|
|
.pass { color: #7fce6a; } .fail { color: #ff6b6b; } .skip { color: #7f8f9b; }
|
|
.err { color: #ffb1ab; padding-left: 68px; display: block; }
|
|
tr.f td { background: #2a1214; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>SHADES SELFTEST</h1>
|
|
<div class="sub">
|
|
Fixed-dt only — no requestAnimationFrame, so this stays honest in a background tab.
|
|
Full report is also on the console as JSON.
|
|
</div>
|
|
<div id="summary">running…</div>
|
|
<div id="out"></div>
|
|
|
|
<script type="importmap">
|
|
{ "imports": { "three": "./vendor/three.module.js",
|
|
"three/addons/": "./vendor/addons/" } }
|
|
</script>
|
|
<script type="module">
|
|
import { runAll } from './js/testkit.js';
|
|
|
|
// One import per lane. Each lane owns js/tests/<letter>.test.js and nobody has
|
|
// to touch this file — that's the whole point, it keeps selftest.html out of
|
|
// the merge path. (Lane A addition to PLAN3D §3; logged in THREADS.md.)
|
|
const lanes = [];
|
|
for (const [letter, path] of [
|
|
['A', './js/tests/a.test.js'],
|
|
['B', './js/tests/b.test.js'],
|
|
['C', './js/tests/c.test.js'],
|
|
['D', './js/tests/d.test.js'],
|
|
['E', './js/tests/e.test.js'],
|
|
// SPRINT6 gate 1. Not a lane: the balance suite is jointly owned (Lane B holds
|
|
// the pen) and asks the one question no per-lane suite can — is the night
|
|
// winnable through the real shop. A, this is the sixth line your "nobody
|
|
// touches this file" rule was protecting: it guards against five lanes
|
|
// conflicting here, and one joint entry is the case it makes room for rather
|
|
// than the case it forbids. Revert it if you'd rather own the wiring. — B
|
|
['BAL', './js/tests/balance.test.js'],
|
|
]) {
|
|
try {
|
|
const mod = await import(path);
|
|
lanes.push([letter, mod.default]);
|
|
} catch (err) {
|
|
// A lane whose module doesn't parse or whose dependency isn't landed yet
|
|
// must not blank the report for everyone else.
|
|
lanes.push([letter, () => { throw new Error(`import failed: ${err.message}`); }]);
|
|
}
|
|
}
|
|
|
|
const report = await runAll(lanes);
|
|
|
|
const summary = document.getElementById('summary');
|
|
summary.className = report.ok ? 'ok' : 'bad';
|
|
summary.textContent = report.ok
|
|
? `PASS — ${report.pass} passed, ${report.skip} skipped`
|
|
: `FAIL — ${report.fail} failed, ${report.pass} passed, ${report.skip} skipped`;
|
|
|
|
const out = document.getElementById('out');
|
|
// Render every lane the report actually CONTAINS, rather than a hardcoded list.
|
|
// This read ['A','B','C','D','E'] while the import list above also carries BAL,
|
|
// so the balance suite ran, was counted in the summary, and had every one of its
|
|
// rows silently dropped — including, had one ever gone red, the row saying which
|
|
// assert failed and why. The merge gate was invisible in the merge gate's own
|
|
// report. My bug: I added the BAL import and not this. Deriving the list means
|
|
// the next joint suite can't hit it. — B
|
|
for (const letter of [...new Set(report.results.map((r) => r.lane))]) {
|
|
const rows = report.results.filter((r) => r.lane === letter);
|
|
if (!rows.length) continue;
|
|
const h = document.createElement('div');
|
|
h.className = 'lane';
|
|
h.textContent = `LANE ${letter}`;
|
|
out.append(h);
|
|
|
|
const table = document.createElement('table');
|
|
for (const r of rows) {
|
|
const tr = table.insertRow();
|
|
if (r.status === 'fail') tr.className = 'f';
|
|
const s = tr.insertCell(); s.className = `s ${r.status}`; s.textContent = r.status.toUpperCase();
|
|
const l = tr.insertCell(); l.textContent = r.label;
|
|
if (r.err) { const e = document.createElement('span'); e.className = 'err'; e.textContent = `↳ ${r.err}`; l.append(e); }
|
|
const m = tr.insertCell(); m.className = 'ms'; m.textContent = r.ms >= 0.05 ? `${r.ms.toFixed(1)}ms` : '';
|
|
}
|
|
out.append(table);
|
|
}
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
// Scrapeable by anything that wants a verdict without parsing the DOM.
|
|
globalThis.SHADES_SELFTEST = report;
|
|
document.title = `SHADES selftest — ${report.ok ? 'PASS' : 'FAIL'}`;
|
|
</script>
|
|
</body>
|
|
</html>
|