Gate 1 evidence, posted early because C's model fix and A's export are both downstream of it. garden_probe.html flies the sim's own garden loop (skyfx gardenExposure + gardenHailExposure, stepped the way main.js does) per rig line and prints the garden HP the sim would report. Browser-only by necessity: skyfx needs `document` (node throws), so garden prediction can never live in audit.mjs. The finding, every corner held on rated shackles (the sail's best case): night 1 gentle no hail bare 97.6 best 99.1 sail worth +1.5 night 2 southerly 0.7 bare 83.7 best 90.1 sail worth +6.4 night 3 buster 0.7 bare 82.6 best 90.1 sail worth +7.5 night 4 wild night 1.3 bare 35.7 best 60.5 sail worth +24.8 night 5 ice night 1.4 bare 0.0 best 27.7 sail worth +27.7 The separation IS hail size, exactly as A's GARDEN_DRAIN comment predicted: cloth blocks 100% of 1.3+ stones, 74% of pea hail, and rain at 73 degrees walks under and does nothing. No value of GARDEN_DRAIN can separate a good rig from none — it scales both by the same factor. Also found: my own audit's cover% is the SUN shadow, which C's skyfx comment already called "a number about nothing" during a storm. On the wild night it predicts the garden BACKWARDS (r = -0.42): the best garden line is p1,p2,p3,p4 at 25% cover / $65 / 60.5 HP, beating a 71% cover / $120 line at 52.8 HP. D's "$20 rig banked 39/45" and the QA's +$97 are the same fact — I sorted the table by the wrong quantity. Flagged to A: the ice night's garden cannot be saved at any price (best 27.7 vs WIN >= 50), and night 1's sail is worth 1.5 HP. The probe carries a TEMPORARY marked copy of main.js's private garden constants; asked A to export the model so audit and sim share one copy. Nothing ships against the copy — the probe is evidence, not a feature.
195 lines
9.1 KiB
HTML
195 lines
9.1 KiB
HTML
<!doctype html>
|
|
<!--
|
|
garden_probe — does cover% predict the garden? [Lane B, SPRINT13 gate 1.2]
|
|
|
|
The QA pass found one variable sighted three times: the audit's "cover" called
|
|
a $20 rig bad and the sim paid it +$97. This page is the measurement behind
|
|
that — it flies the SIM's own garden loop (skyfx's gardenExposure /
|
|
gardenHailExposure, stepped exactly the way main.js:931-941 steps it) for a
|
|
set of rig lines, and prints predicted garden HP beside the audit's cover%.
|
|
|
|
It is a PROBE, not the fix: the garden drain weights are private to main.js, so
|
|
the constants below are a temporary copy, marked and asked-about in THREADS.
|
|
The moment A exports the garden model this page imports it and the copy dies.
|
|
Nothing ships against a duplicated constant — that is how this repo gets bitten.
|
|
|
|
Browser-only by necessity: skyfx needs `document` (verified — node throws
|
|
"document is not defined"), so garden prediction can never live in audit.mjs.
|
|
|
|
tools/site_audit/garden_probe.html?site=site_02_corner_block&storm=storm_03b_earlybuster
|
|
-->
|
|
<meta charset="utf-8">
|
|
<title>garden_probe</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; }
|
|
.sub { color:#888; margin-bottom:14px; white-space:pre-wrap; }
|
|
table { border-collapse:collapse; margin:8px 0; }
|
|
td, th { padding:2px 12px 2px 0; white-space:nowrap; text-align:left; }
|
|
th { color:#9ab; border-bottom:1px solid #333; }
|
|
.ok { color:#6c6; } .bad { color:#e66; } .warn { color:#dc6; }
|
|
.note { color:#888; margin-top:14px; white-space:pre-wrap; }
|
|
</style>
|
|
<h1>garden_probe — does cover% predict the garden?</h1>
|
|
<div class="sub" id="sub">loading…</div>
|
|
<div id="out"></div>
|
|
<div class="note" id="note"></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 { createWind } from '../../web/world/js/weather.js';
|
|
import { createSkyFx } from '../../web/world/js/skyfx.js';
|
|
import { SailRig } from '../../web/world/js/sail.js';
|
|
import { HARDWARE, FIXED_DT } from '../../web/world/js/contracts.js';
|
|
import { AUDIT, auditSweep } from './sweep.js';
|
|
|
|
// ⚠️ TEMPORARY COPY — main.js's garden model is module-private (HAIL_WEIGHT,
|
|
// RAIN_WEIGHT, GARDEN_DRAIN, createGarden). Asked A to export it in THREADS;
|
|
// this dies the moment it lands. Do not let this page outlive the ask.
|
|
const HAIL_WEIGHT = 5.0, RAIN_WEIGHT = 0.25, GARDEN_DRAIN = 0.9;
|
|
|
|
const q = new URLSearchParams(location.search);
|
|
const siteName = q.get('site') || 'site_02_corner_block';
|
|
const stormName = q.get('storm') || 'storm_03b_earlybuster';
|
|
const el = (id) => document.getElementById(id);
|
|
const loadJSON = async (p) => (await fetch(p)).json();
|
|
|
|
const calmStub = {
|
|
sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
|
|
speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
|
|
gustTelegraph: () => null, setSheltersFromTrees() {}, setVenturi() {}, eventsBetween: () => [],
|
|
};
|
|
|
|
async function run() {
|
|
const site = await loadSite(siteName);
|
|
const scene = new THREE.Scene();
|
|
const world = createWorld(scene, { wind: calmStub, site });
|
|
await world.dress();
|
|
const bed = world.gardenBed;
|
|
const venturi = site.wind?.venturi ?? [];
|
|
|
|
const anchors = world.anchors.map((a) => {
|
|
const pos = { x: a.pos.x, y: a.pos.y, z: a.pos.z };
|
|
return { id: a.id, type: a.type, ratingHint: a.ratingHint, pos, sway: () => pos };
|
|
});
|
|
|
|
const stormDef = await loadJSON(`../../web/world/data/storms/${stormName}.json`);
|
|
const calmDef = await loadJSON(`../../web/world/data/storms/${AUDIT.CALM_STORM}.json`);
|
|
|
|
// The audit's own verdict, for its cover% numbers.
|
|
const { rows } = auditSweep({ anchors, bed, stormDef, calmDef, venturi });
|
|
|
|
/**
|
|
* Fly one line and return the garden HP the SIM would report.
|
|
* Mirrors main.js: sky.step(dt, t, {sail: rig}) then garden.step off the two
|
|
* exposure terms, storm phase only. `held` false = never rig at all (bare bed).
|
|
*/
|
|
function gardenFor(ids, hwTier, { bare = false } = {}) {
|
|
const wind = createWind(stormDef);
|
|
wind.setVenturi(venturi);
|
|
wind.setSheltersFromTrees(anchors.filter((a) => a.type === 'tree'));
|
|
|
|
let rig = null;
|
|
if (!bare) {
|
|
rig = new SailRig({ anchors, gridN: 10, porosity: 0.30 })
|
|
.attach(ids, Array(4).fill(hwTier), 1.0);
|
|
}
|
|
const sky = createSkyFx({ wind, night: true });
|
|
let hp = 100, byHail = 0, byRain = 0;
|
|
|
|
const n = Math.round(stormDef.duration / FIXED_DT);
|
|
for (let i = 0; i < n; i++) {
|
|
const t = i * FIXED_DT;
|
|
if (rig) rig.step(FIXED_DT, wind, t);
|
|
sky.step(FIXED_DT, t, { sail: rig });
|
|
const rainExp = sky.gardenExposure(bed, t);
|
|
const hailExp = sky.gardenHailExposure(bed, t);
|
|
const dHail = HAIL_WEIGHT * hailExp * GARDEN_DRAIN * FIXED_DT;
|
|
const dRain = RAIN_WEIGHT * rainExp * GARDEN_DRAIN * FIXED_DT;
|
|
const total = Math.min(hp, dHail + dRain);
|
|
if (total > 0) {
|
|
const fh = dHail / (dHail + dRain);
|
|
byHail += total * fh; byRain += total * (1 - fh);
|
|
hp -= total;
|
|
}
|
|
}
|
|
const cornersLost = rig ? rig.corners.filter((c) => c.failed).length : 4;
|
|
return { hp: +hp.toFixed(1), byHail: +byHail.toFixed(1), byRain: +byRain.toFixed(1), cornersLost };
|
|
}
|
|
|
|
el('sub').textContent =
|
|
`${site.name} · ${stormName} (${stormDef.duration}s) · bed ${bed.w}x${bed.d} @ (${bed.x},${bed.z})\n` +
|
|
`flying the SIM's garden loop (skyfx gardenExposure + gardenHailExposure, main.js weights) per line`;
|
|
|
|
// The bare bed is the control: what the garden does with NO sail at all.
|
|
const bareRes = gardenFor(null, null, { bare: true });
|
|
|
|
// Probe a spread of lines: the cheapest (the decoy), the best-covering, and a
|
|
// few between — sorted by the audit's cover% so any correlation is visible.
|
|
const holdable = rows.filter((r) => !r.unholdable.length);
|
|
const byCover = [...holdable].sort((a, b) => a.cover - b.cover);
|
|
const pick = [];
|
|
const want = 8;
|
|
for (let i = 0; i < want; i++) pick.push(byCover[Math.floor(i * (byCover.length - 1) / (want - 1))]);
|
|
const seen = new Set();
|
|
const probes = pick.filter((r) => r && !seen.has(r.ids.join(',')) && seen.add(r.ids.join(',')));
|
|
|
|
const tbl = document.createElement('table');
|
|
const head = tbl.insertRow();
|
|
for (const h of ['line', 'audit cover%', 'hw $', 'garden HP', 'by hail', 'by rain', 'corners lost', 'vs bare bed'])
|
|
{ const th = document.createElement('th'); th.textContent = h; head.appendChild(th); }
|
|
|
|
const results = [];
|
|
for (const r of probes) {
|
|
const g = gardenFor(r.ids, HARDWARE[2]); // rated shackles: hold it, isolate GEOMETRY
|
|
results.push({ ids: r.ids.join(','), cover: +(r.cover * 100).toFixed(0), hw: r.hw, ...g });
|
|
const tr = tbl.insertRow();
|
|
const cells = [r.ids.join(','), `${(r.cover * 100).toFixed(0)}%`, `$${r.hw}`,
|
|
g.hp.toFixed(1), g.byHail.toFixed(1), g.byRain.toFixed(1), String(g.cornersLost),
|
|
`${(g.hp - bareRes.hp >= 0 ? '+' : '')}${(g.hp - bareRes.hp).toFixed(1)}`];
|
|
cells.forEach((c, i) => { const td = tr.insertCell(); td.textContent = c;
|
|
if (i === 7) td.className = (g.hp - bareRes.hp) > 15 ? 'ok' : (g.hp - bareRes.hp) > 5 ? 'warn' : 'bad'; });
|
|
}
|
|
const tr = tbl.insertRow();
|
|
['BARE BED (no sail)', '—', '$0', bareRes.hp.toFixed(1), bareRes.byHail.toFixed(1), bareRes.byRain.toFixed(1), '—', '—']
|
|
.forEach((c) => { const td = tr.insertCell(); td.textContent = c; td.className = 'warn'; });
|
|
el('out').appendChild(tbl);
|
|
|
|
// Correlation between the audit's cover% and the sim's garden HP. If the audit
|
|
// is telling the truth this is near 1; the QA pass says it is not.
|
|
const xs = results.map((r) => r.cover), ys = results.map((r) => r.hp);
|
|
const mean = (a) => a.reduce((s, v) => s + v, 0) / a.length;
|
|
const mx = mean(xs), my = mean(ys);
|
|
const cov = xs.reduce((s, x, i) => s + (x - mx) * (ys[i] - my), 0);
|
|
const sx = Math.sqrt(xs.reduce((s, x) => s + (x - mx) ** 2, 0));
|
|
const sy = Math.sqrt(ys.reduce((s, y) => s + (y - my) ** 2, 0));
|
|
const r2 = (sx && sy) ? (cov / (sx * sy)) : NaN;
|
|
const spread = Math.max(...ys) - Math.min(...ys);
|
|
const bestGain = Math.max(...ys) - bareRes.hp;
|
|
|
|
el('note').textContent =
|
|
`cover% → garden HP correlation: r = ${r2.toFixed(3)}\n` +
|
|
`garden HP spread across every holdable line: ${spread.toFixed(1)} HP\n` +
|
|
`best line vs bare bed: ${bestGain >= 0 ? '+' : ''}${bestGain.toFixed(1)} HP ` +
|
|
`(a rig that costs money and holds every corner buys THIS much garden)\n` +
|
|
`bare bed finished at ${bareRes.hp.toFixed(1)} HP — hail ${bareRes.byHail.toFixed(1)}, rain ${bareRes.byRain.toFixed(1)}`;
|
|
|
|
window.__probe = { site: siteName, storm: stormName, bare: bareRes, results, r: r2, spread, bestGain };
|
|
document.title = `garden_probe — r=${r2.toFixed(2)}`;
|
|
}
|
|
|
|
run().catch((e) => {
|
|
el('note').className = 'bad';
|
|
el('note').textContent = `garden_probe crashed: ${e.message}\n${e.stack || ''}`;
|
|
window.__probe = { error: e.message };
|
|
document.title = 'garden_probe — ERROR';
|
|
});
|
|
</script>
|