GATE 0.3 — THE CHARTER, SAID OUT LOUD (D's soaker finding, A consulted on
wording and their every-card rule adopted). The audit always flew shade cloth
and never said so; on the soaker that silence read 'WINNABLE over an all-DEAD
garden — the night is broken'. Now: AUDIT.POROSITY is the one charter knob
(sweep.js + gardenfly.js both read it), scorecard.fabricCharter() names the
fabric and whether tonight's stones pass its weave (same hailBlockFor the sim
charges), and the SCORE IT header prints 'fabric · shade cloth (sweep
charter)' on EVERY card — plus, on leaky-storm nights, A's wording: the
fabric is the bet, and the audit does not place it.
GATE 2 — CLIENT CONSTRAINTS: A's shapes (seam contract, built against as
posted), this lane's teeth.
· rigging.js: validateNightConstraint (checked enum, both ends of the seam),
setConstraints (survives reset like _startBudget; [] is the only clear).
The house ban refuses the pick with the CLIENT'S WORDS as the ticker
reason; the cap refuses at SPEND time on every path (rig / cycle / set /
spares / fabric), exact at the boundary, refunds always free — and commit
is belt-and-braces: a caller who walked around the doors gets a THROW.
The cap covers the whole invoice, spares included — the client queries the
invoice, not the hardware line.
· sweep.js: applyNightConstraints, ONE function both drivers share — bans
shrink the candidate list (a card that recommends a forbidden line is the
card lying), the cap re-prices affordable/clean without touching a load.
· scoreSite carries constraints/budget/constrainedOut; the card prints the
client's terms next to funnel and fabric, and tells 'no geometry' from
'client forbids all of it'.
· spent getter now measures against _startBudget (the re-banked session was
measuring spend against the default bank).
Selftest 485/0/0 in the browser (474 + exactly the 11 added: 1 sail, 4
session, 2 charter, 4 audit-constraints). Wiring line for A filed in THREADS:
session.setConstraints(week.job?.constraints ?? []) at the night boundary.
findCandidates/priceCandidate/judgeSweep extracted so the sync auditSweep and
the new auditSweepAsync are ONE copy of the math; scoreSite drives the async
path with onProgress ticks (sweep/fly/separation phases). Yield is a
MessageChannel task, not setTimeout — Chrome's intensive timer throttling
turned an occluded run into one flight per minute, measured. Three new
asserts, mutation-checked red-then-green in one sitting (impure chunk + a
swallowed tick = all three red with the intended diagnostics).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>