Lane B S14 gate 2.3: converge onto C's pin — their knobs, their guard, my chain

C measured my proposed pin and it could not have failed. Converging rather than
landing a second pin that disagrees. Knobs are now C's:
  site_02_corner_block / storm_02_wildnight / throat (-6,y,0) / t=60.0 / exact ===

Three wrong answers before this, all the same failure:
  1. backyard_01 has venturi: [] — nothing to detect. (I found this one.)
  2. site_02's BED is funnel-blind, Δ 0.0000 m/s. (Both of us, independently.)
  3. t=30 on the wildnight is worth 0.44%. I MISSED THIS ONE: my throat
     measurement read a healthy Δ +4.948 m/s and I trusted it, but that was on
     storm_03b_earlybuster, not the wildnight the pin flies. A sensitivity
     measured on one storm says nothing about another. The mutation check I was
     proud of would have passed on the wrong storm.

Adopted C's vacuity guard and its 25% threshold — one number, not two. It is
the check that would have caught my proposal on its own: mutation asks 'did it
move', the guard asks 'is the funnel worth enough here that a wiring bug could
not hide in the rounding'.

Game side now imports main.js's createWindRouter instead of retyping its two
wiring lines (C's correction). My old version built BOTH sides with
windForSite, so it could only have caught a bad input, never a router that
routed wrong.

CONVERGENCE RECEIPT — my chain reads C's numbers to the decimal:
  t=60 throat  editor 47.38  game 47.38  exact === true  funnel-off 31.60 = 33.3%
  t=30 throat  on 18.55  off 18.46  = 0.44%   (C's finding, reproduced)

What my half adds over C's: C pins windForSite (the builder) against the game;
this pins buildScoringWorld — the path SCORE IT actually runs — against the
same router. Three chains, one number.

Kept the garden bed as a SECOND probe on C's explicit offer: what the sail
shades is what the audit is about, and equality there is a real claim even
though the funnel is not what decides it. Labelled as carrying no funnel
tripwire, because it cannot.

Mutation-checked: setting t back to 30 reds the guard and both tripwires with
the exact diagnostic (0.44%, 18.46 -> 18.55) while the EQUALITY asserts stay
green — which is precisely C's argument, reproduced on my own chain.

Selftest 375 passed / 0 failed / 0 skipped.
This commit is contained in:
type-two 2026-07-18 16:41:36 +10:00
parent 19c39acc8d
commit d946636de1

View File

@ -19,17 +19,22 @@
* *
* The two chains, built independently on purpose * The two chains, built independently on purpose
* *
* GAME loadSite(name) createWorld dress() windForSite(storm, site, * GAME loadSite(name) createWorld dress() **main.js's own
* world.anchors). This is main.js's site-load wiring, verified * `createWindRouter`**, wired by main.js's own two lines
* line-for-line: main.js:511-512 calls setVenturi(siteDef.wind.venturi) * (loadSiteInto: setVenturi then setSheltersFromTrees). Importing the
* then setSheltersFromTrees(anchors.filter(type==='tree')), which is * router rather than retyping those lines is C's correction to my
* what windForSite does and in that order. * first draft, and it matters: my version built BOTH sides with
* `windForSite`, so it could only ever have caught a bad INPUT, never
* a router that routed wrong.
* *
* EDITOR siteClone-shaped object buildScoringWorld() dress() * EDITOR siteClone-shaped object buildScoringWorld() dress()
* windForSite(storm, clone, scoringWorld.anchors). The editor's yard * windForSite(storm, clone, scoringWorld.anchors). The editor's yard
* is an object that was never a file, and its scoring world is built * is an object that was never a file, and its scoring world is built
* fresh (A's page renders on a calm stub whose sway closures must * fresh (A's page renders on a calm stub whose sway closures must
* never reach a score C's landmine 2 wearing a new hat). * never reach a score C's landmine 2 wearing a new hat). This is
* the path SCORE IT actually runs, which is what my half adds over
* C's: C pins the BUILDER against the game, this pins the thing the
* BUTTON runs against the game. Three chains, one number.
* *
* The clone goes through a JSON round-trip because that is what `siteClone()` * The clone goes through a JSON round-trip because that is what `siteClone()`
* and the export actually produce: if canonicalisation ever dropped or * and the export actually produce: if canonicalisation ever dropped or
@ -38,40 +43,55 @@
* is about the two WIND CHAINS, and importing A's page would make lane/b's * is about the two WIND CHAINS, and importing A's page would make lane/b's
* selftest unable to run on lane/b. * selftest unable to run on lane/b.
* *
* WHERE the probe goes, and how two wrong answers got caught * WHERE the probe goes: C's knobs, and the three wrong answers before them
* *
* I proposed `backyard_01`, bed corner, t=30 in THREADS. Then I measured it, * **This pin uses Lane C's knobs, not the ones I proposed.** C co-owns gate
* and the measurement killed the proposal twice over: * 2.3, measured my proposal, and moved all three; converging beats landing a
* second pin that disagrees, so the numbers below are theirs.
* *
* 1. backyard_01 declares `venturi: []`. A funnel-off regression cannot turn * site site_02_corner_block the only yard with a shipped venturi
* that pin red, because there is no funnel to lose. * storm storm_02_wildnight (mine, kept)
* 2. So I moved to site_02 and its BED cannot see the funnel either. The * probe (-6, y, 0) the authored throat centre site geometry,
* throat sits at (-6, 0) with radius 5; the bed's NW corner is (0, 1), * not a magic number; speedAt ignores y
* which is 6.08 m away. Outside. Starving that yard of its venturi moved * second t = 60.0 the alignment plateau, where the funnel is
* the bed probe by exactly 0.000 m/s. * worth about a third of the answer
* match exact `===`
* *
* Both versions PASSED their equality assert and both were worthless. That is * I proposed backyard_01 / bed corner / t=30. All three were wrong, and it is
* the entire failure mode of this repo's four harness bugs, reproduced in an * worth writing down because they are the SAME failure three times:
* assert whose job was to catch it and the only reason it got caught here is
* that the mutation checks were written at the same time as the pin, not after.
* *
* Measured sensitivity on site_02 / earlybuster at t = 30 (funnel on vs off, * 1. backyard_01 declares `venturi: []`. There, "setVenturi called with an
* trees present vs hidden): * empty list" and "setVenturi never called" are the same number the
* regression the pin exists to catch is invisible. (Found while measuring;
* it is why the pin moved to site_02 at all.)
* 2. site_02's BED cannot see the funnel either — the throat's disc doesn't
* reach it. Measured Δ 0.0000 m/s, independently by both of us.
* 3. t = 30 is a second where the wildnight's direction does not line up with
* the gap: C measured the funnel worth 0.4% at the throat there. **This one
* I missed.** My throat measurement read a healthy Δ +4.948 m/s and I
* trusted it but that was on `storm_03b_earlybuster`, not on the
* wildnight this pin flies. A sensitivity measured on one storm says
* nothing about another, and I generalised across exactly that gap. The
* mutation check I was proud of would have passed on the wrong storm.
* *
* probe speed Δ funnel Δ trees * A pin at 0.4% passes with the funnel wired backwards. Hence the vacuity
* bed ( 0, 1) 12.452 +0.000 +0.000 blind, do not pin here * guard below: it re-measures what the funnel is WORTH at the pin and fails
* throat (-6, 0) 15.910 +4.948 +0.000 the funnel pin * under 25% the check that would have caught my proposal on its own.
* near tr1 ( 7,-1) 8.593 +0.000 4.891 the shelter pin
* *
* So the gate asserts EQUALITY at every probe (the bed included it is what * The bed is kept, as a SECOND probe, on C's offer
* the score is about, and equality there is still a real claim), and puts each
* MUTATION at the probe that can actually feel it.
* *
* A finding worth more than the pin, filed to THREADS: the corner block's * C offered to carry the garden bed as a second probe rather than the one, and
* funnel does not reach its garden bed at all. It bites the RIGGING ZONE * I took it: what the sail SHADES is what the audit is ultimately about, and
* which is what the site JSON's own comment says ("q1 is 3.5 m away, inside * equality there is a real claim even though the funnel is not what decides it.
* the radius"). The funnel on that yard is a HARDWARE-LOAD story, not a * It is labelled honestly and carries no funnel tripwire, because it cannot. A
* garden-exposure story. * second probe that measures a different thing is worth having; a first probe
* that measures nothing is not.
*
* A finding worth more than the pin, which C reached independently: the corner
* block's funnel does not reach its garden bed at all. It bites the RIGGING
* ZONE what the site JSON's own comment says ("q1 is 3.5 m away, inside the
* radius"). The funnel on that yard is a HARDWARE-LOAD story, not a
* garden-exposure story, and the score card should be read that way.
* *
* Vacuous-pass guard * Vacuous-pass guard
* *
@ -84,36 +104,43 @@
import * as THREE from '../../web/world/vendor/three.module.js'; import * as THREE from '../../web/world/vendor/three.module.js';
import { createWorld, loadSite } from '../../web/world/js/world.js'; import { createWorld, loadSite } from '../../web/world/js/world.js';
import { loadStorm, windForSite } from '../../web/world/js/weather.js'; import { loadStorm, createWind, windForSite } from '../../web/world/js/weather.js';
// main.js's OWN router, not a retyped copy of its two wiring lines — C's
// correction, and the right call: a pin that retypes the thing it pins agrees
// with itself by construction.
import { createWindRouter } from '../../web/world/js/main.js';
import { buildScoringWorld } from './scorecard.js'; import { buildScoringWorld } from './scorecard.js';
const assert = (cond, msg) => { if (!cond) throw new Error(msg); }; const assert = (cond, msg) => { if (!cond) throw new Error(msg); };
/** /**
* GATE 2.3's pinned knobs. Proposed to C in THREADS 2026-07-18 and open to * GATE 2.3's pinned knobs — Lane C's, adopted. One copy, both lanes, no drift.
* being moved but moved in one place, by both lanes, not drifted. * If either half moves these, it moves them here and says so in THREADS.
*
* second t = 30.0 mid-storm, clear of the attach transient at the front
* (where S13's 12 s settle skew lived) and of any duration edge.
* probes named points per yard. `bed` is derived from `world.gardenBed`
* rather than typed as a literal it is bed geometry, and the wind
* over the bed is what the score is about. `throat` and `tree` are
* the site's own weather furniture, and they are where the mutation
* checks bite (see the header table).
*/ */
export const PIN = { t: 30.0, probeY: 0 }; export const PIN = {
site: 'site_02_corner_block',
storm: 'storm_02_wildnight',
t: 60.0,
probeY: 0,
/** The funnel must be worth at least this much AT THE PIN or the pin is
* decoration. C's threshold, shared rather than re-chosen. */
MIN_FUNNEL_SHARE: 0.25,
};
/** Probe sets per yard. `sensitive` names what each probe is allowed to claim. */ /** Probe sets per yard. Each probe says what it is ALLOWED to claim. */
const PROBES = { const PROBES = {
site_02_corner_block: {
// THE pin: the authored throat centre, where the funnel decides the answer
throat: { x: -6, z: 0 },
// C's offered SECOND probe — what the sail shades. Funnel-blind by
// geometry (Δ 0.0000), so it carries equality only, and says so.
bed: 'gardenBed',
tree: { x: 7, z: -1 }, // tr1 — the shelter half
},
backyard_01: { backyard_01: {
bed: 'gardenBed', bed: 'gardenBed',
tree: { x: -9, z: 2 }, // t1, the gum whose sway is dynamic load tree: { x: -9, z: 2 }, // t1, the gum whose sway is dynamic load
}, },
site_02_corner_block: {
bed: 'gardenBed',
throat: { x: -6, z: 0 }, // the venturi's own centre — Δ +4.948 at t=30
tree: { x: 7, z: -1 }, // tr1 — Δ 4.891 at t=30
},
}; };
const vec = (p) => new THREE.Vector3(p.x, PIN.probeY, p.z); const vec = (p) => new THREE.Vector3(p.x, PIN.probeY, p.z);
@ -121,12 +148,21 @@ const resolveProbes = (name, world) => Object.fromEntries(
Object.entries(PROBES[name]).map(([k, v]) => Object.entries(PROBES[name]).map(([k, v]) =>
[k, vec(v === 'gardenBed' ? { x: world.gardenBed.x, z: world.gardenBed.z } : v)])); [k, vec(v === 'gardenBed' ? { x: world.gardenBed.x, z: world.gardenBed.z } : v)]));
/** The GAME chain: main.js's site load, byte for byte. */ /**
* The GAME chain: main.js's OWN router, wired by main.js's own two lines
* (loadSiteInto `setVenturi(siteDef.wind.venturi)` then
* `setSheltersFromTrees(anchors.filter(type === 'tree'))`).
*/
async function gameChain(siteName, stormDef) { async function gameChain(siteName, stormDef) {
const site = await loadSite(siteName); const site = await loadSite(siteName);
const world = createWorld(new THREE.Scene(), { wind: stubProxy(), site }); const world = createWorld(new THREE.Scene(), { wind: stubProxy(), site });
await world.dress(); await world.dress();
return { site, world, wind: windForSite(stormDef, site, world.anchors) }; const inner = createWind(stormDef);
const router = createWindRouter([inner]);
router.use(inner);
router.setVenturi(site.wind?.venturi ?? []);
router.setSheltersFromTrees(world.anchors.filter((a) => a.type === 'tree'));
return { site, world, wind: router };
} }
/** The EDITOR chain: a siteClone-shaped object through the scoring world. */ /** The EDITOR chain: a siteClone-shaped object through the scoring world. */
@ -153,9 +189,12 @@ export async function buildScorecardTests() {
const tests = []; const tests = [];
// ── the two shipped yards ──────────────────────────────────────────────── // ── the two shipped yards ────────────────────────────────────────────────
// The pinned case first (C's knobs). backyard_01 rides along because
// equality on a funnel-less yard is still a real claim about the clone and
// the dress — it just carries no funnel tripwire, and says so.
const cases = [ const cases = [
{ site: 'backyard_01', storm: 'storm_02_wildnight', funnel: false }, { site: PIN.site, storm: PIN.storm },
{ site: 'site_02_corner_block', storm: 'storm_03b_earlybuster', funnel: true }, { site: 'backyard_01', storm: 'storm_02_wildnight' },
]; ];
const measured = {}; const measured = {};
@ -223,6 +262,25 @@ export async function buildScorecardTests() {
* with two orders of headroom, not a threshold anyone tuned to pass. */ * with two orders of headroom, not a threshold anyone tuned to pass. */
const MIN_MOVE = 1.0; const MIN_MOVE = 1.0;
// ── C's vacuity guard, adopted — threshold and all ───────────────────────
// The strongest of the three checks, and the one that would have caught my
// proposal on its own. A mutation check asks "did the number MOVE"; this asks
// "is the funnel worth ENOUGH here that a wiring bug couldn't hide in the
// rounding". At t=30 on this storm the answer was 0.4%.
tests.push([`GATE 2.3 guard: the funnel is worth ≥${(PIN.MIN_FUNNEL_SHARE * 100).toFixed(0)}% at the pinned probe/second`, () => {
const probe = fun.probes.throat;
const on = baseWind().speedAt(probe, PIN.t);
const off = windForSite(fun.stormDef, { ...fun.editor.site, wind: { venturi: [] } },
fun.editor.built.anchors).speedAt(probe, PIN.t);
const share = (on - off) / on;
assert(Number.isFinite(share) && share >= PIN.MIN_FUNNEL_SHARE,
`gate 2.3 GUARD FAILED: at (${probe.x},${probe.z}) t=${PIN.t} on ${PIN.storm} the funnel is `
+ `worth ${(share * 100).toFixed(2)}% (${off.toFixed(2)}${on.toFixed(2)} m/s), under the `
+ `${(PIN.MIN_FUNNEL_SHARE * 100).toFixed(0)}% floor. The equality pin is now decoration — it `
+ 'would pass with setVenturi deleted. Do NOT lower this floor: re-measure and move the '
+ 'second, the way C moved it off t=30 (0.4%) onto the alignment plateau.');
}]);
tests.push(['gate 2.3 mutation: dropping the venturi MOVES the throat (funnel-off tripwire)', () => { tests.push(['gate 2.3 mutation: dropping the venturi MOVES the throat (funnel-off tripwire)', () => {
const probe = fun.probes.throat; const probe = fun.probes.throat;
const before = baseWind().speedAt(probe, PIN.t); const before = baseWind().speedAt(probe, PIN.t);
@ -233,8 +291,9 @@ export async function buildScorecardTests() {
+ `${(before - after).toFixed(4)} m/s (${before}${after}). This probe cannot SEE the ` + `${(before - after).toFixed(4)} m/s (${before}${after}). This probe cannot SEE the `
+ 'funnel, so the equality pin above would stay green through exactly the funnel-off bug it ' + 'funnel, so the equality pin above would stay green through exactly the funnel-off bug it '
+ 'exists to catch — the one that moved the S13 headline 91.5 → 39.8. This already happened ' + 'exists to catch — the one that moved the S13 headline 91.5 → 39.8. This already happened '
+ 'twice while writing this file (backyard_01 has no funnel; site_02\'s BED is 6.08 m from a ' + 'THREE times on this pin (backyard_01 has no funnel; site_02\'s BED is 6.08 m from a '
+ 'radius-5 throat). Re-measure and move the probe; do not relax the assert.'); + 'radius-5 throat; t=30 on the wildnight is worth 0.4%). Re-measure and move the probe; '
+ 'do not relax the assert.');
}]); }]);
tests.push(['gate 2.3 mutation: hiding the trees MOVES the tree probe (shelter tripwire)', () => { tests.push(['gate 2.3 mutation: hiding the trees MOVES the tree probe (shelter tripwire)', () => {
@ -252,7 +311,7 @@ export async function buildScorecardTests() {
+ 'pin green.'); + 'pin green.');
}]); }]);
tests.push(['gate 2.3: the corner block\'s funnel does NOT reach its garden bed (recorded, not a bug)', () => { tests.push(['gate 2.3 (2nd probe): the corner block\'s funnel does NOT reach its garden bed — recorded, not a bug', () => {
// Not a defect — it is what the site JSON's own comment describes ("q1 is // Not a defect — it is what the site JSON's own comment describes ("q1 is
// 3.5 m away, inside the radius"): the funnel is aimed at the RIGGING // 3.5 m away, inside the radius"): the funnel is aimed at the RIGGING
// ZONE, so it is a hardware-load story rather than a garden-exposure one. // ZONE, so it is a hardware-load story rather than a garden-exposure one.