Commit Graph

13 Commits

Author SHA1 Message Date
type-two
3548b6edc1 Lane C S17 gate 1 (cont.): the OFFER BAND and the PAIRING law — night 7 stops lying
Landed ON TOP of f029274 with not one line of it changed. Two Lane C sessions
ran gate 1 in parallel; I found out by having my push rejected. forecastHonest
answers "can the forecast describe this STORM" and is better than the storm-side
half I had written, so mine is deleted. These are the two things it structurally
cannot reach — it takes a def, not a pairing, and it never touches the card.

weather.js is +180/-0. The only line of theirs I touch anywhere is widening one
c.test import from { NIGHTS } to { NIGHTS, nightAt }.

1. THE OFFER BAND, and the finding that motivates it. A's card prints f.wind
   and f.stones, and under that pair NIGHT 7 LIES:
     soaker (scripted)   gusts to ~55 km/h · fine pea stones
     early buster (alt)  gusts to ~76 km/h · pea stones
   On both printed lines the soaker is the softer job. It is not: 14.9s of hail
   against 2.4s, 44 mm/hr against 16.5. The soaker's whole design is that its
   wind reads gentle and its hail is the trap, so the board recommended the trap
   in the storm's own voice. offerBand(def, lead) returns an ORDERED LIST, so
   the forecast decides which facts a night carries and a card cannot drop the
   next one added — same construction as A's stormsToPreload(nights, pool).
   Verified in the real card on :8826.

   Second finding: storm_03 and storm_03b are identical in every number the band
   prints except the change time (30s vs 18s), and the board offers exactly that
   pair on nights 5 and 7. Without the change line it offers two indistinguish-
   able jobs and calls it a choice. Pinned.

