A NaN corner load compares false to every rating, so a diverged corner
held forever while NaN spread node-to-node — watchDivergence only ever
guarded the tests. Non-finite load now breaks the corner instantly with
reason:'divergence', load zeroed for the HUD.
The pond per-node cap only ran on the rain-add: once rain stopped,
consolidation could stack a basin node past POND_MAX_KG_M2 unbounded.
Clamp moved post-flow, every step. Also fixes the dumpPond assert that
compared dumped to itself through a helper that returned its argument.
All three mutation-checked red before landing green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
C measured my proposed pin and it could not have failed. Converging rather than
landing a second pin that disagrees. Knobs are now C's:
site_02_corner_block / storm_02_wildnight / throat (-6,y,0) / t=60.0 / exact ===
Three wrong answers before this, all the same failure:
1. backyard_01 has venturi: [] — nothing to detect. (I found this one.)
2. site_02's BED is funnel-blind, Δ 0.0000 m/s. (Both of us, independently.)
3. t=30 on the wildnight is worth 0.44%. I MISSED THIS ONE: my throat
measurement read a healthy Δ +4.948 m/s and I trusted it, but that was on
storm_03b_earlybuster, not the wildnight the pin flies. A sensitivity
measured on one storm says nothing about another. The mutation check I was
proud of would have passed on the wrong storm.
Adopted C's vacuity guard and its 25% threshold — one number, not two. It is
the check that would have caught my proposal on its own: mutation asks 'did it
move', the guard asks 'is the funnel worth enough here that a wiring bug could
not hide in the rounding'.
Game side now imports main.js's createWindRouter instead of retyping its two
wiring lines (C's correction). My old version built BOTH sides with
windForSite, so it could only have caught a bad input, never a router that
routed wrong.
CONVERGENCE RECEIPT — my chain reads C's numbers to the decimal:
t=60 throat editor 47.38 game 47.38 exact === true funnel-off 31.60 = 33.3%
t=30 throat on 18.55 off 18.46 = 0.44% (C's finding, reproduced)
What my half adds over C's: C pins windForSite (the builder) against the game;
this pins buildScoringWorld — the path SCORE IT actually runs — against the
same router. Three chains, one number.
Kept the garden bed as a SECOND probe on C's explicit offer: what the sail
shades is what the audit is about, and equality there is a real claim even
though the funnel is not what decides it. Labelled as carrying no funnel
tripwire, because it cannot.
Mutation-checked: setting t back to 30 reds the guard and both tripwires with
the exact diagnostic (0.44%, 18.46 -> 18.55) while the EQUALITY asserts stay
green — which is precisely C's argument, reproduced on my own chain.
Selftest 375 passed / 0 failed / 0 skipped.
COLLATERAL ON THE CARD. 'Winnable at $80' is an incomplete sentence on a yard
where letting a corner go also bills $180 of carport, so the card now prices
it — through world.collateralFor(), the one resolver (A's structKey fix), never
a second copy of that lookup in a panel. Deduped per STRUCTURE the way main.js
bills ('two beams letting go is one carport gone'): site_02's four carport
anchors read $180 total, not $720. The bill also rides on the margin-flag line
itself, because 'knife-edge' and 'knife-edge AND $180' are different authoring
decisions. An unpriced label renders UNPRICED — not scored, in red, never
'free' — which is the gutter bug's shape and the lie the card exists to prevent.
Verified A's structKey fix rather than assuming it: an editor-shaped carport
(id s1 + collateralKey) prices to $180, a second one (s2) also $180, and the
negative control (id s1, no collateralKey) reads null = not scored. My first
test reported a false alarm because I renamed a SHIPPED spec's id without the
palette's collateralKey — A's editor writes that field explicitly, so the
editor path was never broken.
THE NIGHT-5 INVOICE LINE (pool item, offered S13, accepted). Night 5's garden
is beyond saving by design, and the job sheet was the last surface still
dangling 'up to $45 / paid on what's left of the bed at dawn' — a maximum
nobody can earn, phrased as a rigging problem the player could solve.
NO ECONOMY CHANGE, deliberately. week.js's own comment forbids quote and settle
diverging ('the job sheet promises a number the invoice doesn't pay'), and
zeroing the quote would break that in the opposite direction — settle really
does pay the bonus proportionally. So the NUMBER stays and its CONDITION tells
the truth, which is the job sheet's own idiom:
nights 1-4 'paid on what's left of the bed at dawn' (unchanged)
night 5 'scales with the bed at dawn — and tonight the ice takes it
whatever you rig'
and the ledger names it too, because an unexplained small number next to
'night lost' reads as the player's failure:
night 4 'garden bonus — 18% of the bed +$8'
night 5 'garden bonus — 18% of the bed, beyond saving from the start +$8'
Verified in the running game on all five nights: flag fires on 5 only, every
quoted and paid figure identical (garden $45, total $136, bonus $8).
Selftest 374 passed / 0 failed / 0 skipped.
The pin (co-owned with B, probe/second moved and argued in THREADS):
site_02_corner_block / storm_02_wildnight / the authored throat centre
(-6,0) / t=60.0 / exact ===. B proposed backyard_01, the garden bed and
t=30; all three are places the funnel contributes ~nothing (backyard_01's
venturi is [], the bed measures 0.0000 m/s of funnel even on site_02, and
t=30 is worth 0.4%), so that pin could not have failed. A vacuity guard
now asserts the funnel is worth >25% AT the pin, so it cannot rot back
into decoration.
Also fixed, found by looking: editor.wind rebuilt its wind in commit()
rather than on 'change', so any EXTERNAL mutation + markDirty() re-rendered
the panel against the previous site's wind. The readout kept insisting a
restored gap was 24% aligned. Refresh moved to the listener, so the overlay
is a function of the site as it is, whoever changed it.
368/0/0 (362 baseline + 6). All six mutation-checked in one run: dropping
setVenturi from windForSite reds the pin (31.60 vs 47.38 m/s), the guard
and the sweep; normalizeAxis as identity reds the fold; drifting the
shelterAtten mirror 0.5 -> 0.62 reds the volume assert.
Two chains built independently — GAME (loadSite -> createWorld -> dress ->
windForSite) and EDITOR (siteClone-shaped object -> buildScoringWorld ->
windForSite) — asserted EXACTLY equal at t=30, speed and vector, at every
probe on both shipped yards. Exactly, not nearly: a tolerance is where the
funnel-off bug hid, and it moved the S13 headline 91.5 -> 39.8.
The mutation checks caught my own probe TWICE before this went green, which is
the only reason it is worth anything:
1. I proposed backyard_01 in THREADS. It declares venturi: [] — no funnel to
lose, so a funnel-off regression could never turn it red.
2. So I moved to site_02 — and its BED is 6.08 m from a radius-5 throat.
Starving that yard of its venturi moved the bed probe by 0.000 m/s.
Both versions passed their equality assert and both were worthless.
Measured sensitivity on site_02/earlybuster at t=30 (funnel on/off, trees
present/hidden):
bed ( 0, 1) 12.452 dV +0.000 dT +0.000 <- blind, not a pin site
throat (-6, 0) 15.910 dV +4.948 dT +0.000 <- funnel tripwire
near tr1 ( 7,-1) 8.593 dV +0.000 dT -4.891 <- shelter tripwire
Mutation-checked: starving the editor clone of its venturi turns the @throat
equality red (plus both funnel asserts), while bed/tree stay green exactly as
the table predicts.
Also recorded as a pin, because it is load-bearing for anyone reading the
score card: the corner block's funnel does NOT reach its garden bed. It is
aimed at the rigging zone (the site JSON says so: q1 is 3.5 m inside the
radius), so on that yard the funnel is a hardware-load story, not a
garden-exposure one.
Selftest 374 passed / 0 failed / 0 skipped (362 baseline + 12).
Gate 1.5 -- the round trip. a.test drives the editor's OWN placement function
(placeEntry, exported for exactly this) through
template -> place -> export -> parse -> validateSite -> createWorld -> dress,
then asserts the GLB-backed anchors ADOPTED and the carport's 0.22/0.30 came
out the far side. Mutation-checked with five deliberate breakages; four went
red and ONE DID NOT -- the key-order test shuffled only root keys, every one
of which is in KEY_ORDER, so the alphabetical fallback it claimed to protect
was never reached. Rewritten around unknown keys, re-mutated, red in both
halves. A determinism test that passed while testing nothing.
Picking bug, found by dispatching real PointerEvents rather than reading the
code: clicking an object's FOOT selected nothing, because the pick handle is a
cylinder centred 1.7 m up and from a raised camera the ray has already run
~2 m horizontally by the time it climbs there. pickRef now falls back to
nearest-placed-thing within 1.3 m of the ground hit -- camera-angle
independent, so it can't rot when the pitch changes.
Phantom sail (pool, filed unlanded in S13) -- LANDED and negative-controlled.
Teardown factored into disposeSailView() and called from loadSiteInto before
refreshCameraSolids (which reads sailView and would re-register a disposed
mesh as a camera collider). Rig state goes with the view. Verified through the
real game both ways: with the fix removed the cloth still hangs over the
corner block, screenshotted.
Dev-line (pool) -- two findings, both landed. The line counted `pieces` only,
so it read "debris 0" while seven leaves flew; now reports both populations
off C's leafCount. And index.html's boot-failure handler wrote into #dev,
which is display:none off localhost -- so a failed boot on partly.party showed
a stranger a blank page. It has its own visible #fatal element now.
E's gap 2, runtime half -- and it was worse than "one click away": nextId
namespaces structures s1, s2..., so the FIRST carport the editor places was
already free. collateralFor/wreckStructure/isWrecked now resolve through
structKey (site collateralKey, then GLB collateral_key, then id last so every
existing site is untouched). Negative-controlled: id-equality prices an
editor carport to null. E's tie_off:false honoured at both ends. E's swing set
and jacaranda are in the palette behind a `requires` gate, so they switch
themselves on when the ANCHOR_TYPE widening merges.
Also: `return t.skip(msg)` is a fake pass -- Suite.skip pushes a new result and
lets the current test record as pass. a.test had six; all converted to the
documented 'SKIPPED --' idiom.
Selftest 373/0/0, no skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gate 2.2. Mounts into Lane A's editor through the published seams only —
panel at reserved order 50, every gizmo parented to EDITOR.overlay, venturi
drag through registerTool, site.wind mutated then markDirty(). No second wind
validator (validateSiteWind is already inside A's validateSite) and no private
harness: windForSite() is the only wind this file builds.
Gizmos are drawn from wind.core.venturi/shelters — what the wind actually
flies — not from the site JSON, so a mis-wired setVenturi shows up as a funnel
that vanishes rather than a number nobody reads.
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.
Dims, tri counts and node names all pass on a bike leaning the wrong way and
a gutter that never fell. The Blender contact sheet is close but renders
through Blender's view transform — it showed this sprint's jacaranda as
near-white lavender when the material is #8C7FC0, so a colour call made there
is a call about Blender, not about the game.
Smallest thing that answers the question: the game's own vendored three.js,
daylight, a ground plane, and the 1.7 m ref capsule beside whatever you name.
/tools/assetcheck/look.html?a=swing_set_01,tree_jacaranda_01&cam=10,4.5,22
It caught two things this sprint the numbers could not: the lilac reads lilac
in the real renderer (the thumb was lying), and the swing set's wreck lands on
+Z with a 2.9 m reach against a 0.95 m standing footprint — which is now baked
as a placement extra and asserted.
Sized at draw time, every time: a tab that boots hidden reports 0x0 and you
get a correct scene rendered into nothing. That cost me the first screenshot
of this very page, so it is in the MANUAL gotcha list where it already was.
SPRINT14 gate 3.1. The editor is about to offer these GLBs to an author by
name, which turns every silent field in them into a trap for whoever places
one. Audited the placeable set, fixed the gaps in the factory (nothing
hand-edited), and added the two props D needs to author a yard worth playing.
THE AUDIT, and what it found:
- Four `*_anchor` nodes carried no rating_hint at all — door_anchor,
pickup_anchor, grip_anchor, window_light_anchor. adoptAnchor does
`rating_hint ?? 1`, so silence there does not mean "unrated", it means
RATED PERFECT: a shed door skin outranking a concreted post, conjured out
of a missing field. They now deny it explicitly (`tie_off: false`).
- The carport's price resolved only by coincidence: collateralFor() matches a
STRUCTURE whose site id equals the anchors' collateral string, and site_02
happens to name its structure "carport". The editor will generate
"carport_2" for the second one, and the trap would have gone free — the
gutter bug reborn in the sprint meant to bury it. The GLB now carries
`collateral_key`, like the house does for "gutter". Lane A: the runtime
half is yours (collateralFor could fall back to it).
- The trampoline is unpriced, and now says so rather than omitting it.
Three new rules make the audit permanent, and each is proven able to fail:
no silent anchors, no collateral string nobody prices, no baked anchor_type
outside the checked ANCHOR_TYPE enum. Rule 2 found the carport gap on its
first run — an unfaked negative control.
THE PROPS:
- swing_set_01 (+ wrecked) — the palette's honest middle. It had a ceiling
(gum fork 1.0) and a trap (carport beam 0.22) and nothing in between. Two
apex anchors at 0.45, typed `swing_frame` (NOT `post`: the type string is
what the player reads before committing, and "post" promises concrete),
and a crossbar that is the most anchor-looking object in the game and is
not one. Priced at $140 — proposal, reasoning baked beside it, A rules the
number: gnome 25 < gutter 90 < swing 140 < carport 180.
- tree_jacaranda_01 — a second species whose LADDER is the feature.
Gum: 1.00/0.88/0.76, climb freely, pay 24%.
Jacaranda: 0.95/0.52/0.40, climb at all, pay 58%.
That makes tree choice a real decision instead of "is there a tree".
Unpriced by ruling, same as the bike: no limb-failure event exists to see.
Both wreck/orientation claims are measured in browser coords, not reasoned —
`wreck_falls_toward: "+Z"` is baked AND asserted against the mesh.
Selftest 376/0/0. Two consecutive full factory runs: 44 files byte-identical.
scorecard.js: the audit run-logic that audit.html had grown moves into one
shared engine both front-ends import (build the scoring world, sweep, fly the
winners, pick the best line, judge the separation block). Takes a site OBJECT,
not a path — loadSite always returned an object, so the fetch was never the
audit's business. Builds its OWN dressed world on a re-pointable wind proxy so
the editor's calm stub can never reach a score; every m/s still comes from
windForSite() via sweep.js/gardenfly.js.
editor_score.js: front-end only, zero audit logic. Mounts at A's reserved
order 40, renders the card with A's contract classes, says the funnel state
first and loudly, and marks itself stale when the yard changes underneath it.
web/world/editor.html + js/editor.js. Loads any existing site or an empty
template and renders the REAL dressed world through the same
loadSite/createWorld/dress path the game boots -- not a diagram, and no
second renderer to keep in sync. Place/drag/delete on the ground plane by
raycast, writing the SAME named-key site-JSON shapes world.js already reads
(the gnome pattern; there is deliberately no generic props[] and the editor
does not invent one). Live validateSite into a panel, canonical
stable-key-order export with a loud _INVALID key, visible _design/_why
authoring fields, and an export panel that says export != shipped.
Published the DOM seam contract in editor.html's header so B's SCORE IT and
C's wind authoring can start against it immediately -- mountPanel + a class
kit, the way E's .letterhead kit worked in Sprint 12.
Two rendering bugs in world.js that the editor surfaced in its first ten
minutes, both verified by looking at the yard rather than by reasoning:
- the graybox house was built unconditionally and dress() only retires it
when a house GLB loads, so site_02_corner_block -- which declares no house
-- has been shipping a 16x3x6 m grey slab across night 3's whole north
horizon, in solids, on the public URL.
- SHED alone was bound raw while SHED_TABLE/GNOME/BIKE all get their degrees
converted, so SHED.rotY was undefined, the shed's quaternion was all NaN,
and three.js silently declines to draw a NaN world matrix. The Hendersons'
shed has never rendered; ladder.js has been leaning the ladder on thin air.
Also: createWorld threw on a site with no shedTable (unreachable for both
shipped sites, reached by the editor template on its first frame). Now null,
which every consumer already guards and which CONTRACT.world permits.
Selftest 362/0/0. Asserts for the two fixes land with the round-trip test in
the next commit -- pushing now because B and C are blocked on the seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Played night 4 on the pinned line through the real UI (D's protocol):
hp 55.7 WIN, 2/4 lost, pyrrhic verdict — the ~8 gap to the sim chain is the
t=38 crate + ponding landing on p2's 4% margin. Ruled deliberate: the wild
night sells no clean line; the pyrrhic win is the intended reading.
- Site block records _playedReference; a.test pin re-labeled as the
deterministic sim-chain tripwire whose margin is what keeps the PLAYED
night over the win line. Band 18-45 ruled an availability floor (B's 1c).
- THREADS: full second ruling, night-5 canon, Sprint-14 notes.
- week.js night 5 carries gardenBeyondSaving: true with the measured case
(bare 0.0, best buyable 27.7 vs WIN>=50 — B's game-true table, my probe
agreeing) and the arc argument; nightAt defaults the flag FALSE so new
nights can never inherit the excuse. Brief levels with the player up front.
- verdictFor honours it on a LOSS only: no more 'the hail fell where your sail
wasn't' on a night where no placement wins; a 4/4 hold is credited as the
whole job, a torn sail is still named and owned. A win ignores the flag (a
stale flag must never eat a real win). Asserted both ways + mutation notes.
- GARDEN_DRAIN untouched, per the standing ruling; icenight hail budget also
untouched — the loss is the design, so the surfaces now say so.
- a.test's separation flight had the phantom 12 s settle B caught (SailRig
samples wind at its INTERNAL clock; the storm flew 12 s off the authored
curve). Settle deleted — commit->attach->storm from t=0, C's corrected bench
pattern; venturi wired from site data so the flight can never assume a yard.
- Game-true the line reads 63.6, not 68.3 — FULL (>66) was met by the harness,
not the game, and the re-sweep says nothing buyable reaches 66 (the twin-post
covering quad tears at 6.9 kN). Target RESTATED to the game's real stakes,
with headroom: held >60 and 0/4 (win line 50; measured 63.6), bare <50
(35.7), separation >=25 (27.9). Field names kept — B's gardenfly reads them.
- DESIGN CANON in the site block: the wild night's garden is WON SCARRED,
never FULL; the week's arc is full/full/full, won-tattered, beyond-saving.
- p5 _why table corrected to game-true numbers (first sweep ran ~4 HP hot).
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>
garden_probe.html drops its TEMPORARY COPY of HAIL_WEIGHT/RAIN_WEIGHT/
GARDEN_DRAIN and flies main.js's own createGarden (imported from garden.js),
so the probe's hp/state/damage-split are the model's, not a re-derivation.
balance.test.js's local GARDEN_DRAIN/W_HAIL/W_RAIN copy re-points at the
garden.js exports (A's flag on balance.test.js:38) — a future retune now
moves this suite the day it lands instead of drifting silently.
Selftest 351/0/0 on the scratch merge (b+a+c) — the constants are identical,
so no number moved; the SOURCE moved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
D's live replay caught landmine 1: bench.js read def.wind.venturi off the
STORM def where a venturi never lives (it is SITE data, main.js:453), and the
probes never attempted it — every site_02 bench number was funnel-off, the
third harness to make B's Sprint-11 mistake. Verifying the fix exposed
landmine 2: the tools remapped anchors to a static sway:()=>pos, freezing the
gum tree whose sway is the dynamic load a tr1 rig eats (ablation on the $80
line's q4: 1.02 funnel-off/static, 1.03 funnel-on/static, 1.24 funnel-on/real
anchors). Backyard numbers matched D's UI to the decimal precisely because
p1..p4 are posts — both landmines are no-ops there.
Fix: windForSite(stormDef, siteDef, anchors) in weather.js is the one shared
wind builder (venturi + shelters, exactly main.js's wiring); all garden_bench
tools build wind through it, fly world.anchors themselves via a re-pointable
wind proxy, print the venturi in their headers, and drop the phantom 12 s
settle (commit->attach->storm is one keypress, and the settle skewed storm
sampling via SailRig's internal clock). c.test.js pins the helper with B's
strictly-raises assert against the real shipped funnel (mutation-checked).
Corrected numbers: $80 line wildnight 91.9 with q4 at 1.24-vs-1.2 rating - a
knife edge the real game falls off (D UI: q3+q4 break, 39.8 TATTERED; D's UI
is canonical there, and a corner over its rating is not a safe line either
way). Buster verdict stands (95.2 bench / 97.9 D live). Icenight 9 DEAD 2/4.
Bench still under-reads UI peaks ~5-15% on site_02 - residual in the pool,
stated in THREADS. Separation-target site_02 example withdrawn to A.
Selftest 341/0/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- backyard_01 gains p5 (5.5,-2) h2.6, honest steel NE of the bed: before it the
yard could not beat tattered on the wild night at any price (covering quads
need a fascia corner capped at 2.27 kN; the holding line ceilings at 61.3).
Swept against the $80 wallet; position/height table in the site JSON.
- Separation target adopted AMENDED (ruled on D's funnel-corrected picture):
wild night, best buyable line FULL (>66) and WIN; bare bed LOSES (<50).
Pinned as site data (backyard_01.separation names the $75 recipe
p5=rated + p1/p2/p3=shackle) and FLOWN by a.test through the real chain:
shop at START_BUDGET -> commit -> attach -> skyfx -> garden.js. Measured
68.3 FULL / 0 lost vs bare 35.7 (sep 32.6).
- Full cover stays unbuyable (covering quads still pull 6.4-6.9 kN), the
>45 m2 floor holds, and the ice night still refuses $80 (needs $105 -
p1/p2 miss their shackles by 0.05 kN) - night 5 stays bank-funded canon.
- New asserts: M reaches the real bus / is only advertised when real;
anchor census now 13 (5 posts). Selftest 341/0/0.
- createGarden / GARDEN_DRAIN / HAIL_WEIGHT / RAIN_WEIGHT move from main.js
module-locals to garden.js, unchanged — the audit must predict the sim off
the sim's OWN code, not a drifting copy. GARDEN_DRAIN's comment now carries
the gate-1.4 ruling: the constant does not move; it owns the bare-bed arc.
- M-mute application extracted to exported applyMute(sky, on): calls C's
sky.setMute when the tap exists, reports honestly when it does not, so the
selftest can fail the wiring instead of trusting a ?.() no-op.
Gate 1: C's model confirmed to the decimal in play (backyard 61.3 exact, carport
82.6 exact, fascia 2.27 eff / t=1 settle tear, QA 88 reproduced). The $80-line
wildnight headline does not survive the real game: probe4/bench read venturi off
the storm def, the site def is where it lives — funnel on, q3+q4 break before
the hail and the line reads 39.8 vs cheap 38.5. Separation table for A's ruling;
the proposed target is currently satisfiable nowhere (bare never reads DEAD).
Gate 2: storm grade, wall (both QA sightings dead from in-yard), leaves (cap 7,
one-wind onset with the lean), lean-under-grade, A's spawn fix (160 px clear on
receipts), hail sliver dead twice over. Plus two sightings: last night's sail
haunts the next prep across a site change; hidden-canvas boot NaNs the camera.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Leaves: a pooled handful (cap 7) streaming with the wind from 8.3 m/s — the
same number D keys the player's lean on, so the yard and the body start
telling one story at one speed. Count rides a 2.5 s speed EMA so it doesn't
strobe; own seeded rng (seed ^ 0x1eaf) so the pool never shifts the crate
sequence; bit-identical under identical (dt, t) streams (asserted).
debris.clear() rewinds the layer. dev_skyfx flies the debris layer now and
warms it to the scrub point, so ?t=50 shows the leaves t=50 would have.
D's aftermath sliver, exactly as diagnosed on lane/d: the hail InstancedMesh
at count:0 with frustum culling and depthWrite off still draws instance 0's
identity matrix — a 5 cm always-on-top speck at the origin. mesh.visible now
gates on n > 0, sky.hail joins the public fx object, and a whole-storm assert
demands hidden-at-zero / shown-when-falling in both directions (gated on
floor(1300 x intensity) >= 1: a burst's first frames honestly draw nothing).
THREADS: gate-2 wrap entry — D's judging unblocked, A's M key live.
Selftest 340/0/0 (+2 tests). Mutation-checked: dropped stepLeaves, a 3 m/s
threshold, and an always-visible hail pool all go red.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two QA sightings from in-yard eye height, both geometry, both reproduced on
dev_skyfx before touching anything. (1) The band stopped AT the camera's
horizontal plane, but the ground's true horizon sits ~0.6 deg below it (the
dip), so a bright sky sliver showed under the base — the floating slab. The
band now overshoots to ~12 deg below the equator; the ground occludes the
overshoot by depth, and it survives scale.y's 0.3 rise floor. (2) The arc-end
fade (sin^0.75) left the outer tenth at ~0.36 alpha — the hard vertical seam.
Now sin^1.6 with the arc widened 2.2->2.6 rad so the solid core keeps its
span while the ends drop under 0.12.
Verified by eye on dev_skyfx (southerly t=24, front 0.51): base meets the
ground line, no sliver, no seam. Selftest 338/0/0 (+1 test pinning both in
numbers). Mutation-checked: the old thetaLength and the old exponent each red.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
QA played the southerly at 65 km/h under a noon-blue sky with razor midday
shadows. The arithmetic said why: sky, sun and hemi all multiplied the weather
by the author's palette (darkness 0.5 on the southerly), so the dial could
zero the weather's say — 0.69 storminess became a 0.35 nudge. The grade floors
it (storminess x lerp(0.5, 1, darkness)): a full gale now goes at least half
way to slate on any palette while the wild night keeps its authored black.
Plus the two things nothing keyed at all: the sun disc lerps toward slate and
sun.shadow.radius lerps 1->7 on storminess (PCFSoft, so it is real blur).
Deterministic — pure in t, same curve the rain uses, exposed as sky.stormGrade
above the render gate for headless reads. dispose() hands back intensity,
colour and radius exactly.
Verified by eye in dev_skyfx: southerly t=55, 18.5 m/s reads slate weather.
Selftest 337/0/0 (+1 test). Mutation-checked: the old formula and a dropped
radius line both go red.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One flag plus a gain write, but the flag is the point: a mute pressed on the
splash screen, before the first gesture builds the audio graph, is remembered
and honoured by unlock(). main.js already feature-detects sky.setMute, so the
M key lights itself up on this landing. audio.masterGain getter added so the
assert reads the real bus, not the flag back.
Selftest 336/0/0 (was 335, +1 test). Mutation-checked both ways: a no-op
setMute and an unlock that forgets a pre-unlock mute both go red.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gate-1 diagnosis the sprint chartered, inverted by measurement: the shadow
geometry is exact (probe2: raycast and rasteriser agree 1.0000/1.0000), the
canonical measuring lines just don't cover the bed against vertical hail, and
site_02's tr1+q1+q3+q4 at exactly $80 already separates 91.5 FULL vs 35.7 bare
(sep 55.8, 0/4 lost) with no model change. Full findings + separation-target
proposal in THREADS.
Re-verified after the authoring session died un-pushed: all headline numbers
reproduce; the bench drives RiggingSession.commit(rig) -> rig.attach(), not the
bypassed-attach seam D's landmine names; balance.test's canonical shade-cloth
peaks reproduce per-anchor to the last digit. One fix over the original staging:
probe3/probe4 labelled per-corner peaks in plan order, but rig.corners is ring
order — labels now come from corners[k].anchorId (values were always true; the
$80 line's two RATED corners read swapped). Selftest 335/0/0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Done/green on lane/d: wind lean, ladder knockdown fix, label, E clip
contract, C/B diagnoses. Gate-1 verdict and gate-2 visual judging blocked
on C (still at integration commit).
Documented a measurement trap before it becomes a false alarm: a headless
garden-separation harness showed zero separation, which looks like 'the
sail casts no shadow'. It's an artifact — (1) sky.step's third param is
named 'world' so world.sail is the passed {sail:rig}, not the global; (2)
session.commit() bypasses onCommit->rigSail->rig.attach, so the sail is
never built and every rig scores as bare (the hp-36 constant skyfx.js:803
warns cost gate 0 two sprints). Garden must be measured via real play or
the full attach path. No baseline numbers posted — a bypassed measurement
is worse than none.
Wired the wind lean as a root pose per E's analysis. Keyed on windBase not
raw speedAt (measured: raw strobes 8-18 flips/min, windBase 0-2 per storm).
Verified live at 71 km/h, leanDir dot wind = 1.0. gate 2.4 wind-clips line
is done with zero new clips: brace and stagger already shipped/wired, lean
is root. Open question to E: is the WindPosture additive worth authoring.
The QA pass's biggest player finding: 'stands bolt upright and unbothered'
at 65 km/h. E rendered the two Mixamo lean candidates and rejected both on
screen, and proved the deeper reason a clip can't do this: _rotOnly drops
Hips.quaternion from every clip at load, so a baked lean is discarded, and
a clip's lean axis is fixed while the wind's bearing swings. So the lean is
the ROOT, the same machine as the knockdown pitch and climbY lift.
sim (player.sim.js): sim.lean 0..1 + sim.leanDir, eased over leanSecs.
Magnitude off windBase (the ~4s EMA), NOT raw windSpeed — measured: raw
strobes the posture 8-18x/min across the storms, windBase settles it to 0-2
changes per 90s. Sustained wind = posture; the gust is already the
stumble/knock event. Suppressed on your back (pitch owns the body), braced
(TakeCover is its own hunch), and up a ladder. Bands land on the storm
rating ladder: night 1 upright, 2-3 lean, 4-5 in the gale.
view (player.js): a foot-pivot tip toward leanDir, max leanMaxRad (~17deg,
a third of a knockdown's pi/2), composed under the yaw. It's the else branch
of the pitch tip by construction — the sim guarantees lean=0 whenever
pitch>0, so they never fight.
Three asserts: magnitude scales with sustained speed, a lone gust does NOT
snap you over (windBase inertia), and it's downwind + suppressed when
floored/braced/aloft. Mutation-checked: forcing lean to 0 reddens two.
Verified live at 71 km/h — leanDir dot wind = 1.0, the figure visibly
braces. Selftest 340/0/0. This is the root lean E asked me to wire first;
whether their WindPosture additive follows is a look-at-it-together call.
SPAWN FRAMING. The QA pass: "a pole dead-centre through the player on every
single first impression." Measured, not eyeballed — player spawns at (0,6)
looking up the yard, and post p3 stands at (0,7), one metre behind, dead on the
view line. spawnYawFor() sweeps outward from the ideal frame (camera opposite
the bed, so the player stands in front of what they protect) and takes the
first yaw that clears every obstacle. Per YARD, off site data: site_02 spawns
elsewhere with its posts elsewhere, so a hand-tuned constant would frame the
corner block by luck.
Two numbers here are measured and both were wrong first. Clearance 0.6, not 1.1
— my first pass asked 1.1m and produced a WORSE frame than the bug, swinging
105° to stare at a fence, because the greatest distance any obstacle can have
from the camera->head line is its own distance from the player, and p3 is only
1m away, so 1.1 was unreachable by construction. maxOff 90° — a frame turned
more than a quarter-circle off the bed is no longer a frame of the bed, so the
"roomiest" fallback is bounded to the arc. The assert reproduces the 105° from
the real yard and pins that the cap brings it to 68°.
AFTERMATH CLOTH-SWALLOW. "The dead draped sail can swallow the camera whole."
The camera has collided with the house since Sprint 2; the sail was just never
in the list. While up it hangs above head height and nothing notices — but a
FAILED sail lies in the yard at head height, the one moment you most want to
look at it. refreshCameraSolids() adds the cloth to the set, called from both
rebuilds (loadSiteInto makes new world.solids, rigSail makes a new cloth; either
alone strands the camera on a disposed mesh). Its bounding sphere recomputes
every frame, so the raycast reads live geometry. Verified live: camera solids
9 -> 10 the moment a sail rigs, and the cloth is in the set.
Added camera.solids getter so that was checkable at all — "did the sail join
the set" was otherwise a claim with no seam. And hoisted sailView's declaration
with rig/rigging: loadSiteInto's boot-time call reads it before any cloth
exists, which is a TDZ throw (page goes blank) that the same block already
documents for rig/rigging.
selftest 339/0/0 (was 338).