Lane B S16: flyGarden grows a porosity param — the audit engine can fly membrane

Default stays the pinned 0.30 shade cloth (asserted: omitting porosity ===
explicit 0.30 to the digit, hp and every corner peak), so no pinned number
moves. porosity 0 flies genuinely heavier (per-corner strictly greater,
measured 1.23x average on the synthetic funnel yard). C's storm_06 gate-3.1
membrane-vs-cloth pin and B's membrane-balance tables ride this seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 20:25:42 +10:00
parent 5bfd843b77
commit edce5c2758
2 changed files with 51 additions and 3 deletions

View File

@ -89,9 +89,17 @@ export function hardwareByName(name) {
* @param {Array} [o.ids] 4 anchor ids; null/omitted = bare bed (the control) * @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 {Array|object} [o.hw] hardware per pick (aligned with ids), or one tier for all 4
* @param {number} [o.tension] * @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 }} * @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); const wind = windForSite(stormDef, siteDef, anchors);
use?.(wind); use?.(wind);
@ -99,8 +107,7 @@ export function flyGarden({ anchors, bed, stormDef, siteDef = null, use = null,
if (ids) { if (ids) {
const hwArr = Array.isArray(hw) ? hw : Array(4).fill(hw ?? hardwareByName('rated shackle')); const hwArr = Array.isArray(hw) ? hw : Array(4).fill(hw ?? hardwareByName('rated shackle'));
cost = hwArr.reduce((s, h) => s + (h.cost || 0), 0); 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 }).attach(ids, hwArr, tension);
rig = new SailRig({ anchors, gridN: 10, porosity: 0.30 }).attach(ids, hwArr, tension);
// D's skipped-attach trap: a rig without its shadow mesh scores as a bare // 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. // bed and the number LOOKS plausible. Refuse to fly it.
if (!rig.rigged || !rig.pos || !rig.tris || !rig.tris.length) { if (!rig.rigged || !rig.pos || !rig.tris || !rig.tris.length) {

View File

@ -122,6 +122,22 @@ export async function buildGardenflyTests() {
venRun = { bare: fly([]), funnelled: fly(SYN_FUNNEL) }; venRun = { bare: fly([]), funnelled: fly(SYN_FUNNEL) };
} catch (err) { venErr = String((err && err.stack) || err); } } 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 [ return [
['gardenfly: the pinned separation line, flown in BOTH chains — the 12s-skew disagreement, pinned', () => { ['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): // ⚠ 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)`); `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', () => { ['gardenfly: unknown hardware names throw — the shop has no mystery tier', () => {
let threw = false; let threw = false;
try { hardwareByName('titanium dream'); } catch { threw = true; } try { hardwareByName('titanium dream'); } catch { threw = true; }