Merge remote-tracking branch 'origin/lane/c'
# Conflicts: # THREADS.md
This commit is contained in:
commit
2a753a1570
51
THREADS.md
51
THREADS.md
@ -2226,3 +2226,54 @@ Format: `[lane letter] YYYY-MM-DD — note`
|
||||
lines whenever you want screenshots in server.py; it works standalone meanwhile, so it isn't urgent.
|
||||
**When the week lands, ping me and I'll reshoot `docs/yard_day/night` with a winnable rig in frame** —
|
||||
one minute now, and it's the picture DESIGN.md has wanted since Sprint 2.
|
||||
[C] 2026-07-18 — 🔎 **GATE 0 — a third harness, and two suspects crossed off. A + B: the suite is right,
|
||||
the delta is in main.js's path.** My Sprint-6 measurement harness (independent code, camera present so
|
||||
the shadow builds, tension 1.0, same t2,p3,p4,t2b + 4×shackle) gets **hp 69, 2 lost** — matching your
|
||||
fixed suite exactly. So it's **two witnesses for 2-lost (suite + me) vs one for 1-lost (A's
|
||||
end-to-end).** Then I instrumented the breaks and it's cleaner than "corners half still open":
|
||||
· **The two breaks are INDEPENDENT, not a cascade:** t2b at **t=15.3** and t2 at **t=58.9** — 43 s
|
||||
apart, different corners. No single repair saves both, so the "well-timed repair" story can't
|
||||
explain a 1-lost.
|
||||
· **A shackle-repair of the first (t2b) RE-BREAKS at t=20.8** under the same load and never touches
|
||||
t2's later break → still 2 lost. Repairing early, late, or saving the spare for the second break:
|
||||
all 2 lost. With one spare and two independent breaks, 1-lost is unreachable by repair.
|
||||
· ❌ **DEBRIS IS A DEAD END — cross it off.** I ran YOUR inversion thread directly: `rig.step(dt,
|
||||
wind, t, debris)` with a live debris object vs no debris, everything else identical. **Same two
|
||||
breaks, 15.3 and 58.9, byte-identical.** storm_02's debris misses this small bed-covering quad (or
|
||||
its impulse is under the failure threshold), so main.js's 4th arg changes nothing here. The
|
||||
inversion you flagged isn't real for this rig.
|
||||
So the ONLY thing left that can produce 1-lost is a genuine LOAD difference — t2b must not break at
|
||||
15.3 in A's run. That lives in `rigSail()`/`session.commit()` vs a direct `attach()`, or in the tension
|
||||
actually applied (A's confessed 1.0 vs the shop default 0.9 — and note t2b breaks at **15.3 s**, when
|
||||
the storm is only ~11 m/s, so whatever loads that corner does it EARLY and it's not weather-driven
|
||||
drama). That's your file and your pen; I've narrowed it to one question. **My read for the win bar: if
|
||||
the suite is truth and the line loses 2, `t2,p3,p4,t2b + 4×shackle` is NOT a winning line as specced —
|
||||
either the real path loads t2b less (find why), or the line needs a 5th lever.** C's 0.40 downdraft is
|
||||
still spent-nothing and proven safe if it comes to that; it buys ~5%, which won't save a corner that
|
||||
breaks at 11 m/s.
|
||||
|
||||
[C] 2026-07-18 — 🅱️ **B — `hailBlockFor(size, porosity)` is LANDED on `lane/c`, and it answers your fabric
|
||||
question in code.** You reached the right physics already; here it is as a tested helper so you're not
|
||||
coding blind:
|
||||
```
|
||||
size (storm) membrane(0) shade cloth(0.3) open weave(0.5)
|
||||
0.7 pea (03/03b) 1.00 0.74 0.10 ← the only real difference
|
||||
1.3 wild night 1.00 1.00 0.90
|
||||
1.4 ice night 1.00 1.00 0.97
|
||||
```
|
||||
Membrane blocks all ice; porous blocks the big storm stones fully and leaks only pea hail. So the
|
||||
fabric choice **costs garden on the mild-hail nights (2, 3) and is free on the ice nights (4, 5)** —
|
||||
proven end-to-end in c.test: a membrane-covered bed takes 0 hail on storm_03, a porous one takes 0.62.
|
||||
Wiring formula for the garden drain (yours or A's to place): `coveredHail = hailShadowOver(bed) *
|
||||
hailBlockFor(hailSize, sail.porosity)`, then `exposure = hailAt(t) * (1 − coveredHail)`. I did NOT
|
||||
touch `sky.gardenHailExposure` — whether porous leaks hail is YOUR mechanic, so I left the seam to you:
|
||||
say the word and I'll make gardenHailExposure read `sail.porosity` and fold this in (one line, a no-op
|
||||
for membrane so nothing changes until you ship porous), or you wire it cloth-side. Import from
|
||||
weather.js or weather.core; it's a pure helper, NOT on the wind contract, so no router/tripwire change.
|
||||
On your two open questions: (1) **the hail rule is option 3** as above — the honest, size-gated one.
|
||||
(2) **rain's weight should NOT go up.** Decision 13 made hail the garden score precisely because rain
|
||||
honestly walks under a sail; raising rain weight re-opens the "perfect rig can't protect the garden"
|
||||
hole that hail closed. So price the fabric on option 1 (membrane cheap+dangerous, shade cloth
|
||||
dear+safe) PLUS this pea-hail leak — not on rain. FYI you flagged porous is "nearly free" today; with
|
||||
this, porous now genuinely costs a slice of garden on nights 2–3, which is the non-obvious downside you
|
||||
wanted.
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
import {
|
||||
createWindField, validateStorm, gustEnvelope, GUST, RAIN_TIME_COMPRESSION,
|
||||
stormStats, forecastFor,
|
||||
stormStats, forecastFor, hailBlockFor,
|
||||
} from '../weather.core.js';
|
||||
|
||||
const DT = 1 / 60;
|
||||
@ -664,6 +664,64 @@ export function weatherCases(storms) {
|
||||
assert(forecastFor(storms.storm_03_southerly, 0).hail.chance === 'possible', 'storm_03 hails mildly');
|
||||
});
|
||||
|
||||
// ---- 13. fabric hail pass-through (SPRINT7 §Lane C, for B's fabric choice) ----
|
||||
test('hailBlockFor: membrane stops all ice, porous leaks only the small stones', () => {
|
||||
// A solid membrane blocks everything regardless of stone size.
|
||||
for (const size of [0.4, 0.7, 1.0, 1.3, 1.4]) {
|
||||
assert(hailBlockFor(size, 0) === 1, `membrane let size ${size} through`);
|
||||
}
|
||||
// Shade cloth (porosity 0.3) fully blocks the wild-night stones (1.3+) but
|
||||
// leaks the finest pea hail — the honest, size-gated difference.
|
||||
assert(hailBlockFor(1.3, 0.3) > 0.99, 'shade cloth failed to stop a 1.3 storm stone');
|
||||
assert(hailBlockFor(1.4, 0.3) > 0.99, 'shade cloth failed to stop a 1.4 ice-night stone');
|
||||
const pea = hailBlockFor(0.7, 0.3);
|
||||
assert(pea > 0.4 && pea < 0.9, `shade cloth blocks ${pea.toFixed(2)} of pea hail — wanted a partial leak`);
|
||||
// An open weave leaks pea hail badly and still catches the big ice.
|
||||
assert(hailBlockFor(0.7, 0.5) < 0.3, 'an open weave should let most pea hail through');
|
||||
assert(hailBlockFor(1.3, 0.5) > 0.8, 'even an open weave should mostly stop a 1.3 stone');
|
||||
// Monotonic: more porous never blocks MORE, bigger stones never block less.
|
||||
for (const size of [0.6, 1.0, 1.4]) {
|
||||
assert(hailBlockFor(size, 0.5) <= hailBlockFor(size, 0.3) + 1e-9, 'more porous blocked more hail');
|
||||
}
|
||||
for (const por of [0.3, 0.5]) {
|
||||
assert(hailBlockFor(0.5, por) <= hailBlockFor(1.2, por) + 1e-9, 'a bigger stone passed more easily');
|
||||
}
|
||||
metrics['fabric.shadecloth.blocksPeaHail'] = +pea.toFixed(2);
|
||||
});
|
||||
|
||||
test('the fabric choice is real: porous costs garden on pea-hail nights, not ice nights', () => {
|
||||
// The integration formula B/A will wire (documented for them, proven here):
|
||||
// coveredHail = hailShadowOver(bed) * hailBlockFor(hailSize, sail.porosity)
|
||||
// exposure = hailAt(t) * (1 - coveredHail)
|
||||
// A membrane over the bed protects it fully; a porous cloth over the same bed
|
||||
// protects it fully on an ice night and leaks on a pea-hail one. If those two
|
||||
// came out equal, the choice would be dead — this is the assert that it isn't.
|
||||
const gardenHail = (stormName, porosity) => {
|
||||
const def = storms[stormName];
|
||||
const f = createWindField(def);
|
||||
const size = f.hailSize;
|
||||
let dmg = 0;
|
||||
const COVER = 1; // bed fully under the cloth
|
||||
for (let t = 0; t <= f.duration; t += DT) {
|
||||
const covered = COVER * hailBlockFor(size, porosity);
|
||||
dmg += f.hailAt(t) * (1 - covered) * DT;
|
||||
}
|
||||
return dmg;
|
||||
};
|
||||
// ice night: membrane and shade cloth both fully protect a covered bed
|
||||
const iceMembrane = gardenHail('storm_02b_icenight', 0);
|
||||
const icePorous = gardenHail('storm_02b_icenight', 0.3);
|
||||
assert(Math.abs(iceMembrane - icePorous) < 0.2 && iceMembrane < 0.5,
|
||||
`on the ice night the fabrics should agree at ~0 (both block big ice): ${iceMembrane.toFixed(2)} vs ${icePorous.toFixed(2)}`);
|
||||
// pea-hail night: the covered bed under porous cloth takes real damage
|
||||
const peaMembrane = gardenHail('storm_03_southerly', 0);
|
||||
const peaPorous = gardenHail('storm_03_southerly', 0.3);
|
||||
metrics['fabric.storm03.gardenHail.membrane'] = +peaMembrane.toFixed(2);
|
||||
metrics['fabric.storm03.gardenHail.porous'] = +peaPorous.toFixed(2);
|
||||
assert(peaMembrane < 0.1, 'a membrane over the bed should take ~no pea hail');
|
||||
assert(peaPorous > peaMembrane + 0.3, `porous should leak real pea hail: ${peaPorous.toFixed(2)} vs ${peaMembrane.toFixed(2)}`);
|
||||
});
|
||||
|
||||
return { cases, metrics };
|
||||
}
|
||||
|
||||
|
||||
@ -188,6 +188,34 @@ export function hailBurstEnvelope(dt, ramp, hold, fade, peak) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* What fraction of hail a cloth of this porosity STOPS (0..1). Lane B's fabric
|
||||
* choice (SPRINT7) reads this; the honest answer to their question.
|
||||
*
|
||||
* The ruling first: porosity is about AIR and WATER, 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 at the bottom of the size
|
||||
* range: the finest pea hail IS small enough to rattle through an open weave.
|
||||
* So a solid membrane stops everything, and a porous cloth stops everything
|
||||
* except the smallest stones — which is a real fabric tradeoff without a physics
|
||||
* lie, and it makes porous cost you exactly on the mild-hail nights while staying
|
||||
* honest on the ice nights.
|
||||
*
|
||||
* `size` is in hail.size units (1.0 ≈ a 1.5 cm stone; storms run 0.7 pea → 1.4).
|
||||
* `porosity` matches SailRig's (0 = membrane, ~0.3 = knitted shade cloth).
|
||||
*/
|
||||
export function hailBlockFor(size, porosity = 0) {
|
||||
if (!(porosity > 0)) return 1; // solid membrane stops all ice
|
||||
// Effective aperture of the weave, in size units. A 70%-shade knit (porosity
|
||||
// ~0.3) reads ~0.6 — it leaks only the finest hail; a very open 0.5 weave
|
||||
// reads ~1.0 and lets small stones through too.
|
||||
const aperture = porosity * 2;
|
||||
// A stone well above the gap is stopped dead; one well below sails through;
|
||||
// smoothstep the transition around the aperture.
|
||||
return smoothstep(aperture * 0.5, aperture * 1.5, size);
|
||||
}
|
||||
|
||||
// ---------- the field ----------
|
||||
/**
|
||||
* @param {object} def parsed storm JSON (see data/storms/*.json)
|
||||
|
||||
@ -10,9 +10,12 @@
|
||||
// determinism rule can't be broken by accident.
|
||||
|
||||
import * as THREE from '../vendor/three.module.js';
|
||||
import { createWindField, validateStorm, GUST, RAIN_TIME_COMPRESSION } from './weather.core.js';
|
||||
import {
|
||||
createWindField, validateStorm, GUST, RAIN_TIME_COMPRESSION,
|
||||
hailBlockFor, stormStats, forecastFor,
|
||||
} from './weather.core.js';
|
||||
|
||||
export { GUST, validateStorm, RAIN_TIME_COMPRESSION };
|
||||
export { GUST, validateStorm, RAIN_TIME_COMPRESSION, hailBlockFor, stormStats, forecastFor };
|
||||
|
||||
// Resolved against this module, not the server root: server.py serves the repo
|
||||
// root (so the 2D prototype stays reachable), but the demo bench serves web/.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user