editor_storm.js owns THE storm selection and THE storm list; SCORE IT's
dropdown is a view of it (default = southerly, C's old default, kept on
purpose). The score panel renders scoreSite's progress ticks — flight x/y,
elapsed — and audit.html gets the same line. C's half: subscribe
editor.wind.js per the THREADS proposal.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
The scoring quantity is the FLOWN garden state now, not static cover%.
New gardenfly.js: windForSite (C's shared builder — venturi + shelters, one
copy in the repo) → real attach (throws on D's skipped-attach trap) → skyfx
exposure → garden.js, per candidate line; cover% demoted to a labelled
geometry diagnostic. audit.html flies every affordable line + bare bed and
judges the site's pinned separation block on the block's own storm; audit.mjs
(node) keeps fast winnability, gains p5 in its verified dump, and points at
the browser for garden truth.
On C's correction, three structural adoptions:
- windForSite everywhere (three harnesses independently mis-built site wind;
no fourth copy);
- LIVE anchors via the re-pointable wind proxy — the audit's own frozen-sway
remap was C's landmine 2 wearing my file's name (audit.html:69);
- the MARGIN rule (AUDIT.MARGIN = 0.15, one copy): every row prices twice —
$hw holds vs $cleanHw with >=15% headroom — and only clean lines are
winners; verdict code 'marginal-only' when the budget can only buy the
knife edge (D's 39.8-TATTERED wild night, C's dead 91.9 headline).
Also game-true flight: the phantom 12s calm settle is GONE from the sweep
(commit->attach->storm is one keypress; the settle skewed every storm sample
12 s off the authored curve — SailRig samples wind at its INTERNAL clock).
That skew is not academic: it is a.test's separation-flight harness too, and
gardenfly.selftest now pins BOTH chains (game-true 63.8 tattered vs skewed
68.4 'full' on the pinned p5 line — the disagreement is flagged to A in
THREADS, not overruled here). The pinned recipe also sweeps regardless of the
18-45 band (it is 45.9 m² — the band predates p5; flagged to A).
Selftest 356/0/0 on the scratch merge (b+a+c@45bdc2d). Mutation-checked:
MARGIN=0 reddens the margin test (node); dropping siteDef from gardenfly's
windForSite call reddens the venturi test (browser, exact test named).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
site_audit's headless false-negative is fixed by the browser front-end
(A's loadSite/createWorld landed in main — that was all it waited on), and
fixing it surfaced a worse bug underneath: the sweep built its wind from the
STORM def alone and never called setVenturi, which main.js:424 does at every
site load. The venturi is SITE data, so every site_02 audit ever run — including
the numbers quoted at C in SPRINT10 — was of a corner block with no gap. The
SPRINT6 p1=7.4kN failure inverted: there the tool called a good site unriggable,
here it called a mean site cheap.
- sweep.js: setVenturi on both winds (the gap doesn't switch off for the settle);
both front-ends pass the site's funnel and print it, so a funnelled run is
legible as one.
- sweep.selftest.js (new, wired into b.test.js): the auditor gets an auditor.
Funnel must strictly raise every corner; a funnel-less site must be
byte-identical. Verified it fails when the setVenturi calls are deleted.
- rigging.js: setWorld(world) + session.setBudget(n)/setAnchors() — A's two asks.
Markers are rebuilt (the anchor set changes across sites) and disposed; stale
picks die with the old yard. Deletes A's "one private touch" into this module.
- dev_rigging.html: dead since SPRINT10 (called createWorld without a site, which
now throws) and nothing noticed, because a dev page has no selftest to go red.
Fixed, dressed, and given the site switch it should have had (N = next yard).
Verdict for C/A, in THREADS: site_02 PASSES with the funnel on — 64 of 66 lines
affordable, cheapest $20. The funnel kills nothing, so the gain does NOT need
dropping and E's tree stays put. It's a reach problem, not a gain problem: the
throat's radius is 5 m and the bed centre is 6.08 m away, so the gap screams over
the carport at +50% and reaches the garden at +0.0%.
selftest 300/0/0 (296 + 4 new).
Gate 2's actual deliverable. The node tool cannot dress, so it can only audit
the built-in snapshot or a resolved export and REFUSES A's dress-source site
JSON. This is the honest audit: audit.html builds the site the way the game does
— createWorld(await loadSite(name)) then dress() — and reads world.anchors, the
single source of DRESSED positions (GLB fascia + branch anchors, 8°-raked posts,
all of it), then runs the SAME sweep. It is the only way to audit a site with a
carport (site_02) before it ships.
tools/site_audit/audit.html?site=backyard_01
tools/site_audit/audit.html?site=site_02_corner_block&storm=storm_03_southerly
The winnability math now lives in ONE place, sweep.js, imported by both
front-ends — a tool built to catch reimplemented-formula drift doesn't get to
keep two copies of its own sweep. audit.mjs is refactored onto it (output
byte-identical); the node refusal message now hands you the exact audit.html URL.
Validated in-browser against backyard_01: dressed 12/12 anchors (GLBs loaded),
PASS, best line p1,p2,p3,p4 $65+$15 spare — identical to the node snapshot
verdict and to balance.test, off A's real data with no snapshot. That
cross-agreement also re-proves A's extraction is byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>