Merge remote-tracking branch 'origin/lane/c'
# Conflicts: # THREADS.md
This commit is contained in:
commit
f696343fbf
70
THREADS.md
70
THREADS.md
@ -3550,3 +3550,73 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
lane clone's dev server collides with every other and with the integration checkout. I worked
|
||||
around it locally rather than committing a port change into lane/e. Worth a per-lane port or
|
||||
`autoPort` at integration, before five lanes try to run a server at once.
|
||||
[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.
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
import * as THREE from '../../vendor/three.module.js';
|
||||
import { assert, fixedLoop } from '../testkit.js';
|
||||
import { FIXED_DT, checkContract, DEBRIS_PIECE_FIELDS } from '../contracts.js';
|
||||
import { loadStorm, createWind, forecastLines } from '../weather.js';
|
||||
import { loadStorm, createWind, forecastLines, forecastFor, leadFor } from '../weather.js';
|
||||
import { createDebris } from '../debris.js';
|
||||
import { createSkyFx, RainShadow } from '../skyfx.js';
|
||||
import { SailRig, HARDWARE } from '../sail.js';
|
||||
@ -414,6 +414,64 @@ export default async function run(t) {
|
||||
'the gentle storm advertised hail it does not have');
|
||||
});
|
||||
|
||||
// SPRINT11 gate 2: the job sheet shows TOMORROW, and tomorrow becomes tonight.
|
||||
// A band that widens or jumps as the night approaches is a card that lied
|
||||
// once — so the RESOLVING is the contract, not just the width.
|
||||
//
|
||||
// What each half is worth, established by mutation (SPRINT11, see THREADS):
|
||||
// · NESTING is the live one. It rests on the wander being the SAME seeded
|
||||
// draw at every lead — reseed `r` per lead and this goes red immediately.
|
||||
// · CONTAINMENT is structural under today's band(): the wander is 0.6 of the
|
||||
// half-width, so c±w never crosses v, and the min/max clamps are belt-and-
|
||||
// braces. Removing the clamps alone does NOT make it fail. It is kept as a
|
||||
// contract guard for a future rewrite of band() (a percentile model, say),
|
||||
// where it stops being free — not as proof of today's code.
|
||||
t.test('the forecast band resolves as the night approaches: nests, and never sheds the truth', () => {
|
||||
const keys = [
|
||||
['sustained', (s) => s.sustained],
|
||||
['gustPeak', (s) => s.gustPeak],
|
||||
['rain', (s) => s.rainPeak],
|
||||
['rainMmPerHour', (s) => s.rainPeakMmPerHour],
|
||||
];
|
||||
for (const [name, def] of Object.entries(storms)) {
|
||||
for (const [k, truthOf] of keys) {
|
||||
let prev = null;
|
||||
// walk the week backwards: the far end -> tonight
|
||||
for (let i = 40; i >= 0; i--) {
|
||||
const f = forecastFor(def, i / 40);
|
||||
const b = f[k];
|
||||
const truth = truthOf(f.truth);
|
||||
assert(b.lo <= truth + 1e-9 && b.hi >= truth - 1e-9,
|
||||
`${name}.${k} @lead ${i / 40}: band ${b.lo}–${b.hi} rules out the truth ${truth}`);
|
||||
if (prev) {
|
||||
assert(b.lo >= prev.lo - 1e-9 && b.hi <= prev.hi + 1e-9,
|
||||
`${name}.${k} @lead ${i / 40}: band ${b.lo.toFixed(3)}–${b.hi.toFixed(3)} is not inside `
|
||||
+ `the vaguer ${prev.lo.toFixed(3)}–${prev.hi.toFixed(3)} — it jumped instead of resolving`);
|
||||
}
|
||||
prev = b;
|
||||
}
|
||||
// and it must ARRIVE: tonight is the truth, not merely a narrow band
|
||||
const exact = forecastFor(def, 0);
|
||||
assert(exact[k].lo === exact[k].hi,
|
||||
`${name}.${k}: tonight should be exact, got ${exact[k].lo}–${exact[k].hi}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
t.test('leadFor: tonight is exact, the far end of the week is vague, past it clamps', () => {
|
||||
assert(leadFor(0, 5) === 0, 'tonight must be lead 0 — the job sheet is not a guess');
|
||||
assert(leadFor(4, 5) === 1, "the week's far end must be lead 1");
|
||||
assert(leadFor(1, 5) === 0.25, `tomorrow @5 nights should be 0.25, got ${leadFor(1, 5)}`);
|
||||
assert(leadFor(9, 5) === 1, 'beyond the week must clamp, not exceed 1');
|
||||
assert(leadFor(-1, 5) === 0, 'a night already survived must not go negative');
|
||||
assert(leadFor(1, 1) === 1, 'a one-night week has no far end: anything but tonight is a guess');
|
||||
// the point of the param: tomorrow reads hedged, tonight does not
|
||||
const def = storms.storm_02_wildnight;
|
||||
assert(!/–/.test(forecastLines(def, leadFor(0, 5)).wind), 'tonight should not hedge');
|
||||
assert(/–/.test(forecastLines(def, leadFor(1, 5)).wind),
|
||||
`tomorrow should hedge, got "${forecastLines(def, leadFor(1, 5)).wind}"`);
|
||||
});
|
||||
|
||||
t.test('every storm in data/storms/ loads and validates', () => {
|
||||
// loadStorm throws on invalid, so reaching here with all of them is the pass
|
||||
assert(Object.keys(storms).length === STORMS.length, 'a storm failed to load');
|
||||
|
||||
@ -23,6 +23,30 @@ export {
|
||||
const kmh = (ms) => ms * 3.6;
|
||||
const band = (b, fmt) => (b.hi - b.lo < 0.05 ? fmt(b.lo) : `${fmt(b.lo)}–${fmt(b.hi)}`);
|
||||
|
||||
/**
|
||||
* How vague a night reads when it is `nightsOut` nights away — the week IS the
|
||||
* forecast horizon, so tonight is exact (0) and the far end of the week is as
|
||||
* vague as this game forecasts (1). Linear, because that maps the week onto
|
||||
* forecastFor's documented 0..1 domain with nothing invented in between.
|
||||
*
|
||||
* For a five-night week: tomorrow reads at lead 0.25 — 75% confidence, a
|
||||
* sustained band about ±8% wide. Visibly hedged, still worth rigging to.
|
||||
*
|
||||
* Lane A: this is the job sheet's missing argument. `forecastLines(def, 0)` is
|
||||
* tonight; `forecastLines(tomorrowDef, leadFor(1, wk.nights))` is tomorrow's
|
||||
* band. Nights beyond the week (or a one-night week) clamp to 1 and 0.
|
||||
*
|
||||
* @param {number} nightsOut 0 = tonight, 1 = tomorrow
|
||||
* @param {number} [weekNights] nights in the week (5)
|
||||
* @returns {number} lead, 0..1
|
||||
*/
|
||||
export function leadFor(nightsOut, weekNights = 5) {
|
||||
if (!(nightsOut > 0)) return 0;
|
||||
const horizon = weekNights - 1;
|
||||
if (!(horizon > 0)) return 1; // a one-night week: anything but tonight is a guess
|
||||
return Math.min(1, nightsOut / horizon);
|
||||
}
|
||||
|
||||
/**
|
||||
* The forecast card's two stat lines, already worded — DESIGN.md's partial
|
||||
* information made visible. Lane A owns the card; this owns the numbers, so the
|
||||
|
||||
Loading…
Reference in New Issue
Block a user