diff --git a/tools/fabric_bet/probe.html b/tools/fabric_bet/probe.html
new file mode 100644
index 0000000..4dd0b4c
--- /dev/null
+++ b/tools/fabric_bet/probe.html
@@ -0,0 +1,172 @@
+
+
+
+
fabric_bet probe
+
+fabric_bet — the same line, both fabrics, the real chain
+loading…
+
+
+
+
+
diff --git a/tools/site_audit/gardenfly.js b/tools/site_audit/gardenfly.js
index 67890c7..33ccbd8 100644
--- a/tools/site_audit/gardenfly.js
+++ b/tools/site_audit/gardenfly.js
@@ -89,9 +89,17 @@ export function hardwareByName(name) {
* @param {Array} [o.ids] 4 anchor ids; null/omitted = bare bed (the control)
* @param {Array|object} [o.hw] hardware per pick (aligned with ids), or one tier for all 4
* @param {number} [o.tension]
+ * @param {number} [o.porosity] fabric: 0.30 = shade cloth (the default every
+ * audit number to date was flown on — unchanged),
+ * 0 = membrane. SPRINT16 gate 3.1 measures the
+ * fabric bet through THIS chain, same driver as
+ * every other garden number, so cloth-vs-membrane
+ * differ by exactly one variable. [Lane C addition,
+ * THREADS 2026-07-20 — B: contest at integration if
+ * you want a different door.]
* @returns {{ hp, state, byHail, byRain, cornersLost, peaks, marginal, cost }}
*/
-export function flyGarden({ anchors, bed, stormDef, siteDef = null, use = null, ids = null, hw = null, tension = 1.0 }) {
+export function flyGarden({ anchors, bed, stormDef, siteDef = null, use = null, ids = null, hw = null, tension = 1.0, porosity = 0.30 }) {
const wind = windForSite(stormDef, siteDef, anchors);
use?.(wind);
@@ -99,8 +107,9 @@ export function flyGarden({ anchors, bed, stormDef, siteDef = null, use = null,
if (ids) {
const hwArr = Array.isArray(hw) ? hw : Array(4).fill(hw ?? hardwareByName('rated shackle'));
cost = hwArr.reduce((s, h) => s + (h.cost || 0), 0);
- // shade cloth (porosity 0.30): the fabric a competent player flies — same as sweep.js
- rig = new SailRig({ anchors, gridN: 10, porosity: 0.30 }).attach(ids, hwArr, tension);
+ // shade cloth (porosity 0.30) unless the caller is measuring the fabric
+ // bet — same default as sweep.js, so nothing pre-SPRINT16 moves.
+ rig = new SailRig({ anchors, gridN: 10, porosity }).attach(ids, hwArr, tension);
// D's skipped-attach trap: a rig without its shadow mesh scores as a bare
// bed and the number LOOKS plausible. Refuse to fly it.
if (!rig.rigged || !rig.pos || !rig.tris || !rig.tris.length) {
diff --git a/web/world/data/storms/storm_06_soaker.json b/web/world/data/storms/storm_06_soaker.json
new file mode 100644
index 0000000..c36c0bd
--- /dev/null
+++ b/web/world/data/storms/storm_06_soaker.json
@@ -0,0 +1,52 @@
+{
+ "name": "Pea-Hail Soaker",
+ "blurb": "Warm, wet, and rattling — fine hail on and off all night under steady soaking rain. The wind never gets serious. The stones are small enough to matter.",
+ "rating": 3,
+ "seed": 61207,
+ "duration": 90,
+
+ "_why_comment": "SPRINT16 gate 3.1 — the first night the DEFAULT fabric is the wrong answer. Every prior hail night throws stones a 2 mm weave stops dead (storm_02 size 1.3, icenight 1.4 — cloth blocks 100%), so the F key never mattered. This storm's stones are size 0.45: UNDER the cloth aperture band (0.3..0.9 at porosity 0.30), so shade cloth blocks only 15.6% and leaks the rest onto the bed ALL NIGHT; membrane blocks everything. Wind is deliberately mild (sustained 9.2, gusts 15.2 m/s measured) so membrane's higher pressure load is affordable — the night's question is the fabric, per the one-variable rule. MEASURED on site_02 through gardenfly (fabric_bet probe, funnel ON, post-ruling tree, 2026-07-20): bare 17.5 DEAD · best cloth line at any price 42.3 TATTERED (a LOSS — 7.7 under the win line) · the SAME $75 ring on membrane 96.5 FULL · the $60 ring on membrane 68.8 FULL. Same line, one key, Δ54.2 HP. And membrane is NOT a free win: on $20 carabiners it reads 19.1 vs cloth's 24.5 — the doubled load tears cheap steel; bring shackles. This night was measured over BOTH shipped yards and it only works on the corner block: backyard_01's buyable geometry caps hail cover at ~31% (its full-cover quads pond-tear at 3.3+ kN even in this mild wind), so membrane cannot save what the sail cannot reach — the debut yard is site_02, and the pairing note is in THREADS for D.",
+
+ "baseCurve": [[0, 5.0], [15, 6.8], [30, 8.2], [50, 9.2], [70, 8.8], [90, 7.3]],
+
+ "gusts": {
+ "firstAt": 4,
+ "minGap": 6,
+ "maxGap": 11,
+ "powBase": 2.0,
+ "powRand": 3.0,
+ "powRamp": 2.0,
+ "downdraftOfTotal": 0.3
+ },
+
+ "_dir_comment": "A soft change at t=40 — later and gentler than the busters, swinging ~75 deg over 8 s. It keeps the series' one honest tell (the front wall rises in the quarter the new wind comes from) and it re-aims the yard: post-change the wind aligns with site_02's funnel axis, so q1 becomes the hot western corner (4.2 kN on cloth — the rated shackle's slot) while the pond load walks to the new downwind corner q2 (3.2-3.4 kN — the broom's slot). Two hot corners, one rated shackle in the budget: that is the night's rigging question once the F key is answered.",
+
+ "dirCurve": [[0, 0.6], [36, 0.7], [40, 0.4], [48, -0.6], [70, -0.7], [90, -0.65]],
+ "dirWander": { "amp": 0.2, "rate": 0.1 },
+
+ "spatial": { "amp": 0.18, "scale": 11, "advect": 0.5 },
+
+ "events": [
+ { "t": 40, "type": "windchange", "telegraph": 6, "over": 8, "text": "the rain squares around" },
+ { "t": 56, "type": "debris", "model": "BlackTub_v2", "lateral": 2.0, "mass": 4, "text": "a tub floats off the path" },
+ { "t": 66, "type": "lightning", "power": 0.4 }
+ ],
+
+ "_rain_comment": "peakMmPerHour 44 and it rains from the first minute to the last — mean intensity ~0.75, so at decision 10's 40x the night delivers ~3 cm on a flat catch: enough that the bed ring (four 4.0 m posts, near-flat) ponds toward a real corner load on EITHER fabric (ponding reads geometry, not porosity — see THREADS), and the broom stays a tool. First drafts ran 55 then 48: at 55 the pond alone tore the $60 membrane ring's corners and the fabric lesson drowned in a rain lesson — 44 keeps exactly one pond-hot corner on the winning line.",
+
+ "rain": { "peakMmPerHour": 44, "curve": [[0, 0.15], [10, 0.45], [25, 0.75], [40, 0.9], [60, 1.0], [80, 0.85], [90, 0.7]] },
+
+ "_hail_comment": "size 0.45 = true pea hail, the storm's entire thesis (see _why_comment). Four authored bursts spread t=16..79 so the leak is 'all night', not a moment — and NO gust-synced hail on purpose: the stones arrive on the RAIN's schedule, not the wind's, which keeps the mild wind honest and the forecast legible. Weighted hail-seconds 14.9 measured — between the southerly's ~2 and the icenight's ~18 — and TUNED not maxed (the icenight's own lesson): first draft ran 17.2 and read bare 7.2 / best-cloth 33, a wall wearing a bet's clothes; at 14.9 the best cloth line loses by a clear 7.7 HP and the membrane ring wins FULL, which is the storm doing exactly one thing.",
+
+ "hail": {
+ "size": 0.45,
+ "bursts": [
+ { "t": 16, "ramp": 2, "hold": 5, "fade": 3, "intensity": 0.35 },
+ { "t": 33, "ramp": 2, "hold": 6, "fade": 3, "intensity": 0.43 },
+ { "t": 49, "ramp": 2, "hold": 8, "fade": 3, "intensity": 0.52 },
+ { "t": 70, "ramp": 2, "hold": 5, "fade": 4, "intensity": 0.39 }
+ ]
+ },
+
+ "sky": { "darkness": 0.85, "cloudScroll": 0.07, "night": true }
+}