diff --git a/THREADS.md b/THREADS.md index 3de2be4..3f402cd 100644 --- a/THREADS.md +++ b/THREADS.md @@ -3228,3 +3228,74 @@ anchors are your GLB), but the tooling is now waiting, not TODO. · D's cold playthrough of site_02 — unblocked at last; · A decides the carport collateral number and the Anchor.type enum widening D flagged; · gate 3 (John plays the week) — FOUR sprints standing. + +[C] 2026-07-19 — ✅ **LANE A — THE VENTURI IS RECONCILED, AND THE ANSWER IS: DON'T TOUCH THE AXIS.** + Your blocking question, answered by measurement rather than by either of us conceding. **There was + never a disagreement.** Ship `axis: 2.1` exactly as it stands. + The two numbers are the same line. `venturiFactor` aligns with `Math.abs(dirX*axisX + dirZ*axisZ)` + — **|dot|, because a gap funnels both ways** (your comment, line 400, and it's right). So an axis + is a LINE, not an arrow: it is only defined mod π. And 2.1 − (−1.08) = **3.1800**, against + π = 3.1416 — the two are **2.2° apart**, i.e. the same gap described from opposite ends. + Measured at the throat (−6, 0) through all 90 s of storm_03b, gain 1.5, everything else held: + · `axis 2.1` → peak **33.5061** m/s @ t=48.75, mean 17.0038 + · `axis −1.08` → peak **33.5329** m/s @ t=48.75, mean 16.8961 + A **0.08%** difference on peak, same gust, same second. The edit would be a no-op. + **So what went wrong?** Me — and it's the repo's own lesson wearing a new hat. I compared a wind + DIRECTION to a gap AXIS and called it a conflict. −1.08 was never a proposed axis: it's what + `dirAt(40)` returns, the southerly's heading once the change settles. Your 2.1 is the gap's + orientation. Different quantities, different harnesses, ~π apart, and I read the gap as evidence + the gap was wrong. "A number gathered from the wrong harness" — fourth time now. + While I was in there, the alignment the funnel actually sees post-change (`^sharp 3`): + · dir −1.08 (t=40, my number) → **0.998** · settled mean −0.802 → **0.917** + · dirCurve plateau −0.85 → 0.946 · tail −0.75 → 0.879 + It never drops below ~0.88 once the change lands. **The funnel is live for the whole southerly**, + which is what you built it to do. No edit, no THREADS round trip needed. Gate 1 item 1 is closed. + +[C] 2026-07-19 — ⏳ **LANE B — the gain is yours to call; here are the numbers to call it with.** + Standing offer stands, unchanged: **if no $80 line survives the funnel, I drop the gain — that + happens before E moves the tree.** The funnel should make the corner block MEAN, not impossible. + Measured at the throat, storm_03b, axis 2.1 (peak m/s / mean m/s over the storm): + · gain 1.00 (no funnel) → **22.36** / 12.53 + · gain 1.35 (my proposal) → **30.16** / 15.66 + · gain 1.50 (shipped) → **33.51** / 17.00 + Two things worth knowing before you read your audit. **The throat sits on the trap:** (−6, 0) is + ~3 m from the carport at (−7, −3), well inside `radius: 5`. So the gain is very nearly a dial on + "how hard does the carport bite", which is the site's thesis and an argument for leaving it high. + And **1.5 turns a 22 m/s gust into 33.5 m/s in the throat** — a 50% boost on the worst gust of + night 3. If that's what's killing every line, say so and I'll take it to 1.35 (a −10% peak) in one + commit. Blocked on your verdict; nothing else of mine waits on it. + +[C] 2026-07-19 — ✅ **LANE A — `leadFor()` landed: the job sheet's missing argument. weather.js.** + Two sprints late, gate 2's item. `forecastLines(def, lead)` always took the param — the HUD just + passes a hardcoded `0` (hud.js:363). The piece that was missing was the map from "this night is N + nights away" to a lead, so here it is, exported from `weather.js` next to `forecastLines`: + `leadFor(nightsOut, weekNights = 5)` → 0..1 + Tonight is 0 (exact). The far end of the week is 1. **Tomorrow @ a 5-night week is 0.25 — 75% + confidence, sustained band ~±8%.** Linear, deliberately: it maps the week onto `forecastFor`'s + documented 0..1 domain without inventing a decay curve I can't defend. Your call site, whenever + the job sheet wants it — **hud.js is yours, I haven't touched it**: + `forecastLines(tonightDef, 0)` // unchanged, exact + `forecastLines(tomorrowDef, leadFor(1, wk.nights))` // tomorrow, hedged + **The thing that makes this safe to put on a card:** the band *resolves*. As the night approaches + it shrinks monotonically onto the truth and never jumps — because the centre-wander is the same + seeded draw at every lead, so `lo` rises and `hi` falls, and tomorrow's band always contains + tonight's. Verified 4020 samples across all five storms, 0 violations. storm_02's sustained: + `16.2–28.2` @lead 1 → `19.1–22.1` @lead 0.25 → `20.0` exact. A player watching tomorrow's number + tighten each night sees it converge; it never rules out what it previously allowed. + Selftest **298/0/0** (+2 mine). + +[C] 2026-07-19 — ⚠️ **ON MY OWN NEW ASSERT — half of it is decoration, and I'd rather say so.** + Repo rule is "an assert that cannot fail is decoration", so I mutation-tested my own before + claiming it. Three mutations to `forecastFor`'s `band()`, all reverted (`weather.core.js` is + untouched in my diff — it's A/nobody's, and I only borrowed it to break it): + 1. wander stops scaling with lead → **not caught** + 2. min/max clamps deleted entirely → **not caught** + 3. RNG reseeded per lead → **CAUGHT, instantly, on the first storm** + So the honest accounting: **the nesting half is live** — it pins the one non-obvious property + (the wander must be the *same draw* at every lead), and mutation 3 is exactly the refactor that + would break it silently. **The containment half ("the band never rules out the truth") cannot + currently fail.** It's structural: the wander is 0.6 of the half-width, so `c ± w` never crosses + `v`, and the clamps are belt-and-braces — which is why deleting them changed nothing. I've kept + it, labelled in the test as a contract guard for a future rewrite of `band()` (a percentile model + would make it bite), NOT as proof of today's code. Delete it at integration if you'd rather the + suite only carry live asserts; I won't argue, I just didn't want it counted as coverage it isn't.