Lane B S14: audit.html onto the shared scorecard engine — one copy of the run logic
The page keeps its rendering and loses its logic. A third front-end (A's editor) was about to make sweep-fly-pick-judge exist twice, which is the exact failure sweep.js was extracted to prevent, one level up. Behaviour-preserving, and measured that way rather than reasoned: both shipped sites were pinned BEFORE the refactor and reproduce to the digit after — backyard_01/wildnight 16 cands, 3 clean, 2 marginal, bare 35.7 tattered, best p1,p2,p3,p4 60.5, sep MEETS held 63.8; site_02/earlybuster 66 cands, 20 clean, 1 marginal, bare 82.6 full, best tr1,tr1b,q1,q4 91.3 full. Caught in the act by that check: __audit.cands went undefined because scoreSite returns the COUNT where the old local was the LIST. A refactor that only got read would have shipped it.
This commit is contained in:
parent
6825794d98
commit
af0d64ffe3
@ -25,6 +25,19 @@
|
|||||||
tools/site_audit/audit.html?site=site_02_corner_block&storm=storm_03b_earlybuster
|
tools/site_audit/audit.html?site=site_02_corner_block&storm=storm_03b_earlybuster
|
||||||
|
|
||||||
Served from the repo root (server.py), so the relative imports resolve.
|
Served from the repo root (server.py), so the relative imports resolve.
|
||||||
|
|
||||||
|
SPRINT14: this page's RUN LOGIC moved to scorecard.js and it kept only its
|
||||||
|
rendering. A third front-end arrived (A's editor, gate 2.1, where the yard
|
||||||
|
being scored has never been a file), and the sweep-fly-pick-judge sequence
|
||||||
|
this page had grown was about to exist in two places — which is the exact
|
||||||
|
failure sweep.js was extracted to prevent, one level up. `scoreSite()` takes
|
||||||
|
a site OBJECT; fetching one is this page's business and nobody else's.
|
||||||
|
|
||||||
|
Verified by measurement, not by reading: the refactor was pinned against
|
||||||
|
both shipped sites BEFORE it happened and reproduced them to the digit —
|
||||||
|
backyard_01/wildnight (16 cands, 3 clean, 2 marginal, bare 35.7 tattered,
|
||||||
|
best p1,p2,p3,p4 60.5, sep MEETS held 63.8) and site_02/earlybuster (66
|
||||||
|
cands, 20 clean, 1 marginal, bare 82.6 full, best tr1,tr1b,q1,q4 91.3).
|
||||||
-->
|
-->
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>site_audit</title>
|
<title>site_audit</title>
|
||||||
@ -51,11 +64,10 @@
|
|||||||
} }
|
} }
|
||||||
</script>
|
</script>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import * as THREE from '../../web/world/vendor/three.module.js';
|
import { loadSite } from '../../web/world/js/world.js';
|
||||||
import { createWorld, loadSite } from '../../web/world/js/world.js';
|
|
||||||
import { HARDWARE, START_BUDGET } from '../../web/world/js/contracts.js';
|
import { HARDWARE, START_BUDGET } from '../../web/world/js/contracts.js';
|
||||||
import { AUDIT, auditSweep } from './sweep.js';
|
import { AUDIT } from './sweep.js';
|
||||||
import { flyGarden, flySeparation } from './gardenfly.js';
|
import { scoreSite, FLY_CAP } from './scorecard.js';
|
||||||
|
|
||||||
const q = new URLSearchParams(location.search);
|
const q = new URLSearchParams(location.search);
|
||||||
const siteName = q.get('site') || 'backyard_01';
|
const siteName = q.get('site') || 'backyard_01';
|
||||||
@ -65,41 +77,30 @@ const el = (id) => document.getElementById(id);
|
|||||||
const loadJSON = async (path) => (await fetch(path)).json();
|
const loadJSON = async (path) => (await fetch(path)).json();
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
// Build the site the way the game does — data in, dressed yard out — on a
|
// Fetching the site is this page's job; scoring it is scorecard.js's. The
|
||||||
// RE-POINTABLE wind proxy (C's bench pattern), so the audit can fly
|
// whole "build a dressed world on a re-pointable wind proxy so live tree-sway
|
||||||
// world.anchors THEMSELVES. The old version here remapped every anchor to a
|
// closures sample the storm being flown" dance lives in buildScoringWorld()
|
||||||
// static `sway: () => pos`, which froze the gum tree — the same landmine
|
// now — including the reason it must (a static `sway: () => pos` remap froze
|
||||||
// that made C's bench under-read q4 by 0.22 kN on the $80 line (a tree rig
|
// the gum tree and under-read every tree corner: C's landmine 2).
|
||||||
// eats the tree's sway as dynamic load). Backyard posts never noticed;
|
|
||||||
// every tr1/t1/t2 line was optimistic.
|
|
||||||
const site = await loadSite(siteName);
|
const site = await loadSite(siteName);
|
||||||
const scene = new THREE.Scene();
|
|
||||||
let currentWind = {
|
|
||||||
sample: (p, t, o) => (o || new THREE.Vector3()).set(0, 0, 4),
|
|
||||||
speedAt: () => 4, rainAt: () => 0, rainMmPerHour: () => 0,
|
|
||||||
gustTelegraph: () => null, eventsBetween: () => [],
|
|
||||||
};
|
|
||||||
const windProxy = {
|
|
||||||
sample: (p, t, o) => currentWind.sample(p, t, o || new THREE.Vector3()),
|
|
||||||
speedAt: (p, t) => currentWind.speedAt(p, t),
|
|
||||||
rainAt: (t) => currentWind.rainAt(t),
|
|
||||||
rainMmPerHour: (t) => (currentWind.rainMmPerHour ? currentWind.rainMmPerHour(t) : 0),
|
|
||||||
gustTelegraph: (t) => currentWind.gustTelegraph(t),
|
|
||||||
eventsBetween: (a, b) => currentWind.eventsBetween(a, b),
|
|
||||||
setSheltersFromTrees() {},
|
|
||||||
};
|
|
||||||
const use = (w) => { currentWind = w; };
|
|
||||||
const world = createWorld(scene, { wind: windProxy, site });
|
|
||||||
let dressed = false;
|
|
||||||
if (world.dress) { try { await world.dress(); dressed = true; } catch (e) { /* fall through, flagged below */ } }
|
|
||||||
|
|
||||||
// world.anchors, LIVE — sway closures and ratingHint intact. The sweep and
|
|
||||||
// every garden flight below re-point the proxy at their own wind via `use`.
|
|
||||||
const anchors = world.anchors;
|
|
||||||
|
|
||||||
const stormDef = await loadJSON(`../../web/world/data/storms/${stormName}.json`);
|
const stormDef = await loadJSON(`../../web/world/data/storms/${stormName}.json`);
|
||||||
|
|
||||||
const vlist = site.wind?.venturi ?? [];
|
// The separation block names its own storm; load it if it differs.
|
||||||
|
const sepKey = site.separation?.stormKey ?? null;
|
||||||
|
const sepStormDef = sepKey
|
||||||
|
? (sepKey === stormName ? stormDef : await loadJSON(`../../web/world/data/storms/${sepKey}.json`))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const score = await scoreSite({ site, stormDef, stormName, sepStormDef });
|
||||||
|
const {
|
||||||
|
dressed, cands, rows, verdict, winners, marginalWinners,
|
||||||
|
flown, skipped, bare, separation: sep, sepStormName,
|
||||||
|
} = score;
|
||||||
|
const anchors = { length: score.anchorCount };
|
||||||
|
const bestGarden = score.bestGarden ? { key: score.bestGarden.ids, g: score.bestGarden } : null;
|
||||||
|
const bestMarginal = score.bestMarginal ? { key: score.bestMarginal.ids, g: score.bestMarginal } : null;
|
||||||
|
|
||||||
|
const vlist = score.venturi;
|
||||||
const vtxt = vlist.length
|
const vtxt = vlist.length
|
||||||
? vlist.map((v) => `(${v.x},${v.z}) axis ${v.axis} gain ${v.gain}`).join(' · ')
|
? vlist.map((v) => `(${v.x},${v.z}) axis ${v.axis} gain ${v.gain}`).join(' · ')
|
||||||
: 'none';
|
: 'none';
|
||||||
@ -110,55 +111,9 @@ async function run() {
|
|||||||
`venturi: ${vtxt}\n` +
|
`venturi: ${vtxt}\n` +
|
||||||
`shop: $${START_BUDGET} · ${HARDWARE.map((h) => `${h.name} $${h.cost}/${(h.rating / 1000).toFixed(1)}kN`).join(' · ')}`;
|
`shop: $${START_BUDGET} · ${HARDWARE.map((h) => `${h.name} $${h.cost}/${(h.rating / 1000).toFixed(1)}kN`).join(' · ')}`;
|
||||||
|
|
||||||
// The venturi rides in on the SITE def — windForSite (C's shared builder,
|
// `rowBy` is the only helper the RENDER still needs (the picking it used to
|
||||||
// inside sweep.js and gardenfly.js) is the ONE door site wind comes through
|
// serve moved into scoreSite).
|
||||||
// now. Three harnesses independently mis-built it; there is no fourth copy.
|
|
||||||
const bed = world.gardenBed;
|
|
||||||
const { cands, rows, verdict, winners, marginalWinners } =
|
|
||||||
auditSweep({ anchors, bed, stormDef, siteDef: site, use });
|
|
||||||
|
|
||||||
// ── SPRINT13: fly the garden for every line the budget can actually buy ──
|
|
||||||
// The scoring quantity is the FLOWN outcome (gardenfly.js — real attach,
|
|
||||||
// real skyfx exposure, real garden.js), not static cover%. Marginal lines
|
|
||||||
// fly too, deliberately: they are the trap the margin rule exists to name.
|
|
||||||
const bare = flyGarden({ anchors, bed, stormDef, siteDef: site, use });
|
|
||||||
const FLY_CAP = 12;
|
|
||||||
const flown = new Map();
|
|
||||||
for (const r of [...winners, ...marginalWinners].slice(0, FLY_CAP)) {
|
|
||||||
// clean winners fly the CLEAN tiers (what the audit recommends buying);
|
|
||||||
// marginal winners fly the knife-edge tiers (the trap, priced as sold).
|
|
||||||
const tierBy = new Map(r.tiers.map((c) => [c.id, r.clean ? c.cleanTier : c.tier])); // ring-ordered; align by anchorId
|
|
||||||
flown.set(r.ids.join(','), flyGarden({
|
|
||||||
anchors, bed, stormDef, siteDef: site, use,
|
|
||||||
ids: r.ids, hw: r.ids.map((id) => tierBy.get(id)),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
const skipped = Math.max(0, winners.length + marginalWinners.length - FLY_CAP);
|
|
||||||
|
|
||||||
// best GARDEN line the budget buys — the audit's answer to "what should I
|
|
||||||
// rig", which used to be "the cheapest line that holds" and is now "the line
|
|
||||||
// that saves the garden" — cheapest on ties, and never a marginal one (a
|
|
||||||
// marginal flight is C's 91.9-FULL illusion; it gets reported, not sold).
|
|
||||||
const rowBy = (key) => rows.find((w) => w.ids.join(',') === key);
|
const rowBy = (key) => rows.find((w) => w.ids.join(',') === key);
|
||||||
const pickBest = (entries) => entries.reduce((best, [key, g]) => {
|
|
||||||
if (!best) return { key, g };
|
|
||||||
if (g.hp > best.g.hp + 0.05) return { key, g };
|
|
||||||
if (Math.abs(g.hp - best.g.hp) <= 0.05 && (rowBy(key)?.hw ?? 1e9) < (rowBy(best.key)?.hw ?? 1e9)) return { key, g };
|
|
||||||
return best;
|
|
||||||
}, null);
|
|
||||||
const cleanFlown = [...flown.entries()].filter(([k, g]) => !g.marginal.length && rowBy(k)?.clean);
|
|
||||||
const bestGarden = pickBest(cleanFlown);
|
|
||||||
const bestMarginal = pickBest([...flown.entries()].filter(([k]) => !rowBy(k)?.clean));
|
|
||||||
|
|
||||||
// ── the site's pinned separation target (A's gate-1.4 ruling), judged on the
|
|
||||||
// block's OWN storm — the target is site data, not a per-run choice.
|
|
||||||
let sep = null, sepStormName = null;
|
|
||||||
if (site.separation) {
|
|
||||||
sepStormName = site.separation.stormKey;
|
|
||||||
const sepStorm = sepStormName === stormName ? stormDef
|
|
||||||
: await loadJSON(`../../web/world/data/storms/${sepStormName}.json`);
|
|
||||||
sep = flySeparation({ anchors, bed, separation: site.separation, stormDef: sepStorm, siteDef: site, use });
|
|
||||||
}
|
|
||||||
|
|
||||||
// render rows — cover% is a DIAGNOSTIC column now (static overhead projection
|
// render rows — cover% is a DIAGNOSTIC column now (static overhead projection
|
||||||
// of the taut attach shape); the flown garden column is what scores.
|
// of the taut attach shape); the flown garden column is what scores.
|
||||||
@ -261,7 +216,7 @@ async function run() {
|
|||||||
// a machine-readable line, so this page can also be driven headless-in-browser
|
// a machine-readable line, so this page can also be driven headless-in-browser
|
||||||
window.__audit = {
|
window.__audit = {
|
||||||
site: siteName, storm: stormName, dressed, venturi: vlist, anchors: anchors.length,
|
site: siteName, storm: stormName, dressed, venturi: vlist, anchors: anchors.length,
|
||||||
cands: cands.length, verdict,
|
cands, verdict, // scoreSite already returns the COUNT, not the list
|
||||||
winners: winners.map((w) => ({ ids: w.ids, hw: w.hw, cover: w.cover, garden: flown.get(w.ids.join(',')) ?? null })),
|
winners: winners.map((w) => ({ ids: w.ids, hw: w.hw, cover: w.cover, garden: flown.get(w.ids.join(',')) ?? null })),
|
||||||
marginalWinners: marginalWinners.map((w) => ({ ids: w.ids, hw: w.hw,
|
marginalWinners: marginalWinners.map((w) => ({ ids: w.ids, hw: w.hw,
|
||||||
marginal: w.marginal.map((c) => ({ id: c.id, headroom: c.headroom })),
|
marginal: w.marginal.map((c) => ({ id: c.id, headroom: c.headroom })),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user