diff --git a/tools/site_audit/gardenfly.js b/tools/site_audit/gardenfly.js index 67890c7..a73193d 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 flown. DEFAULT 0.30 (shade cloth — the + * fabric a competent player flies, same as sweep.js, + * and every pinned number to date was measured on + * it); pass 0 for membrane. SPRINT16: the F key is + * live and storm_06's whole bet is membrane-vs- + * cloth, so the audit engine must be able to fly + * both — C's gate-3.1 pin and B's membrane-balance + * tables ride this parameter. * @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,7 @@ 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); + 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/tools/site_audit/gardenfly.selftest.js b/tools/site_audit/gardenfly.selftest.js index b819372..5b5b248 100644 --- a/tools/site_audit/gardenfly.selftest.js +++ b/tools/site_audit/gardenfly.selftest.js @@ -122,6 +122,22 @@ export async function buildGardenflyTests() { venRun = { bare: fly([]), funnelled: fly(SYN_FUNNEL) }; } catch (err) { venErr = String((err && err.stack) || err); } + // SPRINT16 [B]: flyGarden grew a `porosity` param (storm_06's membrane bet + // needs the audit engine to fly both fabrics). Two things must hold or every + // pinned number in THREADS quietly changes meaning: the DEFAULT is still the + // 0.30 shade cloth every pin was measured on, and membrane genuinely flies + // as a different fabric (double the pressure term → heavier corners). + let fabRun = null, fabErr = null; + try { + const flyFab = (porosity) => flyGarden({ + anchors: SYN_ANCHORS, bed: SYN_BED, stormDef: SYN_STORM, + siteDef: { wind: { venturi: SYN_FUNNEL } }, + ids: ['a1', 'a2', 'a3', 'a4'], hw: hardwareByName('rated shackle'), + ...(porosity === undefined ? {} : { porosity }), + }); + fabRun = { byDefault: flyFab(undefined), cloth: flyFab(0.30), membrane: flyFab(0) }; + } catch (err) { fabErr = String((err && err.stack) || err); } + return [ ['gardenfly: the pinned separation line, flown in BOTH chains — the 12s-skew disagreement, pinned', () => { // ⚠ THE STATE OF PLAY (2026-07-18, [B] THREADS, receipts in the entry): @@ -168,6 +184,31 @@ export async function buildGardenflyTests() { `the siteDef's venturi is not reaching gardenfly's wind (windForSite must receive the SITE def)`); } }], + ['gardenfly: default fabric is still the pinned 0.30 cloth, and membrane flies heavier', () => { + if (fabErr) throw new Error(`fabric flight died: ${fabErr}`); + const { byDefault, cloth, membrane } = fabRun; + // omitting porosity === saying 0.30, to the digit — every pinned number + // in THREADS was measured on the default and must not move under it + assert(byDefault.hp === cloth.hp, `default flew hp ${byDefault.hp} vs explicit cloth ${cloth.hp} — the default fabric drifted`); + for (const d of byDefault.peaks) { + const c = cloth.peaks.find((p) => p.id === d.id); + assert(c && c.peakN === d.peakN, `corner ${d.id}: default peak ${d.peakN} != explicit-cloth ${c?.peakN} — the default fabric drifted`); + } + // porosity 0 must be a genuinely different fabric: the solid membrane + // catches the full pressure term, so every corner flies heavier + for (const m of membrane.peaks) { + const c = cloth.peaks.find((p) => p.id === m.id); + assert(m.peakN > c.peakN, + `corner ${m.id}: membrane peak ${m.peakN} N not above cloth ${c.peakN} N — the porosity param is not reaching the rig`); + } + // Measured 1.23x on this synthetic yard (browser, 2026-07-20) — less + // than the raw 1/0.7 pressure ratio because relative wind bleeds load as + // the heavier-loaded membrane accelerates. Floor at 1.1: the per-corner + // strictly-greater asserts above prove the wiring; this only guards + // against the difference collapsing to epsilon. + const up = membrane.peaks.reduce((s, m) => s + m.peakN / cloth.peaks.find((p) => p.id === m.id).peakN, 0) / 4; + assert(up > 1.1, `membrane only ${up.toFixed(2)}x cloth load on average — porosity is wired but not biting`); + }], ['gardenfly: unknown hardware names throw — the shop has no mystery tier', () => { let threw = false; try { hardwareByName('titanium dream'); } catch { threw = true; }