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
*
* GAME loadSite(name) createWorld dress() windForSite(storm, site,
* world.anchors). This is main.js's site-load wiring, verified
* line-for-line: main.js:511-512 calls setVenturi(siteDef.wind.venturi)
* then setSheltersFromTrees(anchors.filter(type==='tree')), which is
* what windForSite does and in that order.
* GAME loadSite(name) createWorld dress() **main.js's own
* `createWindRouter`**, wired by main.js's own two lines
* (loadSiteInto: setVenturi then setSheltersFromTrees). Importing the
* router rather than retyping those lines is C's correction to my
* 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()
* windForSite(storm, clone, scoringWorld.anchors). The editor's yard
* 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
* 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()`
* 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
* 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,
* and the measurement killed the proposal twice over:
* **This pin uses Lane C's knobs, not the ones I proposed.** C co-owns gate
* 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
* that pin red, because there is no funnel to lose.
* 2. So I moved to site_02 and its BED cannot see the funnel either. The
* throat sits at (-6, 0) with radius 5; the bed's NW corner is (0, 1),
* which is 6.08 m away. Outside. Starving that yard of its venturi moved
* the bed probe by exactly 0.000 m/s.
* site site_02_corner_block the only yard with a shipped venturi
* storm storm_02_wildnight (mine, kept)
* probe (-6, y, 0) the authored throat centre site geometry,
* not a magic number; speedAt ignores y
* second t = 60.0 the alignment plateau, where the funnel is
* worth about a third of the answer
* match exact `===`
*
* Both versions PASSED their equality assert and both were worthless. That is
* the entire failure mode of this repo's four harness bugs, reproduced in an
* 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.
* I proposed backyard_01 / bed corner / t=30. All three were wrong, and it is
* worth writing down because they are the SAME failure three times:
*
* Measured sensitivity on site_02 / earlybuster at t = 30 (funnel on vs off,
* trees present vs hidden):
* 1. backyard_01 declares `venturi: []`. There, "setVenturi called with an
* 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
* bed ( 0, 1) 12.452 +0.000 +0.000 blind, do not pin here
* throat (-6, 0) 15.910 +4.948 +0.000 the funnel pin
* near tr1 ( 7,-1) 8.593 +0.000 4.891 the shelter pin
* A pin at 0.4% passes with the funnel wired backwards. Hence the vacuity
* guard below: it re-measures what the funnel is WORTH at the pin and fails
* under 25% the check that would have caught my proposal on its own.
*
* So the gate asserts EQUALITY at every probe (the bed included it is what
* the score is about, and equality there is still a real claim), and puts each
* MUTATION at the probe that can actually feel it.
* The bed is kept, as a SECOND probe, on C's offer
*
* A finding worth more than the pin, filed to THREADS: the corner block's
* funnel does not reach its garden bed at all. It bites the RIGGING ZONE
* which is 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.
* C offered to carry the garden bed as a second probe rather than the one, and
* I took it: what the sail SHADES is what the audit is ultimately about, and
* equality there is a real claim even though the funnel is not what decides it.
* It is labelled honestly and carries no funnel tripwire, because it cannot. A
* 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
*
@ -84,36 +104,43 @@
import * as THREE from '../../web/world/vendor/three.module.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';
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
* being moved but moved in one place, by both lanes, not drifted.
*
* 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).
* GATE 2.3's pinned knobs — Lane C's, adopted. One copy, both lanes, no drift.
* If either half moves these, it moves them here and says so in THREADS.
*/
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 = {
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: {
bed: 'gardenBed',
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);
@ -121,12 +148,21 @@ const resolveProbes = (name, world) => Object.fromEntries(
Object.entries(PROBES[name]).map(([k, 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) {
const site = await loadSite(siteName);
const world = createWorld(new THREE.Scene(), { wind: stubProxy(), site });
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. */
@ -153,9 +189,12 @@ export async function buildScorecardTests() {
const tests = [];
// ── 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 = [
{ site: 'backyard_01', storm: 'storm_02_wildnight', funnel: false },
{ site: 'site_02_corner_block', storm: 'storm_03b_earlybuster', funnel: true },
{ site: PIN.site, storm: PIN.storm },
{ site: 'backyard_01', storm: 'storm_02_wildnight' },
];
const measured = {};
@ -223,6 +262,25 @@ export async function buildScorecardTests() {
* with two orders of headroom, not a threshold anyone tuned to pass. */
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)', () => {
const probe = fun.probes.throat;
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 `
+ '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 '
+ 'twice while writing this file (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.');
+ 'THREE times on this pin (backyard_01 has no funnel; site_02\'s BED is 6.08 m from a '
+ '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)', () => {
@ -252,7 +311,7 @@ export async function buildScorecardTests() {
+ '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
// 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.