HardYards/tools/storm_envelope/envelope.html
type-two 8208c80d78 storm_envelope: the wind side of the failure envelope, per anchor (gate 2.4)
Second harness on B's post-ratingHint audit: measures what the STORM
delivers at every dressed anchor — peak speedAt, tPeak, dynamic pressure
(downdraft folded back in), dose, and Pa/hint (the wind-side failure-order
prediction under load > rating x ratingHint). No cloth, no rig, no tension:
independent by construction.

Browser front-end dresses the yard the way the game does (createWorld +
dress) and wires venturi + tree shelters exactly as main.js:446-447 does.
envelope.selftest.js wired into c.test.js, every assert written to fail if
a wiring line is deleted (sweep.selftest's pattern). Selftest 320/0/0.

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

141 lines
6.3 KiB
HTML
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.

<!doctype html>
<!--
storm_envelope, browser front-end. [Lane C, SPRINT12 — gate 2.4's second harness]
B's site_audit measures the failure envelope through the CLOTH (peak corner
loads, kN). This measures the same envelope through the STORM: what the wind
actually delivers at every dressed anchor, per night — peak local speed, when,
dynamic pressure, dose, and the pressure/hint ranking that predicts failure
ORDER under B's `load > rating × ratingHint` wiring. Two harnesses, one
envelope; if they disagree, find the variable before anyone retunes E's hints.
Dressed positions the way the game gets them — createWorld(await loadSite(name))
then dress() — because a graybox envelope measures a yard that does not ship
(site_audit's own lesson, kept).
tools/storm_envelope/envelope.html ← the whole week
tools/storm_envelope/envelope.html?site=site_02_corner_block&storm=storm_03b_earlybuster
Served from the repo root (server.py).
-->
<meta charset="utf-8">
<title>storm_envelope</title>
<style>
body { background:#111; color:#ddd; font:13px/1.5 ui-monospace,Menlo,monospace; margin:0; padding:20px; }
h1 { font-size:15px; color:#fff; margin:0 0 2px; }
h2 { font-size:13px; color:#9cf; margin:18px 0 2px; }
.sub { color:#888; margin-bottom:10px; white-space:pre-wrap; }
table { border-collapse:collapse; margin:6px 0; }
th { text-align:right; color:#789; font-weight:normal; padding:1px 12px 1px 0; }
th:first-child, td:first-child { text-align:left; }
td { padding:1px 12px 1px 0; white-space:nowrap; text-align:right; }
.weak { color:#e96; } .probe { color:#678; font-style:italic; }
</style>
<h1>storm_envelope — the wind side of the failure envelope</h1>
<div class="sub" id="sub">loading…</div>
<div id="out"></div>
<script type="importmap">
{ "imports": {
"three": "../../web/world/vendor/three.module.js",
"three/addons/": "../../web/world/vendor/addons/"
} }
</script>
<script type="module">
import * as THREE from '../../web/world/vendor/three.module.js';
import { createWorld, loadSite } from '../../web/world/js/world.js';
import { NIGHTS } from '../../web/world/js/week.js';
import { stormEnvelope } from './envelope.js';
const q = new URLSearchParams(location.search);
const el = (id) => document.getElementById(id);
const loadJSON = async (path) => (await fetch(path)).json();
// One dressed world per site, cached — the yard doesn't change between storms.
const siteCache = new Map();
async function dressedSite(name) {
if (siteCache.has(name)) return siteCache.get(name);
const site = await loadSite(name);
const calmStub = {
sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
gustTelegraph: () => null, setSheltersFromTrees() {}, eventsBetween: () => [],
};
const world = createWorld(new THREE.Scene(), { wind: calmStub, site });
let dressed = false;
if (world.dress) { try { await world.dress(); dressed = true; } catch (e) { /* flagged below */ } }
const anchors = world.anchors.map((a) => ({
id: a.id, type: a.type, ratingHint: a.ratingHint,
pos: { x: a.pos.x, y: a.pos.y, z: a.pos.z },
}));
const out = { site, anchors, dressed, bed: world.gardenBed };
siteCache.set(name, out);
return out;
}
function render(title, siteName, stormName, res, dressed) {
const h = document.createElement('h2');
h.textContent = title;
el('out').appendChild(h);
const sub = document.createElement('div');
sub.className = 'sub';
sub.textContent = `${siteName} × ${stormName} — downdraftOfTotal ${res.downFrac}, ${res.duration}s`
+ (dressed ? '' : ' ⚠ dress() FAILED — graybox positions, not what ships');
el('out').appendChild(sub);
const tbl = document.createElement('table');
const head = tbl.insertRow();
for (const c of ['anchor', 'type', 'hint', 'peak m/s', 'tPeak s', 'peak Pa', 'Pa/hint', 'dose m²/s', 'eff. cara/shackle/rated N']) {
const th = document.createElement('th'); th.textContent = c; head.appendChild(th);
}
for (const r of res.rows) {
const tr = tbl.insertRow();
if (r.probe) tr.className = 'probe';
else if (r.hint < 0.5) tr.className = 'weak';
tr.insertCell().textContent = r.id;
tr.insertCell().textContent = r.type;
tr.insertCell().textContent = r.hint.toFixed(2);
tr.insertCell().textContent = r.peak.toFixed(2);
tr.insertCell().textContent = r.tPeak.toFixed(1);
tr.insertCell().textContent = r.peakPa.toFixed(0);
tr.insertCell().textContent = r.paPerHint.toFixed(0);
tr.insertCell().textContent = r.dose.toFixed(0);
tr.insertCell().textContent = r.probe ? '—' : r.effN.map((n) => n.toFixed(0)).join(' / ');
}
el('out').appendChild(tbl);
}
async function runPair(siteName, stormName, title) {
const { site, anchors, dressed, bed } = await dressedSite(siteName);
const stormDef = await loadJSON(`../../web/world/data/storms/${stormName}.json`);
const venturi = site.wind?.venturi ?? [];
const probes = [{ id: 'bed centre', pos: { x: bed.x, z: bed.z } }];
if (venturi.length) probes.push({ id: 'throat', pos: { x: venturi[0].x, z: venturi[0].z } });
const res = stormEnvelope({ anchors, stormDef, venturi, probes });
render(title, siteName, stormName, res, dressed);
return { site: siteName, storm: stormName, dressed,
rows: res.rows.map(({ id, type, hint, peak, tPeak, peakPa, paPerHint, dose }) =>
({ id, type, hint, peak, tPeak, peakPa, paPerHint, dose })) };
}
async function run() {
const single = q.get('site') || q.get('storm');
const pairs = single
? [{ site: q.get('site') || 'backyard_01', storm: q.get('storm') || 'storm_02_wildnight', title: 'requested pair' }]
: NIGHTS.map((n, i) => ({ site: n.site, storm: n.storm, title: `night ${i + 1}` }));
el('sub').textContent = `${pairs.length} pair(s) · sampling at FIXED_DT over each storm · `
+ `venturi + tree shelters wired the way main.js wires them`;
const results = [];
for (const p of pairs) results.push(await runPair(p.site, p.storm, p.title));
// machine-readable, so the page can be driven headless-in-browser
window.__envelope = results;
document.title = 'storm_envelope — done';
}
run().catch((e) => {
el('sub').textContent = `storm_envelope crashed: ${e.message}\n${e.stack || ''}`;
window.__envelope = { error: e.message };
document.title = 'storm_envelope — ERROR';
});
</script>