2. PAIRING_LAW. A band can be perfectly honest about a storm and still be an
   invitation to a night nobody can work. A's three POOL exclusions were prose;
   promoted to checked data with the measurement in each message, credited by
   owner (the soaker's is mine with the S16 ~31% receipt; wildnight and icenight
   are recorded as A's reasons, not re-measured by me). Absent key = unrestricted
   deliberately — inventing a refusal for a pairing nobody flew is the same
   offence inverted, and that direction is pinned too. pairingRefusals is NOT a
   filter: silently dropping an entry is a runtime surprise wearing a fix's
   clothes. Walks NIGHTS u POOL carrying the SITE: 12 pairings, 0 refusals, with
   a vacuity guard in the same case.

Mutation-checked on THIS tree after the code changed address: hail line dropped
(RED, names the storm and the seconds), soaker restriction deleted (RED x2,
including the vacuity guard), change line dropped (RED, names the two busters).
All restored, 494/0/0.

Filed in THREADS, not landed: the hud/main hunk for A (leadFor precedent), and
a floor for stoneWord — hail.size 0 words itself "fine pea stones" because
stoneWord has no floor. Their STONE_WORD_CEILING fixes the top of that range;
nothing fixes the bottom. It belongs inside forecastHonest, which is theirs.

Selftest 494/0/0 (their 490 + 4).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 03:07:02 +10:00
type-two
f029274940 Lane C S17 gate 1 + 1.2: the honesty gate on the board, and the exposure cross
- forecastHonest(def): "a storm the forecast can't describe honestly doesn't
  get offered" as a predicate, not a sentence — validator + measurable stats +
  the stone vocabulary ceiling (STONE_WORD_CEILING 2.0: past it, "golf ball"
  undersells the ice and the word ambushes where the band can't). The band-
  containment recomputes are disclosed as structural regression armour, not
  counted as coverage.
- c.test walks NIGHTS ∪ POOL from the shipped data (the union, per A's
  stormsToPreload finding — a pool-only storm is covered the day it exists).
  Negative controls: a lying windchange, a cricket-ball stone, both refused
  by name. Lead-0 card pins: the offer card's gust/sustained figures parsed
  back out and matched against stormStats; a lead-0 offer must not hedge.
- The exposure cross (the S14 pin pattern, third outing): my failure-route
  envelope (built world, anchor collateral keys through world.collateralFor,
  the gnome per scoreRun) vs A's exposureOf — item-level equality on ALL
  THREE yards: backyard 115, corner block 205, swing lawn 255. Zero unpriced
  keys. Teeth proven in-suite: strip the carport key and the cross reds.

Selftest 490/0/0 (A's 485 + 5), browser-verified; board verified live on
:8826 — both cards print the honest lead-0 lines and the crossed dollars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 02:39:07 +10:00
type-two
d03712ae47 Lane C S16: gate 3 lands — the fabric bet pinned, forecast argues it, storm in the lists; selftest 438/0/0
Gate 3.1 pins (c.test): the soaker over site_02 through gardenfly, porosity
the only variable — bare <30 (17.5), best cloth line <50 (42.3, still >bare:
the leak is partial), same $75 ring on membrane >90 FULL (96.5) with lost<2
and cost<=80, $60 membrane ring wins too (68.8), separation >40 (54.2). The
inverse guard: membrane on carabiners reads WORSE than cloth (19.1/3 lost vs
24.5/2) — the F key is a bet, not an upgrade. Gate 3.3: forecastLines grows
stones + rainRate (worded, banded); stormStats.hailSize + forecastFor
hail.size band, spread 0.30, nesting/truth-holding pinned in the resolving
loop; existing card fields byte-identical (new band draws last). storm_06 in
STORM_KEYS + suite STORMS (sync assert is the tripwire). Envelope tool flown:
throat 21.55 @ 59.2, q1 17.78 (rated slot), q2 wind-coldest yet cloth-side
second-hottest — the pond's signature isolated between harnesses. Mutations,
separate runs, red-then-green: gardenfly seam hardcoded -> both 3.1 pins red;
STORM_KEYS drop -> list-sync red with diff; stormgen rng -> byte-equal red.
Browser selftest 438/0/0 (418 + 4 storm honesty + 12 stormgen + 4 gate-3).
THREADS: gate entries + D pairing receipt + B seam/finding notes.
2026-07-20 20:33:10 +10:00
type-two
45bdc2d079 Lane C S13 correction: the bench flew site_02 funnel-OFF and tree-FROZEN; wild-night headline dies
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>
2026-07-18 13:08:21 +10:00
type-two
8e23bf4702 Land leadFor(): the job sheet's per-night forecast lead
forecastLines(def, lead) always took the param; what was missing was the
map from "this night is N nights away" to a lead. leadFor(nightsOut,
weekNights) is that map, linear onto forecastFor's documented 0..1 domain
so tonight is exact and the week's far end is lead 1. Tomorrow at a
five-night week reads 0.25 — 75% confidence, sustained band ~±8%.

hud.js is Lane A's; this only exports the argument.

The new assert pins the property that makes a band safe to print: it
RESOLVES. lo rises and hi falls monotonically onto the truth as the night
approaches, because the centre-wander is the same seeded draw at every
lead — so tomorrow's band always contains tonight's and the card never
jumps. Verified against a mutation that reseeds per lead (caught).

Its containment half is honestly decoration today and is labelled as
such in the test: 0.6-of-half-width wander means c±w never crosses v, so
deleting the clamps does not make it fail. Kept as a contract guard for a
future rewrite of band(), not counted as coverage of today's code.

Selftest 298/0/0.
2026-07-17 17:14:53 +10:00
m3ultra
5cb838b314 Add validateSiteWind — the site JSON's wind block fails loud (SPRINT10)
A builds world.js FROM DATA this sprint, and a site's `wind` block (venturi
funnels, later per-site shelter overrides) is hand-authored data like a storm —
so it validates like one. setVenturi already clamps defensively, but a clamp
hides a typo where the repo's ethos is to name it: a gain < 1 (someone reaching
for a shelter), a funnel with no throat coordinates, an axis typed "south". A
calls this at site load next to validateStorm. A site with no wind block, an
empty block, or an empty venturi list all validate — the funnel is opt-in.

Node 56/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:30:00 +10:00
m3ultra
19f4ab6c63 Venturi: per-site wind personality for site_02 (the corner block funnels the southerly)
A venturi is a shelter's opposite — a gap between buildings SPEEDS the wind up
when it blows along the gap's axis. Where a tree shadow depends on being downwind
of the tree, a venturi depends on the wind being ALIGNED with a fixed axis the
SITE owns, so it fires only when the storm's direction swings to match: the
corner block is calm until the southerly comes through, then it screams. That
interplay is exactly SPRINT9's brief — funnel geometry is the site's, the wind
that funnels is the storm's.

Extends the existing shelter system rather than bolting on a parallel one:
speedAt/vecAt/verticalAt all route through one localHoriz, which now folds
venturiFactor in alongside spatialFactor and shelterFactor. The downdraft rides
local speed, so a funnelled wind gets a proportionally stronger downdraft for
free — physically right.

`setVenturi(list)` on the wind + router (A's "add it to the router too" rule; the
tripwire is green). Inert on an empty list, so backyard_01 is byte-identical —
asserted. Multiplier is ALWAYS ≥ 1 (a venturi accelerates, never shelters), and
it's C1-continuous through the change, so it can't snap a corner — both asserted
(aligned wind boosts 1.5×, crosswind exactly 1.0×, max frame jump 0.50 m/s).

Data lives in site JSON (A's schema, arriving with the extraction) — this is the
physics + the setter, ready to wire. Proposed shape flagged for A in THREADS.

Also fixed a stale comment: storm_02's _gusts_comment still said "HELD at 0.12
for now" from Sprint 3 while the value has been 0.45 since Sprint 4 — a lying
comment in a repo that reads comments as canon. Now records the real history and
the pending paired-0.40 flip.

Selftest 278/0/0; node 55/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:26:24 +10:00
m3ultra
756994d98e Ship forecast uncertainty to the card (carried twice — now visible)
The model landed in Sprint 6 and nothing ever called it, which is why this kept
getting carried: `forecastFor` existed while the card went on estimating inline.
So this is the wiring, not more API.

`forecastLines(def, lead)` turns a storm into the card's two stat lines, already
worded in A's voice. lead 0 = tonight = exact numbers (reads exactly as the card
always has); further out it hedges into ranges and prints a confidence line. The
bands always contain the truth — a forecast may be vague, never wrong, or a
player who rigs for the top of the stated range gets ambushed.

Two things the card gains beyond the bands:
- The numbers are now MEASURED. The inline estimate (`baseCurve peak + powBase
  + powRamp`) read 30 m/s for storm_02; it really gusts to 32.3, because gust
  power is drawn per gust and rides a ramp. The wild night now says 116 km/h
  because that is what hits you.
- The card finally MENTIONS HAIL. Hail has been the garden score since decision
  13 and the forecast never said so — you were being scored on something the
  card didn't tell you was coming. Now: "hail likely" on the wild night, silent
  on the gentle one, "possible" when it's too far out to promise.

hud.js is Lane A's file and A is meant to build the week uninterrupted, so this
is deliberately a 3-line swap that keeps their markup, classes and wording — the
week's card rewrite can carry it or drop `forecastLines` and call `forecastFor`
directly. `lead` defaults to 0, so nothing changes until the week passes one.

Verified live at both ends: tonight renders exact; five nights receding into the
week render 0%-confidence hedges ("16–28 m/s · gusts ~72–144 km/h · hail
possible"). Selftest 265/0/0 + gate 0's 2 skips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:10:53 +10:00
m3ultra
0d05cb1936 Land hailBlockFor(size, porosity) — the honest fabric-hail rule for B
B asked for the hail ruling before coding fabric choice; this is it as code.
The physics: porosity is about AIR (blows through → less wind load) and WATER
(drains → no ponding), NOT ice. A knitted shade cloth's gaps are ~1-3 mm; a
damaging hailstone is 6-45 mm, so it can't pass a mesh an order of magnitude
finer than itself — porous and membrane block the big stones identically. The
ONE true difference is the finest pea hail, which IS small enough to rattle
through an open weave. So: membrane stops everything; porous stops everything
except the smallest stones.

Verified across the storm sizes: shade cloth (0.3) fully blocks the wild-night
1.3/1.4 stones and leaks 26% of storm_03's 0.7 pea hail; an open 0.5 weave
leaks 90% of pea hail but still catches the big ice. That makes the fabric
choice cost you exactly on the mild-hail nights and stay honest on the ice
nights — a real tradeoff without a physics lie.

The assert also proves the integration end to end so nobody wires it blind: a
membrane-covered bed takes ~0 hail on storm_03, a porous-covered one takes 0.62
— the choice is genuinely non-trivial. Formula for whoever wires the garden
drain: coveredHail = hailShadowOver(bed) * hailBlockFor(hailSize, sail.porosity).

Pure helper (no THREE), exported through weather.js alongside stormStats/
forecastFor; NOT on the wind contract, so no router change. Selftest 263/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 11:30:37 +10:00
m3ultra
f1d51d6b87 Hail — the system that makes the garden score respond to the rig (decision 13)
Rain honestly walks under a sail (a droplet's terminal velocity is ~9 m/s, so a
30 m/s crosswind blows it in at ~73° off vertical), which is why a perfect rig
scored 54% garden vs 48% for no rig at all. Hail is dense: terminal ~22 m/s, and
a dense stone couples weakly to the crosswind, so even a gale leans it ≤20°.
Steep hail is blocked by overhead cloth, so the garden score becomes
rig-responsive without faking the rain physics — and it was always DESIGN.md
canon (hail shreds gardens; drainage answers rain, later).

weather.core: `hail` block in storm JSON — authored bursts (envelopes) plus one
synced to every gust at/above `withGustsAbove`, so the biggest gusts arrive WITH
ice. `hailAt(t)` (max over live bursts), `hailSize`, validator. Gust-synced
bursts key off the deterministic gust timeline and draw ZERO randomness, so
tuning hail can't re-time the storm — asserted, same guarantee as the downdraft.
storm_02 bursts on the southerly change (peak 1.0 at t=56.5, 11.4 hail-seconds);
storm_03 a mild 0.5; storm_01 none.

skyfx: `hailVelocity` (steep, ≤20° lean, terminal-velocity reasoning cited so
nobody re-opens the rain-angle argument), a second RainShadow fed the steep
vector, `gardenHailExposure(bed, t)` in the gardenExposure mold (A wires the
drain), instanced falling stones (hidden under the cloth so you SEE the sail
working), and hail audio: ground clatter that fades as the sail intercepts, plus
the cloth DRUM that rises exactly as the sail catches hail — the "my sail is
earning its money" sound.

Decision-13 gate proven: no-sail garden takes 4.4× the hail of a bed under a
good rig over a full storm_02 (bar is ≥2×), through the real gardenHailExposure
and B's SailRig. Verified live too (router-patched): stones fall visibly steeper
than the rain beside them, and a bed-covering rig cuts hail exposure roughly in
half at the burst.

Selftest 214/0/0 (was 207); node 36/0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 03:14:27 +10:00
m3ultra
1f99cd9bca Rain gets physical units for ponding; night pass on wildnight
SPRINT4 §Lane C 2/3/4.

PONDING DATA (decision 10). rainAt() was a dimensionless 0..1 — fine for drop
count and opacity, useless for water mass. Lane B would have had to invent the
mm/hr scale, which is exactly the "default-off code tuned by a constant I
invented" they rightly reverted. So the scale is storm data now:
rain.peakMmPerHour (validated 0..300; a rain curve without one is a hard error,
since ponding would silently use the default instead of what the author meant),
plus wind.rainMmPerHour(t) and rainDepthMm(t0,t1). RAIN_TIME_COMPRESSION=40 is
exported from weather.core so B applies it cloth-side rather than either of us
hardcoding 40 twice: how hard it rains is mine, how much water a sail holds is
theirs.

Calibrated to B's own arithmetic, and the numbers land on it:
  storm_02  80 mm/hr severe  -> 50.9 mm = 3.12 kN/corner   (B predicted 3.1)
  storm_03  30 mm/hr moderate -> 8.3 mm = 0.51 kN/corner   (teases a carabiner)
  storm_01   8 mm/hr shower   -> 0.9 mm = 0.06 kN/corner   (harmless, as designed)
against a storm_02 wind load of 0.2-1.1 kN. A flat rig should drown in the wild
night; a hypar pools nothing and won't notice. Asserted with B's arithmetic so
the storms are provably fit for their water before their cloth lands.

DECISION 7 helper for Lane A: sky.gardenExposure(bed, t) = rainAt x (1-shadow),
the whole drain term in one call. Note it moves on its own — the rain shadow
follows the wind, so the southerly change walks the dry patch off the bed and
the drain climbs with no corner having failed.

NIGHT PASS. sky.night is now the author's call (was: inferred from a darkness
threshold), and darkening scene.background did nothing anyway — the cloud dome
covers it at 0.85 opacity, so an overcast-grey texture was what you actually saw.
The dome now tints AND crushes (a lerp alone lands #717273: it runs in linear
space and the texture is baked near-white). Stops at 0.78 because the yard has no
lights and a storm you can't see is a black screen. Lightning now lights the
cloud it's inside (#3e3e3f -> #d4ddf2), and fires on the biggest gusts via
sky.lightningGustPow, not just the three authored strikes — driven off the
telegraph so the flash lands with the gust that earned it.

Also: c.test's storm list was hardcoded to two storms while the node runner globs
the directory, so storm_03 was untested in the browser half. Its own ponding
assert caught it.

Selftest 195/0/0 (was 184). Verified live: night reads as night, flash lights the
cloud, exposure responds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:28:09 +10:00
m3ultra
383471d0f5 Add debris, skyfx and the Lane C bench; align to landed contracts
debris.js: hand-rolled kinematic tumble, drag ∝ speed² so the gust that
spikes a corner is the one that launches the neighbour's bin. Ground bounce
via world.heightAt (contracts.js documents it as ours), sphere-vs-player
knockdown reported to Lane D, sphere-vs-sail-node impulse duck-typed so it
lights up when Lane B exposes nodes and stays silent until then.

skyfx.js: instanced rain that wraps around the camera rather than respawning,
storm sky + procedural cloud dome, lightning, and synthesized WebAudio layers
(wind bed, howl, rain, gust whoosh on the telegraph, rope creak off the worst
corner, flog when one blows). It modulates Lane A's lights and hands them back
on dispose() rather than owning them.

weather_demo.html: graybox bench to drive all three before M0 — mock sail,
storm scrub, 4x, throw-a-crate.

Aligned to contracts.js now that it has landed: relative three imports (there
is no importmap), contracts' rng() instead of a local copy, and storm paths
resolved off import.meta.url — server.py serves the repo root, so an absolute
/world/... would have 404'd at integration.

storm_02: fix the southerly change. contracts.js puts north at -Z and the wind
vector blows toward (cos d, sin d), so the old swing to +2.6 blew toward due
south — a northerly wearing a southerly's name. Now slews to -1.35: a SSW
buster off the open side of the yard, into the house.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:38:21 +10:00
m3ultra
84f647a90c Add wind field, storm timelines and weather selftest
Implements the contracts.js wind surface (PLAN3D §4): sample(pos,t) and
gustTelegraph(t), plus storm defs as data.

The prototype scheduled gusts by integrating (wind.gustT += dt). We can't:
sample(pos,t) is called by sail/player/debris/rain at arbitrary t, so gusts
are precomputed into a timeline from a seeded PRNG at load and read from t.
Envelope shape is a faithful port — telegraph 1.5s / ramp 0.8s / hold 1.7s /
fade 1.0s.

Maths lives in weather.core.js with zero imports (no THREE, no DOM, no
Date.now), so the determinism rule is structural and the suite runs in node
without waiting on Lane A's M0.

storm_01_gentle peaks at 11 m/s; storm_02_wildnight sustains 20 m/s and gusts
to 32 (BOM 'destructive'), with the southerly change landing just before the
worst of it — the corners that were slack all storm are the ones that cop it.

15 asserts green: telegraph lead >=1.2s, wind continuity in time and space,
storm JSON validator (incl. 14 deliberately-broken defs it must reject),
determinism, sample-order independence, tree wind shadow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:38:12 +10:00