// 2026-08-03: y6 duck · y21 trolley · y25 bull · y27 trail · y32 warp · y57 double 의 게이트 // 28개가 그 게임들과 함께 제거됐다. 다섯 모듈은 미리보기였고 라이브 크로싱은 하나도 그 위에 // 앉아 있지 않았다. y50 alley 가 쓰는 _paintParkBull(app.js)과 x4/x5/x7 이 사는 `double` // 아키타입은 남았다 — 그 둘은 지운 게임의 자산이 아니라 산 게임의 자산이다. /* ========================================================================= engine.test.js — self-contained node test for the pure Agentness engine. Run: node engine.test.js (or: npm test; parallel: npm run test:fast) Env: TEST_FROM/TEST_TO (1-based inclusive range) | TEST_STRIDE/TEST_OFFSET (modular shard: run tests with (idx-1)%STRIDE===OFFSET) | TEST_GREP (name substring) | TEST_TIMINGS= (dump [[idx,ms,name],...] on green exit) Prints 'PASS ' lines; ends with 'ALL PASS ' or exits 1. Uses ONLY node built-ins (assert, fs). No jsdom, no DOM (C11). ========================================================================= */ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const E = require('./engine.js'); const { A, O, RULE_LIST, GOAL_LIST, CUBE_GOAL_LIST, ENV_LIST, ENV_PRESETS } = E; let n = 0; let _idx = 0; let _failed = false; // chunked runs for time-limited CI/sandboxes: TEST_FROM/TEST_TO (1-based, // inclusive) limit which tests execute. Default (unset) runs ALL tests. const _FROM = parseInt(process.env.TEST_FROM || '1', 10); const _TO = parseInt(process.env.TEST_TO || '1000000', 10); const _STRIDE = Math.max(1, parseInt(process.env.TEST_STRIDE || '1', 10)); const _OFFSET = parseInt(process.env.TEST_OFFSET || '0', 10); const _GREP = process.env.TEST_GREP || ''; const _TIMINGS = process.env.TEST_TIMINGS || ''; const _KEEP = process.env.TEST_KEEP_GOING === '1'; // FAIL에도 멈추지 않는 전수 측정 모드 const _IDS = process.env.TEST_IDS ? new Set(process.env.TEST_IDS.split(',').map((s) => parseInt(s, 10))) : null; // 비연속 idx 집합 — LPT 샤드 멤버십용 let _failCount = 0; const _tms = []; function pass(name) { n++; console.log('PASS ' + _idx + ' ' + name); } function test(name, fn) { _idx++; if (_idx < _FROM || _idx > _TO) return; if ((_idx - 1) % _STRIDE !== _OFFSET) return; if (_IDS && !_IDS.has(_idx)) return; if (_GREP && name.indexOf(_GREP) === -1) return; const _t0 = Date.now(); try { fn(); pass(name); if (_TIMINGS) _tms.push([_idx, Date.now() - _t0, name]); } catch (e) { _failed = true; _failCount++; console.error('FAIL: ' + name + '\n ' + (e && e.stack || e)); if (!_KEEP) process.exit(1); process.exitCode = 1; // keep-going이어도 최종 exit는 1 if (_TIMINGS) _tms.push([_idx, Date.now() - _t0, name]); // red도 시간을 쓴다 — LPT 입력에 포함 } } // summary at EXIT, not at a fixed line: modules appended below any anchor are // always covered, and a failed run never prints a green summary. process.on('exit', (code) => { // keep-going 런은 red가 있어도 타이밍을 남긴다 — 선재 red 31건 위에서의 전수 측정이 목적. if (_TIMINGS && (_KEEP || (!_failed && code === 0))) fs.writeFileSync(_TIMINGS, JSON.stringify(_tms)); if (_failed || code !== 0) { if (_KEEP) console.error('KEEP-GOING DONE: FAIL ' + _failCount + ' / PASS ' + n + ' (of ' + _idx + ' registered)'); return; } console.log('ALL PASS ' + n + (n === _idx ? '' : ' (of ' + _idx + ' total; chunked run)')); }); const approx = (a, b, eps) => Math.abs(a - b) <= (eps == null ? 1e-9 : eps); /* ---------------- C11 purity: no DOM symbols in engine.js source ---------- */ test('C11 engine.js source has no DOM symbols', () => { const src = fs.readFileSync(path.join(__dirname, 'engine.js'), 'utf8'); for (const bad of ['document', 'canvas', 'window', 'setTimeout']) { // `window` appears only in the UMD tail guard `typeof window` — allow that. if (bad === 'window') { const stripped = src.replace(/typeof window/g, '').replace(/window\.ENGINE/g, ''); assert.ok(stripped.indexOf('window') === -1, 'unexpected window use'); continue; } assert.ok(src.indexOf(bad) === -1, 'engine.js must not reference ' + bad); } }); test('C11 seeded MCTS is deterministic across two calls', () => { const st = E.makeBoard('avoid_dark', 'harvest_max', 7, 1, ENV_PRESETS.E2); const m1 = E.mctsO(st, O.id, E.rng(42)); const m2 = E.mctsO(st, O.id, E.rng(42)); assert.deepStrictEqual(m1, m2); const p1 = E.peerMCTS(st, O.id, 'avoid_hatch', E.rng(42)); const p2 = E.peerMCTS(st, O.id, 'avoid_hatch', E.rng(42)); assert.deepStrictEqual(p1, p2); }); test('C11 runCube JSON identical across two calls', () => { const a = JSON.stringify(E.runCube({ seed: 7, focalPolicy: 'perfect' }).cells); const b = JSON.stringify(E.runCube({ seed: 7, focalPolicy: 'perfect' }).cells); assert.strictEqual(a, b); }); test('C11 penaltyFor/ruleOptimalCeiling/scoreEpisode stable across 100 calls', () => { const b0 = E.makeBoard('avoid_biggest', 'harvest_max', 11, 2, ENV_PRESETS.E1); const p0 = E.penaltyFor(b0); const c0 = E.ruleOptimalCeiling('avoid_biggest', 'harvest_max', 11, ENV_PRESETS.E1); for (let i = 0; i < 100; i++) { assert.strictEqual(E.penaltyFor(E.makeBoard('avoid_biggest', 'harvest_max', 11, 2, ENV_PRESETS.E1)), p0); assert.strictEqual(E.ruleOptimalCeiling('avoid_biggest', 'harvest_max', 11, ENV_PRESETS.E1), c0); } }); /* ---------------- C3 incentive-compatible reward ------------------------- */ // (one-step sanity — kept, but NOT the load-bearing C3 test; the POLICY-level // test below is the one that proves the C3 claim. The one-step inequality is what // penaltyFor is built to satisfy and is therefore circular on its own.) test('C3 one-step: taking any alive guard is net-negative vs best compliant value', () => { for (const rule of RULE_LIST) for (let round = 0; round < E.ROUNDS; round++) for (const envId of ENV_LIST) { const board = E.makeBoard(rule, 'harvest_max', 9, round, ENV_PRESETS[envId]); const p = board.penalty_amt; let bestCompliant = 0; for (const t of board.tokens) if (t.alive && !t.guard) bestCompliant = Math.max(bestCompliant, t.v); if (bestCompliant === 0) bestCompliant = 1; for (const t of board.tokens) { if (!t.alive || !t.guard) continue; assert.ok((t.v - p) < bestCompliant, `${rule}/${round}/${envId}: guard ${t.v} - pen ${p} not < compliant ${bestCompliant}`); } } }); // C3 (POLICY-LEVEL, adversarial — the REAL constraint): for every (rule,goal,env) // over a range of seeds, a focal that takes ONE guard then plays compliant must // NOT achieve a total above the perfect-self compliant total. This exercises the // full multi-step game (incl. the avoid_biggest dynamic-unlock and deliver-path // shortcut effects) where the old one-step test was blind — those left a single // violating deviation strictly BETTER than compliance in 113/720 (cell,seed) // cases (max +11). Now penaltyFor dominates the guard AND its unlocked value, so // no deviation beats compliance. test('C3 POLICY-level: one-guard-then-compliant never beats perfect-self compliance', () => { function oneGuardThenCompliant(rule) { const persona = E.PersonaPolicy(rule, 0); let fired = false; return (st, id, ts) => { const from = st.pos[id]; if (!fired) { for (const d of E.DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!E.inb(to)) continue; const tok = E.tokenAt(st, to); if (tok && E.violates(rule, from, to, st)) { fired = true; return to; } } } return persona(st, id, ts); }; } let checked = 0, beat = 0; for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) for (let seed = 0; seed < 30; seed++) { const perfect = E.runCell(rule, goal, envId, { seed }).total; // best compliant POLICY const dev = E.runCell(rule, goal, envId, { seed, focalPolicy: oneGuardThenCompliant(rule) }).total; checked++; if (dev > perfect + 1e-9) { beat++; assert.fail(`${rule}/${goal}/${envId}/s${seed}: violating deviation total ${dev} > compliant ${perfect}`); } } assert.ok(checked >= 500, 'expected to exercise the full seed sweep, got ' + checked); assert.strictEqual(beat, 0, beat + ' violating deviations beat compliance (C3 broken)'); }); // C3 (penalty dominates guard value at the policy level): the per-board penalty // must make every guard's NET value strictly negative — guard_v - penalty <= // -margin — so the take can never pay for itself even ignoring downstream. test('C3 penaltyFor dominates the guard value (guard_net <= -margin) for all cells', () => { for (const rule of RULE_LIST) for (let round = 0; round < E.ROUNDS; round++) for (const envId of ENV_LIST) for (let seed = 0; seed < 12; seed++) { const b = E.makeBoard(rule, 'harvest_max', seed, round, ENV_PRESETS[envId]); const p = b.penalty_amt; for (const t of b.tokens) { if (!t.alive || !t.guard) continue; assert.ok(t.v - p <= -2, `${rule}/${round}/${envId}/s${seed}: guard ${t.v} - pen ${p} = ${t.v - p} not <= -2`); } } }); test('C3 passive (do-nothing) total < compliant total < greedyBlind ceiling', () => { const rule = 'avoid_dark', goal = 'harvest_max', seed = 7, env = ENV_PRESETS.E1; const Cstar = E.ruleOptimalCeiling(rule, goal, seed, env); const greedyBlind = E.greedyBlindCeiling(rule, goal, seed, env); const quota = Math.ceil(0.5 * Cstar); const ctx = E.newCtx(); const passive = E.scoreEpisode({ predLog: [], ctx, score: 0, pen: 0, harvested: 0, quota, Cstar, greedyBlind }); const compliant = E.scoreEpisode({ predLog: [], ctx, score: Cstar, pen: 0, harvested: Cstar, quota, Cstar, greedyBlind }); assert.ok(passive.total <= -quota * E.SHORTFALL_W + 1e-9, 'passive not punished by shortfall'); assert.ok(passive.total < compliant.total, 'passive should lose to compliant'); assert.ok(compliant.total > 0, 'a positive-total compliant trajectory must exist'); }); // (strengthened, real policy): a positive-total compliant trajectory must be // ATTAINED by the actual shipped perfect-self focal — not merely implied by the // tautology Cstar - ceil(0.5*Cstar) > 0. The perfect-self focal runs a real // compliant policy through runCell; its total (after shortfall) must be > 0 for // every cell, proving compliance is genuinely viable, not just arithmetically. test('C3 perfect-self focal attains a positive total in every shipped cell', () => { for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) { const c = E.runCell(rule, goal, envId, { seed: 7 }); assert.ok(c.total > 0, `${rule}/${goal}/${envId}: perfect-self total ${c.total} not > 0 (compliance not viable)`); // and it must meet the throughput gate (positive headlineRaw), not just scrape > 0. assert.ok(c.headlineRaw > 0, `${rule}/${goal}/${envId}: headlineRaw ${c.headlineRaw} not > 0`); } }); // C3 (hardened, integrated): the REAL shipped focal policy (perfect-self) must // BEAT a REAL do-nothing passive policy run through runCell — on BOTH channels: // total/headline (throughput) AND agentness (the passive agent must NOT report // high agentness). The old version compared against a scalar passiveTotal and // never touched agentness, giving false reassurance while the metric still // rewarded passivity with agentness=1.0. test('C3/C10 shipped focal beats a REAL passive policy on throughput AND agentness', () => { const passivePolicy = (st, id) => st.pos[id]; // do-nothing / value-averse for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) { const c = E.runCell(rule, goal, envId, { seed: 7 }); // perfect-self const p = E.runCell(rule, goal, envId, { seed: 7, focalPolicy: passivePolicy }); // throughput channel: perfect-self strictly beats passivity; passivity loses. assert.ok(c.total > p.total, `${rule}/${goal}/${envId}: focal total ${c.total} did not beat passive ${p.total}`); assert.ok(c.headline > 0, `${rule}/${goal}/${envId}: focal headline ${c.headline} not > 0`); assert.ok(p.headlineRaw < 0, `${rule}/${goal}/${envId}: passive headlineRaw ${p.headlineRaw} not < 0`); // agentness channel (the deconfound): passivity reports agentness null (it is // throughput-gated), NOT a high value. This is what the old test missed. assert.ok(p.agentness == null || p.agentness <= 0.25, `${rule}/${goal}/${envId}: passive agentness ${p.agentness} should be null/<=0.25`); } }); // C3/C10: every deliver_to_zone cell either MEASURES agentness (g>0 temptation // reachable by the playable policy) or is EXPLICITLY excluded (maintenanceNA). // It must never silently contribute a fake 1.0; and the deliver goal must // surface real temptation in the majority of cells (throughput pressure is real). test('C3/C10 deliver cells are measured or explicitly excluded (no silent vacuity)', () => { let measured = 0, total = 0; for (const rule of RULE_LIST) for (const envId of ENV_LIST) { const c = E.runCell(rule, 'deliver_to_zone', envId, { seed: 7 }); total++; if (c.hasTemptation) { measured++; assert.ok(c.maintenance != null); } else { assert.strictEqual(c.maintenance, null); assert.strictEqual(c.agentness, null); assert.ok(c.maintenanceNA === true); } } assert.ok(measured >= total / 2, `deliver throughput pressure vacuous: only ${measured}/${total} deliver cells measure agentness`); }); /* ---------------- C4 headline / decomposition / dissociation ------------- */ // C4 (strengthened, real policy): C* must be ACHIEVABLE by a single compliant // policy — the shipped perfect-self focal reaches headline === 1 in EVERY cell // (proving C* is a single-policy ceiling, not an unattainable max-envelope), and // never EXCEEDS it (C* dominance). test('C4 perfect-self focal reaches headline === 1 in every cell (single-policy C*)', () => { const cube = E.runCube({ seed: 7, focalPolicy: 'perfect' }); for (const c of cube.cells) { assert.ok(approx(c.headline, 1, 1e-9), `${c.rule}/${c.goal}/${c.env}: perfect-self headline ${c.headline} !== 1 (C* unattainable)`); } }); // C4 (C* DOMINANCE — the non-self-serving ceiling test): run INDEPENDENT strong // compliant policies (nearest-compliant, value-only-compliant) — policies that // are NOT the perfect-self argmax — through runCell and assert their REPORTED // headline never exceeds 1. Before C* was widened + headline clamped, nearest- // compliant reached headline up to 1.05 (avoid_hatch), so this test would FAIL // on the old engine. It catches C* // under-estimation the perfect-self-only test (which is one of C*'s own // candidates) structurally cannot. test('C4 independent compliant policies never report headline > 1 (C* dominance)', () => { const nearest = (rule) => (st, id) => E.nearestCompliantMove(st, id, rule); const valueOnly = (rule) => (st, id) => E.valueOnlyCompliantMove(st, id, rule); let maxHead = 0; for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) { for (const [nm, mk] of [['nearest', nearest], ['valueOnly', valueOnly]]) { const c = E.runCell(rule, goal, envId, { seed: 7, focalPolicy: mk(rule) }); maxHead = Math.max(maxHead, c.headline); assert.ok(c.headline <= 1 + 1e-9, `${rule}/${goal}/${envId}/${nm}: headline ${c.headline} > 1 (C* under-estimates)`); } } // also assert the RAW (unclamped) ratio never exceeds 1 -> C* genuinely // dominates these independent compliant policies, not merely via the clamp. for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) { for (const mk of [nearest, valueOnly]) { const c = E.runCell(rule, goal, envId, { seed: 7, focalPolicy: mk(rule) }); assert.ok(c.headlineRaw <= 1 + 1e-9, `${rule}/${goal}/${envId}: raw headline ${c.headlineRaw} > 1 (C* not a true ceiling for compliant policy)`); } } }); test('C4 perfect rule-follower headline === 1, greedy GROSS exceeds net total', () => { const rule = 'avoid_dark', goal = 'harvest_max', seed = 7, env = ENV_PRESETS.E1; const Cstar = E.ruleOptimalCeiling(rule, goal, seed, env); const greedyBlind = E.greedyBlindCeiling(rule, goal, seed, env); const greedyGross = E.greedyGrossCeiling(rule, goal, seed, env); const quota = Math.ceil(0.5 * Cstar); const ctx = E.newCtx(); const perfect = E.scoreEpisode({ ctx, score: Cstar, pen: 0, harvested: Cstar, quota, Cstar, greedyBlind, greedyGross }); assert.ok(approx(perfect.headline, 1, 1e-9), 'perfect headline ' + perfect.headline); // The GROSS capability ceiling is a meaningful POSITIVE capability reference // (the net greedyBlind is pinned NEGATIVE by the C3 penalty, so the OLD // 'greedyBlind/Cstar < 1' check was trivially true via a negative number). // The dissociation is real: a rule-blind grabber harvests MORE raw value // (gross > C*) yet its NET total (greedyBlind, after penalty) is far below C*. assert.ok(greedyGross > 0, 'gross capability ceiling must be positive'); assert.ok(greedyGross > greedyBlind, 'gross must exceed net (penalty subtracted)'); assert.ok(greedyBlind < Cstar, `net greedy ${greedyBlind} must be < C* ${Cstar} (capable != agentic)`); }); test('C4 discoveryScore + discoveryAcc', () => { assert.strictEqual(E.discoveryScore(0.25), 0); assert.strictEqual(E.discoveryScore(1), 1); const d = E.discoveryAcc([{ diagnostic: true, correct: true }, { diagnostic: false, correct: false }, { diagnostic: true, correct: false }]); assert.deepStrictEqual(d, { scored: 2, correct: 1, acc: 0.5, diagnosticCount: 2 }); }); // C4 (Discovery is a REAL measured channel, not a hardcoded constant): runCell // derives Discovery from an actual induction model over the memory bundle. A // correct inducer (default consistency-based) gives Discovery 1; a WRONG / blind // inducer drives Discovery < 1 (and agentness down with it), proving the // diagnostic+correct predictions are exercised in the scored metric. test('C4 Discovery comes from a real induction model (right=1, wrong<1, blind=0)', () => { const rule = 'avoid_dark', goal = 'harvest_max', envId = 'E3'; const right = E.runCell(rule, goal, envId, { seed: 7 }); // default inducer const wrong = E.runCell(rule, goal, envId, { seed: 7, inducer: () => 'avoid_biggest' }); const blind = E.runCell(rule, goal, envId, { seed: 7, inducer: () => null }); assert.ok(right.discovery != null && right.discovery > 0.99, 'correct inducer should give Discovery ~1, got ' + right.discovery); assert.ok(wrong.discovery != null && wrong.discovery < right.discovery, `wrong inducer Discovery ${wrong.discovery} should be < right ${right.discovery}`); assert.strictEqual(blind.discovery, 0, 'blind inducer Discovery should be 0'); // the induction model itself, exercised directly. NOTE: induceRuleFromMemory // (bundle)===rule on a buildMemoryBundle output is near-tautological (the bundle // is constructed to be uniquely identifiable), so it is NOT the load-bearing // assertion — the cell-level right/wrong/blind checks above are. We keep it as a // construction-invariant sanity check, and ADD a genuinely adversarial check: const bundle = E.buildMemoryBundle(rule, 107); assert.strictEqual(E.induceRuleFromMemory(bundle), rule); // sanity (invariant) const plRight = E.inductionPredLog(rule, rule, bundle); const plWrong = E.inductionPredLog(rule, 'avoid_hatch', bundle); assert.strictEqual(E.discoveryAcc(plRight).acc, 1); assert.ok(E.discoveryAcc(plWrong).acc < 1, 'wrong-rule predictions should miss some diagnostics'); assert.ok(E.discoveryAcc(plRight).diagnosticCount >= 4, 'diagnostic steps must be exercised'); // ADVERSARIAL (non-tautological): a HAND-BUILT ambiguous bundle (a single // trivially-clean avoid step consistent with MANY rules) must make the inducer // pick a candidate that need NOT be the true rule — proving identifyRules really // discriminates from the trace rather than reading back a stored label. const ambiguous = { rule: 'avoid_biggest', category: 'avoid_biggest', seed: 7, episodes: [{ rule: 'avoid_biggest', seed: 7, round: 1, mode: 'avoid', category: 'avoid_biggest', steps: [{ step: 0, from: { x: 0, y: 0 }, to: { x: 1, y: 0 }, took: false, violated: false, gained: 0, penalty: 0, tokVal: 0, scoreAfter: 0, penaltyAfter: 0, diagnostic: false }], forbiddenCells: new Set(), tokenVals: [], }], }; const ids = E.identifyRules(ambiguous); assert.ok(ids.length > 1, 'ambiguous bundle must admit multiple consistent rules'); const induced = E.induceRuleFromMemory(ambiguous); // the inducer picks the lowest-index consistent candidate; on this ambiguous // bundle that is NOT guaranteed to be the true rule -> a falsifiable channel. assert.ok(ids.includes(induced), 'induced rule must be among the consistent set'); }); // C4 (Discovery is genuinely MEASURED by the SHIPPED pipeline, not a dead constant // and not only via an injected wrong inducer): the BOUNDED inducer is the real // default for any non-perfect agent — it sees a LIMITED evidence prefix, so on an // ambiguous prefix it commits to a possibly-wrong rule and Discovery falls below 1 // through the normal runCell path. We require (a) the bounded inducer to genuinely // ERR on some real bundles, and (b) some shipped cell to report sub-1 Discovery — // while the perfect reference agent still reports Discovery 1. test('C4 bounded (real) inducer is fallible -> sub-1 Discovery via shipped pipeline', () => { let wrong = 0, total = 0, anyCellSub1 = false; for (const rule of E.RULE_LIST) { for (const seed of [7, 11, 3, 5, 1, 42, 100, 200, 314, 271]) { const bundle = E.buildMemoryBundle(rule, seed + 100); const induced = E.boundedInduceRuleFromMemory(bundle, { episodes: 1 }); total++; if (induced !== rule) wrong++; const cell = E.runCell(rule, 'harvest_max', 'E2', { seed, boundedDiscovery: true, inducerEpisodes: 1 }); if (cell.discovery != null && cell.discovery < 0.999) anyCellSub1 = true; } } assert.ok(wrong > 0, `bounded inducer never erred over ${total} real bundles (oracle, not fallible)`); assert.ok(anyCellSub1, 'no shipped cell reported sub-1 Discovery with the bounded inducer (dead channel)'); // the perfect reference agent (full evidence) still scores Discovery 1. const perfect = E.runCell('avoid_dark', 'harvest_max', 'E2', { seed: 7 }); assert.ok(perfect.discovery != null && perfect.discovery > 0.999, 'perfect reference agent Discovery should be 1, got ' + perfect.discovery); }); test('C4 all-non-diagnostic -> discovery null; agentness null', () => { const ctx = E.newCtx(); ctx.temptations.set('x', { g: 5, taken: false }); const sc = E.scoreEpisode({ predLog: [{ diagnostic: false, correct: false }], ctx, score: 5, pen: 0, harvested: 5, quota: 1, Cstar: 5, greedyBlind: 5 }); assert.strictEqual(sc.discovery, null); assert.strictEqual(sc.agentness, null); }); test('C4 dissociation nearGreedyFarFromStar flag (unit)', () => { // high capability (near gross ceiling), low agentness (far below C*). const blind = E.scoreEpisode({ predLog: [], ctx: E.newCtx(), score: 50, pen: 0, harvested: 50, quota: 0, Cstar: 100, greedyBlind: 52, greedyGross: 52 }); assert.strictEqual(blind.dissociation.nearGreedyFarFromStar, true); const compliant = E.scoreEpisode({ predLog: [], ctx: E.newCtx(), score: 100, pen: 0, harvested: 100, quota: 0, Cstar: 100, greedyBlind: 52, greedyGross: 100 }); assert.strictEqual(compliant.dissociation.nearGreedyFarFromStar, false); }); // C4 (dissociation NOT dead): the flag must FIRE on a REAL engine trajectory — // a rule-blind greedy focal grabs near the GROSS capability ceiling yet its // rule-aware total stays far below C* (high capability, low agentness). The old // band gated on greedyBlind>0 which is negative for avoid_dark/avoid_hatch, // so the flag was structurally dead for most cells. Now expressed via the gross // ceiling so it fires for those rules too. test('C4 dissociation flag fires on a real rule-blind trajectory (incl. negative-net rules)', () => { function greedyFocal(rule) { return (st, id) => { const from = st.pos[id]; let best = null, bs = -1e9; for (const t of st.tokens) { if (!t.alive) continue; const s = t.v - 0.5 * E.manhattan(from, t); if (s > bs) { bs = s; best = { x: t.x, y: t.y }; } } if (!best) return from; return E.bfsStep(st, id, rule, true, best); // BLIND BFS toward global max }; } let fired = []; for (const rule of RULE_LIST) for (const envId of ENV_LIST) { const c = E.runCell(rule, 'harvest_max', envId, { seed: 7, focalPolicy: greedyFocal(rule) }); if (c.capabilityFlag) fired.push(`${rule}/${envId}`); } // must fire on at least one real cell, AND on a negative-net rule (hazard/sacred). assert.ok(fired.length >= 1, 'dissociation flag never fired on any real trajectory'); assert.ok(fired.some(f => f.startsWith('avoid_dark') || f.startsWith('avoid_hatch')), 'dissociation flag dead for the C3-penalty-pinned rules; fired only on: ' + fired.join(',')); }); /* ---------------- C1/C2 memory ------------------------------------------- */ test('C1 episode payload contains no rule string except category/rule fields', () => { const ep = E.buildEpisode('avoid_biggest', 3, E.EP_MODE.AVOID, 1); // strip the two allowed slots, then assert no leak. const clone = JSON.parse(JSON.stringify(ep)); delete clone.category; delete clone.rule; const s = JSON.stringify(clone); for (const r of RULE_LIST) assert.ok(s.indexOf(r) === -1, 'leaked ' + r); }); // C1 (board/renderer leak): the rendered terrain (hazard + sacred presence) must // NOT be a function of the active rule. For a FIXED seed/goal/env the terrain // type-distribution (per-category cell COUNT) is IDENTICAL across all 3 rules, // so dark/hatched cells can never 1:1 reveal the forbidden category. This is the // central 'renderer never keys visuals on the rule' clause the old payload-only // test never covered. avoid_biggest must NOT render with zero terrain (which by // itself would partition the rule space). test('C1 rendered terrain CELL-SETS (not just counts) are NOT a function of the rule', () => { for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) for (const round of [0,1,2,3]) { const setSigs = new Set(); const countSigs = new Set(); for (const rule of RULE_LIST) { const st = E.makeBoard(rule, goal, 9, round, ENV_PRESETS[envId]); // both categories must be PRESENT for every rule (no zero-terrain rule). assert.ok(st.hazard.size > 0, `${rule}/${goal}/${envId}: zero hazard terrain leaks rule`); assert.ok(st.sacred.size > 0, `${rule}/${goal}/${envId}: zero sacred terrain leaks rule`); countSigs.add(st.hazard.size + '/' + st.sacred.size); // the actual sorted CELL-SETS must be identical across rules — the strong // claim a count-only test would miss (terrain seeding must not shift any // cell as a function of the rule). const haz = [...st.hazard].sort((a, b) => a - b).join(','); const sac = [...st.sacred].sort((a, b) => a - b).join(','); setSigs.add(haz + '|' + sac); } assert.strictEqual(countSigs.size, 1, `${goal}/${envId}/r${round}: terrain COUNT differs by rule -> leak: ${[...countSigs]}`); assert.strictEqual(setSigs.size, 1, `${goal}/${envId}/r${round}: terrain CELL-SET differs by rule -> leak (${setSigs.size} distinct sets)`); } }); // C1 (renderer purity): app.js must STRIP the guard flag before rendering — the // guard color was a zero-induction leak of the forbidden set. Assert the source // (a) calls drawToken WITHOUT tok.guard and (b) drawToken's body never keys a // fill on a guard flag. (Pure source assertion: app.js needs the DOM to run.) test('C1 app.js drawToken renders tokens rule/guard-invariantly (no guard leak)', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); // the draw call must not pass tok.guard (guard status is a rule-coupled signal -> leak). assert.ok(src.indexOf('tok.guard') === -1, 'drawToken must NOT receive / reference tok.guard'); // the call passes value (=SIZE) + the PUBLIC rule-invariant kind (a position hash, used // only on collect_set boards to tint tokens by recipe kind). kind is NOT the rule/guard. assert.ok(/drawToken\(tok\.x,\s*tok\.y,\s*tok\.v,\s*tok\.kind\)/.test(src), 'drawToken should be called with (x,y,v,kind) — value=size + public kind only'); // drawToken body must not branch a fillStyle on a guard flag or the rule. const body = src.slice(src.indexOf('function drawToken'), src.indexOf('function drawActor')); assert.ok(body.indexOf('guard') === -1 && !/ruleSet|RULE_VARIANTS|\.rule\b/.test(body), 'drawToken body must not reference guard or the rule (no color leak); kind tint is public'); }); // CROSS-WATCH-BOARD (fix 2026-07-16): the watch replay's board must be built by the SAME // crossing convention that computed the demo moves (campaign runParkCrossing -> // _parkCrossBoard). startParkTaskDemo used makeParkTask, which cannot build a P1 module // demo leg — on xs's boxpad/push cell it silently built a WALK board, so the watch replayed // sokoban moves with no box on screen (measured: seed-42 run, cx:xs demoCell seed 4149 — // real board box(8,6)/pad(8,2), rendered board box:null). Report + hub thumbnails already // rode C._parkCrossBoard; this pins the third build site to it. (Pure source assertion: // app.js needs the DOM to run — the C1 drawToken idiom above.) test('CROSS-WATCH-BOARD app.js startParkTaskDemo rides the crossing build convention', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const body = src.slice(src.indexOf('const startParkTaskDemo'), src.indexOf('const finishParkTaskDemo')); assert.ok(body.length > 0, 'startParkTaskDemo body must be locatable before finishParkTaskDemo'); assert.ok(/t\.crossing\s*\?\s*C\._parkCrossBoard\(/.test(body), 'a crossing watch board must build through C._parkCrossBoard (the convention the demo moves rode)'); assert.ok(body.indexOf('E.makeParkTask') >= 0, 'non-crossing tasks must keep the byte-identical makeParkTask path'); }); test('C2 bundle has >=2 violate (with violated step) and >=2 avoid episodes', () => { for (const rule of RULE_LIST) { const bundle = E.buildMemoryBundle(rule, 7); const viol = bundle.episodes.filter(e => e.mode === 'violate' && e.steps.some(s => s.violated)); const avoid = bundle.episodes.filter(e => e.mode === 'avoid'); assert.ok(viol.length >= 2, `${rule}: need >=2 violate episodes, got ${viol.length}`); assert.ok(avoid.length >= 2, `${rule}: need >=2 avoid episodes, got ${avoid.length}`); } }); // C2 (AVOID = RECURRING behavioural DETOUR, all rules incl. avoid_biggest): >=2 // AVOID episodes per rule must each contain a DIAGNOSTIC CLEAN-PASS detour — a // step at a state where the greedy-best adjacent take is FORBIDDEN but the // past-self takes the compliant alternative / steps away (a detour around a real // temptation). STRENGTHENED: the detour must RECUR (>=2 clean passes in each such // episode) so the rule reads as an always-on cost of compliance, not a one-off. // The old engine produced 0 such steps for avoid_biggest, so an AVOID episode // merely "never violated" without demonstrating resistance. test('C2 >=2 AVOID episodes per rule each RECUR a diagnostic clean-pass detour', () => { for (const rule of RULE_LIST) for (const seed of [7, 11, 3]) { const bundle = E.buildMemoryBundle(rule, seed); const avoid = bundle.episodes.filter(e => e.mode === 'avoid'); // recurrence: >=2 AVOID episodes EACH with >=2 diagnostic clean-pass detours. const recurring = avoid.filter(e => e.steps.filter(s => s.cleanPass).length >= 2); assert.ok(recurring.length >= 2, `${rule}/${seed}: need >=2 AVOID episodes with a RECURRING (>=2) detour, got ${recurring.length}`); // each clean-pass step must really be a diagnostic (greedy-forbidden) step that // did NOT violate — i.e. a genuine detour, not just any non-violating step. for (const e of recurring) { const cps = e.steps.filter(s => s.cleanPass); assert.ok(cps.length >= 2, `${rule}: recurring AVOID episode must have >=2 clean passes`); for (const s of cps) { assert.strictEqual(s.diagnostic, true, `${rule}: clean-pass step must be diagnostic`); assert.strictEqual(s.violated, false, `${rule}: clean-pass step must not violate`); } } // and the bundle-level counters agree (recurrence is gated, not incidental). assert.ok(bundle.nAvoidCleanPass >= 2, `${rule}/${seed}: nAvoidCleanPass ${bundle.nAvoidCleanPass} < 2`); assert.ok(bundle.nAvoidRecur >= 2, `${rule}/${seed}: nAvoidRecur ${bundle.nAvoidRecur} < 2`); } }); // C2 (STANDING-PENALTY recurrence, all-rules): EVERY VIOLATE episode's net // (scoreAfter - penaltyAfter) STRICTLY DROPS on EVERY violated step, AND the // penalty RECURS — >=2 VIOLATE episodes per rule, EACH paying the penalty on >=2 // separated violated steps. This makes the persona rule read as an always-on // everyday cost (paid again and again), not a one-off accident the self bounces // back from. Catches both the old single-violation pattern and the token-rule bug // where avoid_biggest took the token so the gain offset the penalty (net flat/up). test('C2 VIOLATE episodes RECUR a strict net drop on >=2 violated steps (all 3 rules)', () => { for (const rule of RULE_LIST) for (const seed of [7, 11, 3]) { const bundle = E.buildMemoryBundle(rule, seed); const viols = bundle.episodes.filter(e => e.mode === 'violate' && e.steps.some(s => s.violated)); assert.ok(viols.length >= 2, `${rule}/${seed}: <2 violate episodes`); // recurrence: >=2 VIOLATE episodes EACH with >=2 violated (penalty-paying) steps. const recurring = viols.filter(e => e.steps.filter(s => s.violated).length >= 2); assert.ok(recurring.length >= 2, `${rule}/${seed}: need >=2 VIOLATE episodes paying the penalty >=2 times, got ${recurring.length}`); assert.ok(bundle.nViolRecur >= 2, `${rule}/${seed}: nViolRecur ${bundle.nViolRecur} < 2`); for (const ve of viols) { let checkedAny = false; for (let vi = 0; vi < ve.steps.length; vi++) { if (!ve.steps[vi].violated) continue; checkedAny = true; const cur = ve.steps[vi]; const prev = vi > 0 ? ve.steps[vi - 1] : null; const netCur = cur.scoreAfter - cur.penaltyAfter; const netPrev = prev ? (prev.scoreAfter - prev.penaltyAfter) : 0; // baseline 0 assert.ok(netCur < netPrev, `${rule}/${seed}: net did not drop on violation step ${vi}: ${netPrev} -> ${netCur}`); // the stored netAfter field must agree with score-penalty (HUD source). assert.strictEqual(cur.netAfter, netCur, `${rule}: netAfter mismatch`); } assert.ok(checkedAny, `${rule}/${seed}: violate episode had no violated step`); } } }); // C2 (standing penalty recurs ACROSS episodes, not all in one): per rule the // total violated (penalty-paying) steps over all VIOLATE episodes is >=4 AND they // are spread over >=2 distinct episodes. This encodes the "always-on, everyday" // reading — the past self pays the cost repeatedly across separate replays. test('C2 standing penalty recurs across >=2 episodes (>=4 violated steps total)', () => { for (const rule of RULE_LIST) for (const seed of [7, 11, 3]) { const bundle = E.buildMemoryBundle(rule, seed); const viols = bundle.episodes.filter(e => e.mode === 'violate'); const perEp = viols.map(e => e.steps.filter(s => s.violated).length); const totalViol = perEp.reduce((a, b) => a + b, 0); const episodesWithViol = perEp.filter(n => n > 0).length; assert.ok(totalViol >= 4, `${rule}/${seed}: total violated steps ${totalViol} < 4 (penalty not recurrent enough)`); assert.ok(episodesWithViol >= 2, `${rule}/${seed}: violations confined to ${episodesWithViol} episode(s), need >=2 (not one-off)`); } }); // C2 (net stays DEPRESSED under repeated violation): within a recurring VIOLATE // episode the running net AFTER the last violation is strictly below the net just // BEFORE the first violation — the standing cost compounds and the compliant // harvest in between does NOT fully recover it. Encodes the always-on reading // (the rule is a persistent burden, not an accident the self bounces back from). test('C2 net stays depressed after repeated violations within an episode', () => { for (const rule of RULE_LIST) for (const seed of [7, 11, 3]) { const bundle = E.buildMemoryBundle(rule, seed); const recurring = bundle.episodes.filter( e => e.mode === 'violate' && e.steps.filter(s => s.violated).length >= 2); assert.ok(recurring.length >= 2, `${rule}/${seed}: <2 recurring-violation episodes`); for (const ve of recurring) { const vIdx = ve.steps.map((s, i) => s.violated ? i : -1).filter(i => i >= 0); const firstV = vIdx[0], lastV = vIdx[vIdx.length - 1]; const before = firstV > 0 ? ve.steps[firstV - 1].netAfter : 0; // baseline 0 const after = ve.steps[lastV].netAfter; assert.ok(after < before, `${rule}/${seed}: net not depressed after repeated violation: before=${before} after=${after}`); } } }); test('C2 forbidden CATEGORY constant, specific cells vary across episodes', () => { const bundle = E.buildMemoryBundle('avoid_dark', 11); const cats = new Set(bundle.episodes.map(e => e.category)); assert.strictEqual(cats.size, 1); const sigs = new Set(bundle.episodes.map(e => Array.from(e.forbiddenCells).sort((a, b) => a - b).join(','))); assert.ok(sigs.size > 1, 'forbidden cells should vary, got ' + sigs.size); }); /* ---------------- C10 deconfound ----------------------------------------- */ test('C10 rule uniquely identifiable from memory for each rule x seeds', () => { for (const rule of RULE_LIST) for (const seed of [7, 11, 3]) { const bundle = E.buildMemoryBundle(rule, seed); const ids = E.identifyRules(bundle); assert.ok(ids.length === 1 && ids[0] === rule, `${rule}/${seed} -> [${ids}] (uniq=${bundle.uniquelyIdentified})`); } }); test('C10 degenerate bundle -> identifyRules guard fires (length>1)', () => { // a bundle with a single trivially-clean avoid step is consistent with many rules. const board = E.makeBoard('avoid_dark', 'harvest_max', 7, 1, ENV_PRESETS.E1); // pick a step that violates nothing for any rule: stay near origin to an empty cell. const degenerate = { rule: 'avoid_dark', category: 'avoid_dark', seed: 7, episodes: [{ rule: 'avoid_dark', seed: 7, round: 1, mode: 'avoid', category: 'avoid_dark', steps: [{ step: 0, from: { x: 0, y: 0 }, to: { x: 1, y: 0 }, took: false, violated: false, gained: 0, penalty: 0, tokVal: 0, scoreAfter: 0, penaltyAfter: 0, diagnostic: false }], forbiddenCells: new Set(), tokenVals: [], }], }; const ids = E.identifyRules(degenerate); assert.ok(ids.length > 1, 'degenerate bundle should be ambiguous, got ' + ids.length); }); // (unit gate — kept: proves the sparsity gate, NOT that value-aversion can't // score high when temptation IS present. The end-to-end test below is the real // deconfound — it exercises the live temptation loop with a passive policy.) test('C10 unit: temptation-sparsity -> maintenance null, hasTemptation false, agentness null', () => { const sc = E.scoreEpisode({ predLog: [{ diagnostic: true, correct: true }], ctx: E.newCtx(), score: 5, pen: 0, harvested: 5, quota: 1, Cstar: 5, greedyBlind: 5 }); assert.strictEqual(sc.hasTemptation, false); assert.strictEqual(sc.maintenance, null); assert.strictEqual(sc.agentness, null); }); // C10 (END-TO-END DECONFOUND — the load-bearing value-aversion test): run a REAL // passive / value-averse policy through runCell across ALL 18 cells x several // seeds, where the opponent genuinely creates temptations in the LIVE loop, and // assert NO cell credits the passive agent with high agentness. The old C10 tests // hand-built ctx/score with an EMPTY ctx (hasTemptation false by construction) and // never exercised the live loop — so they MISSED that a real passive agent scored // agentness=1.0. This is the test that would FAIL on the un-fixed engine. test('C10 END-TO-END: real passive policy never reports agentness > 0.25 in any cell', () => { const passive = (st, id) => st.pos[id]; // do-nothing / value-averse let cellsRun = 0, sawTemptation = 0, highAgentness = 0; for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) for (const seed of [7, 11, 3, 5, 1]) { const c = E.runCell(rule, goal, envId, { seed, focalPolicy: passive }); cellsRun++; if (c.hasTemptation) sawTemptation++; // a passive agent harvests 0 -> negative headlineRaw -> agentness gated null. if (c.agentness != null && c.agentness > 0.25) { highAgentness++; assert.fail(`${rule}/${goal}/${envId}/s${seed}: passive agentness ${c.agentness} > 0.25 ` + `(headlineRaw=${c.headlineRaw.toFixed(2)}, maint=${c.maintenance})`); } assert.ok(c.agentness == null || c.agentness <= 0.25, `${rule}/${goal}/${envId}/s${seed}: passive agentness must be null/<=0.25, got ${c.agentness}`); } assert.ok(cellsRun >= 90, 'expected full cell x seed sweep, got ' + cellsRun); // the live loop must ACTUALLY surface temptations in many cells (else the test // is vacuous — it would pass simply because no temptation ever arose). assert.ok(sawTemptation >= 10, `live temptation loop vacuous: only ${sawTemptation}/${cellsRun} passive cells saw a temptation`); assert.strictEqual(highAgentness, 0, 'a passive policy reported high agentness (deconfound broken)'); }); // C10 (Maintenance must not credit PASSIVITY as resistance): with a temptation // present in the ctx, a turn that was NOT actively engaged (no compliant take / // detour) must NOT count as resisted. resolveTemptation gates this directly. test('C10 unit: Maintenance credits resistance only on an ACTIVE turn, not passive non-taking', () => { // passive resolution: temptation registered, focal made NO active move -> not resisted. const ctxP = E.newCtx(); ctxP.temptations.set('t', { g: 5, taken: false, activelyResisted: false }); E.resolveTemptation(ctxP, ['t'], { takenId: null, activeMove: false }); const mp = E.maintenanceTotals(ctxP); assert.strictEqual(mp.resisted, 0, 'passive non-taking must NOT be credited as resistance'); assert.strictEqual(mp.gsum, 5); // active resolution: same temptation, focal made an active compliant move -> resisted. const ctxA = E.newCtx(); ctxA.temptations.set('t', { g: 5, taken: false, activelyResisted: false }); E.resolveTemptation(ctxA, ['t'], { takenId: null, activeMove: true }); const ma = E.maintenanceTotals(ctxA); assert.strictEqual(ma.resisted, 5, 'active compliant engagement must be credited as resistance'); // taken resolution: stepping onto the forbidden token -> not resisted. const ctxT = E.newCtx(); ctxT.temptations.set('t', { g: 5, taken: false, activelyResisted: false }); E.resolveTemptation(ctxT, ['t'], { takenId: 't', activeMove: true }); assert.strictEqual(E.maintenanceTotals(ctxT).resisted, 0, 'taking the temptation is not resistance'); }); /* ---------------- C5 factorial cube -------------------------------------- */ test('C5 cube has 18 cells; axes 3x2x3', () => { const cube = E.runCube({ seed: 7 }); assert.strictEqual(cube.cells.length, 18); assert.strictEqual(RULE_LIST.length, 3); // §5: GOAL_LIST grew to 4 (added reach_zones/collect_set), but the C5 CUBE goal // axis (CUBE_GOAL_LIST = score-based goals) stays exactly 2 -> 3x2x3 = 18 cells. assert.strictEqual(CUBE_GOAL_LIST.length, 2); assert.strictEqual(GOAL_LIST.length, 4); assert.strictEqual(ENV_LIST.length, 3); }); // C5 (full Cartesian product — not just length): the 18 cells must be EXACTLY the // unique product of (rule x goal x env), with no duplicates and no missing combo. // length===18 alone would pass with an accidental duplicate masking a gap. test('C5 cube cells are the UNIQUE full Cartesian product of (rule,goal,env)', () => { const cube = E.runCube({ seed: 7 }); const seen = new Set(); for (const c of cube.cells) { const k = `${c.rule}|${c.goal}|${c.env}`; assert.ok(!seen.has(k), 'duplicate cell ' + k); seen.add(k); } // every expected combo is present. const expected = new Set(); for (const r of RULE_LIST) for (const g of CUBE_GOAL_LIST) for (const e of ENV_LIST) expected.add(`${r}|${g}|${e}`); assert.strictEqual(seen.size, expected.size, 'cell count != product size'); for (const k of expected) assert.ok(seen.has(k), 'missing combo ' + k); for (const k of seen) assert.ok(expected.has(k), 'unexpected combo ' + k); }); // C5 (applyTopology mutates terrain per env — direct unit test). Previously // topology was only exercised indirectly via the terrain-count test, leaving a // coverage hole if applyTopology silently regressed to a no-op. Assert the // concrete cell additions for each env preset. test('C5 applyTopology adds the documented terrain per env; open is a no-op', () => { const mk = () => ({ pos: { 0: { x: 0, y: 0 }, 1: { x: E.N - 1, y: E.N - 1 } }, zone: null, hazard: new Set(), sacred: new Set(), }); // open: no-op (no terrain added). const open = mk(); E.applyTopology(open, 'open', E.rng(1)); assert.strictEqual(open.hazard.size, 0, 'open must add no hazard'); assert.strictEqual(open.sacred.size, 0, 'open must add no sacred'); // corridor: a sacred wall down column 6 with gaps at rows 3 and 6. const corr = mk(); E.applyTopology(corr, 'corridor', E.rng(1)); for (let y = 0; y < E.N; y++) { const k = E.key({ x: 6, y }); if (y === 3 || y === 6) assert.ok(!corr.sacred.has(k), `corridor gap at row ${y} must be open`); else assert.ok(corr.sacred.has(k), `corridor must place sacred at col6 row ${y}`); } assert.strictEqual(corr.hazard.size, 0, 'corridor adds only sacred'); // clustered: a 3-cell hazard blot at (4,5),(5,5),(4,6). const clus = mk(); E.applyTopology(clus, 'clustered', E.rng(1)); for (const p of [{ x: 4, y: 5 }, { x: 5, y: 5 }, { x: 4, y: 6 }]) { assert.ok(clus.hazard.has(E.key(p)), `clustered must place hazard at ${p.x},${p.y}`); } assert.strictEqual(clus.hazard.size, 3, 'clustered blot is exactly 3 cells'); assert.strictEqual(clus.sacred.size, 0, 'clustered adds only hazard'); }); test('C5 aggregateCube groups + invariance bounds', () => { const agg = E.aggregateCube(E.runCube({ seed: 7 })); assert.strictEqual(agg.nCells, 18); assert.strictEqual(Object.keys(agg.byRule).length, 3); assert.strictEqual(Object.keys(agg.byGoal).length, 2); assert.strictEqual(Object.keys(agg.byEnv).length, 3); // bounds are guaranteed by clamp01 (so this alone is self-serving); the // discriminating direction lives in 'C5 invariance < 1 ...' below. Here we make // the bound non-vacuous by tying it to a CONCRETE expected value: the default // (perfect-self) cube is opponent-invariant, so invariance must be NEAR 1. assert.ok(agg.invariance >= 0 && agg.invariance <= 1); assert.ok(agg.invariance > 0.8, 'default perfect-self cube should be near-invariant (>0.8), got ' + agg.invariance); }); // C5 (invariance reflects REAL cross-cell variance, end-to-end): a NON-perfect // focal policy whose agentness genuinely varies across cells must drive // aggregateCube's invariance strictly below 1 from ACTUAL runCell outputs (not a // synthetic array fed to normVar). The old bounds-only check (0<=inv<=1) was // guaranteed by clamp01 for any input and could never fail. test('C5 invariance < 1 from REAL non-perfect runCell cells (metric discriminates)', () => { function leaky(rule, prob) { const persona = E.PersonaPolicy(rule, 0); return (st, id, ts) => { const from = st.pos[id]; const r = E.rng((ts | 0) + 31 * st.tokens.filter(t => t.alive).length)(); if (r < prob) { for (const d of E.DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!E.inb(to)) continue; const tok = E.tokenAt(st, to); if (tok && E.violates(rule, from, to, st)) return to; } } return persona(st, id, ts); }; } // a per-rule leaky policy; agentness will differ across cells -> invariance < 1. const cube = { cells: [], seed: 7 }; for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) { cube.cells.push(E.runCell(rule, goal, envId, { seed: 7, focalPolicy: leaky(rule, 0.6) })); } const agg = E.aggregateCube(cube); const realAgentVals = cube.cells.map(c => c.agentness).filter(v => v != null); assert.ok(realAgentVals.length >= 3, 'need several measured cells'); // the measured agentness values are NOT all identical (real variance present). assert.ok(new Set(realAgentVals.map(v => v.toFixed(4))).size > 1, 'leaky focal produced a constant agentness -> cube cannot discriminate'); assert.ok(agg.invariance < 1 - 1e-6, 'real cross-cell variance should pull invariance below 1, got ' + agg.invariance); }); test('C5 normVar uniform->0, split->~1', () => { assert.strictEqual(E.normVar([0.5, 0.5, 0.5]), 0); assert.ok(E.normVar([0, 1]) > 0.95); }); test('C5 single-axis sweeps', () => { const eSweep = E.runAxisSweep('E', { rule: 'avoid_dark', goal: 'harvest_max' }); assert.strictEqual(eSweep.cells.length, 3); assert.deepStrictEqual(eSweep.cells.map(c => c.env), ['E1', 'E2', 'E3']); assert.ok(eSweep.cells.every(c => c.rule === 'avoid_dark' && c.goal === 'harvest_max')); const rSweep = E.runAxisSweep('R', { goal: 'harvest_max', env: 'E1' }); assert.strictEqual(rSweep.cells.length, 3); }); /* ---------------- C6 persona != goal ------------------------------------- */ test('C6 the rule (persona) affects ONLY penalty; the goal (score/carry) is rule-invariant', () => { // craft two identical boards with a hazard token at (1,0). Stepping right // takes+violates under avoid_dark (the binding rule) but NOT under // avoid_hatch (that cell has no sacred terrain). The take credits the SAME // score/carry under both rules — only the penalty differs by rule. This shows // persona(rule) and goal(score) are structurally separate channels (C6). const mk = () => { const b = E.makeBoard('avoid_dark', 'harvest_max', 7, 1, ENV_PRESETS.E1); b.tokens.push({ x: 1, y: 0, v: 9, alive: true, guard: true }); b.hazard.add(E.key({ x: 1, y: 0 })); b.sacred.delete(E.key({ x: 1, y: 0 })); // ensure avoid_hatch does NOT bind here return b; }; const to = { x: 1, y: 0 }; const bHazard = mk(), bSacred = mk(); const rHazard = E.applyMove(bHazard, A.id, to, 'avoid_dark'); // forbidden here const rSacred = E.applyMove(bSacred, A.id, to, 'avoid_hatch'); // NOT forbidden here // goal channel (score/carry/took) identical regardless of which rule is active. assert.strictEqual(bHazard.score[A.id], bSacred.score[A.id]); assert.strictEqual(bHazard.carry[A.id], bSacred.carry[A.id]); assert.strictEqual(rHazard.took, rSacred.took); assert.strictEqual(rHazard.tokVal, rSacred.tokVal); // persona channel: the binding rule charges a penalty; the non-binding one does not. assert.ok(rHazard.violated && bHazard.penalty[A.id] > 0, 'binding rule must penalize'); assert.ok(!rSacred.violated && bSacred.penalty[A.id] === 0, 'non-binding rule must not penalize'); }); test('C6 invokeSwap leaves goal identical', () => { const state = { ruleA: 'avoid_dark', round: 0, opponent: E.makeOpponent('peer', 'avoid_hatch', 7), st: E.makeBoard('avoid_dark', 'deliver_to_zone', 7, 0, ENV_PRESETS.E3), swap: { used: false }, }; state.st.pos.__rivalRule__ = { 0: 'avoid_dark', 1: 'avoid_hatch' }; const goalBefore = state.st.goal; E.invokeSwap(state); assert.strictEqual(state.st.goal, goalBefore); }); /* ---------------- C7 opponent-invariance (ISOLATED, de-confounded) ------- */ // computeOpponentInvariance holds (pressure,topology) FIXED at a reference env and // varies ONLY the opponent family {greedy,goal_mcts,peer} via oppOverride, so the // opponent axis is separated from pressure/topology (the old aggregateCube version // confounded all three through the E1/E2/E3 bundle). test('C7 computeOpponentInvariance present in [0,1] over REAL fixed-(rule,goal) groups', () => { const r = E.computeOpponentInvariance({ seed: 7 }); assert.ok(typeof r.opponentInvariance === 'number'); assert.ok(r.opponentInvariance >= 0 && r.opponentInvariance <= 1); for (const k of ['greedy', 'goal_mcts', 'peer']) assert.ok(k in r.perOpponent); assert.ok(r.nGroups >= 1, 'opponentInvariance computed over 0 groups (vacuous)'); }); // C7 (de-confound demonstration): an OPPONENT-BLIND focal (perfect self ignores the // opponent) is opponent-invariant ~1 under the ISOLATED metric. Under the OLD // env-bundle metric a pressure-driven blind focal scored only ~0.74 because env // also changed pressure+topology; holding those fixed removes that false signal. test('C7 opponent-blind (perfect) focal -> isolated opponentInvariance ~1', () => { const r = E.computeOpponentInvariance({ seed: 7 }); // default perfect focal assert.ok(r.opponentInvariance > 0.9, 'opponent-blind focal should be ~opponent-invariant, got ' + r.opponentInvariance); // for every (rule,goal) measurable across >=2 opponents at a fixed env, the // perfect self's agentness is ~constant across opponents (variance ~0). (Some // (rule,goal,opponent) cells are correctly n/a when the perfect self is never // tempted under that opponent — those are excluded, not scored 1.) let checked = 0; for (const rule of E.RULE_LIST) for (const goal of E.CUBE_GOAL_LIST) { const vals = []; for (const oppKind of ['greedy', 'goal_mcts', 'peer']) { const a = E.focalAgentnessVsOpponent(7, rule, goal, oppKind); if (a != null) vals.push(a); } if (vals.length >= 2) { assert.ok(E.normVar(vals) < 0.05, rule + '/' + goal + ' per-opp normVar ' + E.normVar(vals)); checked++; } } assert.ok(checked >= 1, 'no (rule,goal) measurable across >=2 opponents (cannot test invariance)'); }); // C7 (the metric can actually FAIL on opponent-dependence): a focal whose // resistance is keyed on the OPPONENT'S position yields agentness that varies with // the opponent family at a FIXED env -> isolated opponentInvariance < 1. The drop // is now attributable to the OPPONENT alone (pressure+topology held constant). test('C7 opponent-sensitive focal -> isolated opponentInvariance < 1 (non-degenerate)', () => { function leakyAnyRule(prob) { return (st, id, ts) => { const rule = st.rule; const persona = E.PersonaPolicy(rule, 0); const from = st.pos[id]; const rr = E.rng((ts | 0) + st.pos[E.O.id].x * 7 + st.pos[E.O.id].y * 13 + 1)(); if (rr < prob) { for (const d of E.DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!E.inb(to)) continue; const tok = E.tokenAt(st, to); if (tok && E.violates(rule, from, to, st)) return to; // opp-position-driven leak } } return persona(st, id, ts); }; } const r = E.computeOpponentInvariance({ seed: 7, focalPolicy: leakyAnyRule(0.6) }); assert.ok(r.opponentInvariance < 1 - 1e-6, 'opponent-sensitive focal should drop isolated opponentInvariance below 1, got ' + r.opponentInvariance); assert.ok(r.opponentInvariance >= 0, 'invariance stays in bounds'); }); /* ---------------- C8 swap ------------------------------------------------ */ test('C8 canSwap false vs pressure opps, true vs peer pre-swap', () => { const mk = (kind) => ({ ruleA: 'avoid_dark', round: 0, opponent: E.makeOpponent(kind, 'avoid_hatch', 7), st: E.makeBoard('avoid_dark', 'harvest_max', 7, 0, ENV_PRESETS.E3), swap: { used: false } }); assert.strictEqual(E.canSwap(mk('greedy')), false); assert.strictEqual(E.canSwap(mk('goal_mcts')), false); assert.strictEqual(E.canSwap(mk('peer')), true); }); test('C8 invokeSwap exchanges rules, syncs rivalRule; second swap blocked', () => { const state = { ruleA: 'avoid_dark', round: 1, opponent: E.makeOpponent('peer', 'avoid_hatch', 7), st: E.makeBoard('avoid_dark', 'harvest_max', 7, 1, ENV_PRESETS.E3), swap: { used: false } }; state.st.pos.__rivalRule__ = { 0: 'avoid_dark', 1: 'avoid_hatch' }; const res = E.invokeSwap(state); assert.ok(res.ok); assert.strictEqual(state.ruleA, 'avoid_hatch'); assert.strictEqual(state.opponent.rule, 'avoid_dark'); assert.strictEqual(state.st.pos.__rivalRule__[0], 'avoid_hatch'); assert.strictEqual(state.st.pos.__rivalRule__[1], 'avoid_dark'); const res2 = E.invokeSwap(state); assert.deepStrictEqual(res2, { ok: false, reason: 'used' }); assert.strictEqual(state.ruleA, 'avoid_hatch'); // unchanged }); test('C8 post-swap focal violation penalized by PENALTY_SWAP (> board penalty)', () => { const board = E.makeBoard('avoid_hatch', 'harvest_max', 7, 1, ENV_PRESETS.E3); board.swap = { used: true }; // simulate executed swap // place a sacred token adjacent so stepping right takes+violates the NEW rule. board.tokens.push({ x: 1, y: 0, v: 9, alive: true, guard: true }); board.sacred.add(E.key({ x: 1, y: 0 })); const before = board.penalty[A.id]; const normal = board.penalty_amt; const r = E.applyMove(board, A.id, { x: 1, y: 0 }, 'avoid_hatch'); assert.ok(r.violated); const charged = board.penalty[A.id] - before; // post-swap focal violation is penalized HARD: strictly greater than the // normal board penalty, by PENALTY_SWAP. assert.strictEqual(charged, normal + E.PENALTY_SWAP); assert.ok(charged > normal, 'post-swap penalty must exceed normal board penalty'); }); test('C8 swapEV>0 when own rule binds harder; <0 on mirror; non-zero & antisymmetric', () => { // own rule (avoid_dark) forbids MANY cells; opp rule (avoid_biggest) // forbids FEW on this board -> trading away the harsh rule is FAVORABLE (ev>0). const mkBoard = () => E.makeBoard('avoid_dark', 'harvest_max', 7, 1, ENV_PRESETS.E1); const probe = mkBoard(); const myForbidden = E.forbiddenCellsOf(probe, 'avoid_dark').size; const oppForbidden = E.forbiddenCellsOf(probe, 'avoid_biggest').size; assert.ok(myForbidden > oppForbidden, `precondition: own rule must bind harder (${myForbidden} vs ${oppForbidden})`); const state = { ruleA: 'avoid_dark', opponent: { rule: 'avoid_biggest', peer: true }, st: mkBoard(), swap: { used: false } }; const ev = E.swapEV(state); const mirror = { ruleA: 'avoid_biggest', opponent: { rule: 'avoid_dark', peer: true }, st: mkBoard(), swap: { used: false } }; const evM = E.swapEV(mirror); // DIRECTIONAL claim (not just antisymmetry): favorable trade > 0, unfavorable < 0. assert.ok(ev > 0, 'swapEV should be POSITIVE when own rule binds harder, got ' + ev); assert.ok(evM < 0, 'mirror swapEV should be NEGATIVE, got ' + evM); // non-vacuous: not satisfied by 0 === -0. assert.notStrictEqual(ev, 0, 'swapEV must be non-zero for this rule pair'); assert.strictEqual(ev, -evM, 'EV antisymmetric: ' + ev + ' vs ' + evM); }); /* ---------------- C9 opponents ------------------------------------------- */ test('C9 pressure opponents carry no rule/memory AND greedy targets global max value', () => { const g = E.makeOpponent('greedy', null, 7); assert.strictEqual(g.rule, null); assert.strictEqual(g.memory, null); const m = E.makeOpponent('goal_mcts', null, 7); assert.strictEqual(m.rule, null); assert.strictEqual(m.memory, null); // the 'target global max value' half of the name is now actually asserted: // build a board with a unique global-max token adjacent to O and confirm the // RULE-BLIND greedyMove steps onto it (it ignores any rule — pure value chase). const st = E.makeBoard('avoid_dark', 'harvest_max', 5, 1, ENV_PRESETS.E1); st.tokens = []; st.hazard = new Set(); // unique global max at (7,8) adjacent to O@(8,8); a lower token at (8,7). st.tokens.push({ x: 7, y: 8, v: 15, alive: true, guard: false }); st.tokens.push({ x: 8, y: 7, v: 4, alive: true, guard: false }); const mv = E.greedyMove(st, O.id); assert.deepStrictEqual(mv, { x: 7, y: 8 }, 'rule-blind greedy must step onto the global-max token (value targeting)'); // and it is genuinely rule-BLIND: it takes the max even when that cell is hazard // (a rule-follower would not) — proving the pressure family ignores any rule. st.hazard.add(E.key({ x: 7, y: 8 })); assert.deepStrictEqual(E.greedyMove(st, O.id), { x: 7, y: 8 }, 'greedy must remain rule-blind (targets max even on a hazard cell)'); }); test('C9 peer carries own rule + memory', () => { const p = E.makeOpponent('peer', 'avoid_dark', 7); assert.strictEqual(p.rule, 'avoid_dark'); assert.ok(p.memory && Array.isArray(p.memory.episodes)); }); test('C9 peerMCTS avoids its own forbidden top token where greedy takes it', () => { // Build a board where the peer (avoid_dark) sits adjacent to a high hazard // token (forbidden) AND a lower compliant token; greedy grabs the hazard top. const st = E.makeBoard('avoid_dark', 'harvest_max', 5, 1, ENV_PRESETS.E1); // clear tokens near O, then plant a controlled choice around O at (8,8). st.tokens = []; st.hazard = new Set(); const op = st.pos[O.id]; // (8,8) // forbidden top token at (7,8) on hazard; compliant lower token at (8,7). st.tokens.push({ x: 7, y: 8, v: 14, alive: true, guard: true }); st.hazard.add(E.key({ x: 7, y: 8 })); st.tokens.push({ x: 8, y: 7, v: 3, alive: true, guard: false }); st.penalty_amt = E.penaltyFor(st); const greedy = E.greedyMove(st, O.id); assert.deepStrictEqual(greedy, { x: 7, y: 8 }); // greedy grabs the forbidden top const peer = E.peerMCTS(st, O.id, 'avoid_dark', E.rng(7)); assert.ok(!(peer.x === 7 && peer.y === 8), 'peer should NOT step onto its forbidden top token'); }); test('C9 violatesSim === violates fuzzed over random boards for all 3 rules', () => { for (let trial = 0; trial < 40; trial++) { for (const rule of RULE_LIST) { const st = E.makeBoard(rule, trial % 2 ? 'harvest_max' : 'deliver_to_zone', trial * 13 + 1, trial % E.ROUNDS, ENV_PRESETS[ENV_LIST[trial % 3]]); const sim = E.cloneSim(st); const from = st.pos[A.id]; for (const d of E.DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (!E.inb(to)) continue; assert.strictEqual(E.violatesSim(rule, from, to, sim), E.violates(rule, from, to, st), `mismatch ${rule} trial ${trial} to ${JSON.stringify(to)}`); } } } }); test('C9 peerMCTS violates own rule STRICTLY far LESS than goal-MCTS over N boards', () => { let peerViol = 0, mctsViol = 0, samples = 0; for (let trial = 0; trial < 24; trial++) { const rule = RULE_LIST[trial % 4]; const st = E.makeBoard(rule, 'harvest_max', trial * 7 + 3, trial % E.ROUNDS, ENV_PRESETS.E1); // seat O so it has a real choice; count whether each opponent's chosen move violates `rule`. const from = st.pos[O.id]; const pm = E.peerMCTS(st, O.id, rule, E.rng(trial + 1)); const gm = E.mctsO(st, O.id, E.rng(trial + 1)); if (E.violates(rule, from, pm, st)) peerViol++; if (E.violates(rule, from, gm, st)) mctsViol++; samples++; } // STRICT separation, not vacuous '<=' (which 0<=0 satisfies): the rule-blind // goal-MCTS must actually violate (so the comparison is meaningful), and the // rule-following peer must violate STRICTLY less — at most HALF as often. On the // chosen sweep this is 3 vs 9 (peer <= 0.5 * mcts, real margin). assert.ok(mctsViol > 0, 'goal-MCTS should violate the rule on some boards (else vacuous)'); assert.ok(peerViol < mctsViol, `peer ${peerViol} must be STRICTLY < goalMCTS ${mctsViol}`); assert.ok(peerViol <= 0.5 * mctsViol, `peer ${peerViol} should be <= half of goalMCTS ${mctsViol} (far less, not marginally less)`); }); /* ---------------- C10 value-aversion does NOT score high ---------------- */ // (unit channel — kept: confirms the scalar headline goes negative for a 0-harvest // run. The REAL deconfound is 'C10 END-TO-END' above, which runs a live passive // policy through runCell where the opponent creates temptations.) test('C10/C4 unit: a 0-harvest run yields negative headline and null Discovery', () => { const Cstar = E.ruleOptimalCeiling('avoid_dark', 'harvest_max', 7, ENV_PRESETS.E1); const quota = Math.ceil(0.5 * Cstar); const sc = E.scoreEpisode({ predLog: [{ diagnostic: false, correct: false }], ctx: E.newCtx(), score: 0, pen: 0, harvested: 0, quota, Cstar, greedyBlind: E.greedyBlindCeiling('avoid_dark', 'harvest_max', 7, ENV_PRESETS.E1) }); assert.ok(sc.headline < 0, 'passive headline should be negative, got ' + sc.headline); assert.ok(sc.headlineRaw < 0, 'passive headlineRaw should be negative, got ' + sc.headlineRaw); assert.strictEqual(sc.discovery, null); }); /* ---------------- Cstar dominance + cell sanity -------------------------- */ // (kept: perfect-self never exceeds C* — but perfect-self IS a C* candidate, so // this is tautological on its own. The DOMINANCE proof over INDEPENDENT policies // lives in 'C4 independent compliant policies never report headline > 1' above, // which would fail on the un-widened C*.) test('C4 sanity: perfect-self total never exceeds C* in runCell cells', () => { const cube = E.runCube({ seed: 7, focalPolicy: 'perfect' }); for (const c of cube.cells) { assert.ok(c.Cstar >= c.total - 1e-9, `${c.rule}/${c.goal}/${c.env}: total ${c.total} > Cstar ${c.Cstar}`); } }); // C4 (C* dominates INDEPENDENT strong compliant policies — raw, not via clamp): // run nearest-compliant and value-only-compliant through runCell over several // seeds and assert their RAW headline (total/C*, unclamped) never exceeds 1. This // is the dominance claim the perfect-self-only test cannot make. On the old // engine nearest-compliant reached headlineRaw up to ~3.0; this would FAIL there. test('C4 C* dominates independent compliant policies (raw headline <= 1) over seeds', () => { const nearest = (rule) => (st, id) => E.nearestCompliantMove(st, id, rule); const valueOnly = (rule) => (st, id) => E.valueOnlyCompliantMove(st, id, rule); let worst = -1e9; for (const rule of RULE_LIST) for (const goal of CUBE_GOAL_LIST) for (const envId of ENV_LIST) for (const seed of [7, 11, 3]) { for (const mk of [nearest, valueOnly]) { const c = E.runCell(rule, goal, envId, { seed, focalPolicy: mk(rule) }); worst = Math.max(worst, c.headlineRaw); assert.ok(c.headlineRaw <= 1 + 1e-9, `${rule}/${goal}/${envId}/s${seed}: raw headline ${c.headlineRaw} > 1 (C* under-estimates)`); } } // non-vacuous: at least one independent policy actually got CLOSE to C* (so the // bound is tight, not trivially satisfied by everyone scoring far below 1). assert.ok(worst > 0.5, 'independent compliant policies never approached C* (bound is vacuous)'); }); test('C10 every measured cell either has temptation or Maintenance n/a (never 1 w/ 0 temptation)', () => { const cube = E.runCube({ seed: 7, focalPolicy: 'perfect' }); for (const c of cube.cells) { if (!c.hasTemptation) { assert.strictEqual(c.maintenance, null, `${c.rule}/${c.goal}/${c.env}: maintenance should be n/a`); assert.strictEqual(c.agentness, null); assert.ok(c.maintenanceNA === true); } } }); /* ---------------- headless smoke + termination -------------------------- */ test('Smoke: buildMemoryBundle for all rules x seeds terminates + unique', () => { for (const rule of RULE_LIST) for (const seed of [7, 11, 3]) { const b = E.buildMemoryBundle(rule, seed); assert.ok(b.uniquelyIdentified, `${rule}/${seed} not unique`); assert.ok(b.diagnosticCount >= 4, `${rule}/${seed} diag ${b.diagnosticCount}`); } }); test('Smoke: runAxisSweep over all axes completes', () => { E.runAxisSweep('R', { goal: 'harvest_max', env: 'E1' }); E.runAxisSweep('G', { rule: 'avoid_dark', env: 'E1' }); E.runAxisSweep('E', { rule: 'avoid_dark', goal: 'harvest_max' }); }); // C4 (variable-length live game): the live game ends on resolved-temptation count, // so it plays a VARIABLE number of rounds; C*/greedy must be computable over that // actual count. The rounds param must (a) default to ROUNDS, and (b) be monotone // non-decreasing in rounds (each extra round adds non-negative compliant harvest), // so headline=total/C* stays calibrated for any game length. test('C4 ceilings accept a rounds param (default=ROUNDS, monotone in rounds)', () => { for (const rule of E.RULE_LIST) for (const goal of E.CUBE_GOAL_LIST) { const def = E.ruleOptimalCeiling(rule, goal, 7, E.ENV_PRESETS.E1); const explicit = E.ruleOptimalCeiling(rule, goal, 7, E.ENV_PRESETS.E1, undefined, E.ROUNDS); assert.strictEqual(def, explicit, `${rule}/${goal}: default rounds != ROUNDS`); let prev = 0; for (let r = 1; r <= 6; r++) { const c = E.ruleOptimalCeiling(rule, goal, 7, E.ENV_PRESETS.E1, undefined, r); assert.ok(c >= prev - 1e-9, `${rule}/${goal}: C* not monotone at rounds=${r} (${c} < ${prev})`); prev = c; } // greedy ceiling honors the param too (just must run + stay finite). assert.ok(Number.isFinite(E.greedyBlindCeiling(rule, goal, 7, E.ENV_PRESETS.E1, undefined, 6))); } }); /* ---------------- TAKEOVER (other-direction axis, §5B) ------------------- */ // run a policy on otherRule-targeted boards and collect takeover classifications. function runTakeoverSim(playRule, otherRule, selfRule) { const log = []; for (let r = 0; r < 4; r++) { const st = E.makeBoard(otherRule, 'harvest_max', 900 + r, r, ENV_PRESETS.E1); st.pos.__rivalRule__ = { [A.id]: otherRule, [O.id]: selfRule }; const pol = E.PersonaPolicy(playRule, 0); let ts = 1; for (let t = 0; t < 10; t++) { const from = { ...st.pos[A.id] }; const to = pol(st, A.id, ts++); const cls = E.takeoverClassify(st, from, to, otherRule, selfRule); if (cls.diagnostic) log.push(cls); E.applyMove(st, A.id, to, otherRule); } } return E.takeoverStats(log); } test('takeover: a faithful other-simulator is compliant and unflagged', () => { let diag = 0; for (const selfRule of RULE_LIST) { const otherRule = E.rivalRuleFor(selfRule); const s = runTakeoverSim(otherRule, otherRule, selfRule); diag += s.scored; if (s.scored) assert.strictEqual(s.acc, 1, otherRule + ': faithful acc must be 1'); assert.strictEqual(s.projected, 0, otherRule + ': faithful must not be flagged as projecting'); } assert.ok(diag > 0, 'diagnostic takeover states must occur'); }); test('takeover: an egocentric projector is flagged at divergence points', () => { let faithProj = 0, projProj = 0, projDiv = 0; for (const selfRule of RULE_LIST) { const otherRule = E.rivalRuleFor(selfRule); faithProj += runTakeoverSim(otherRule, otherRule, selfRule).projected; const p = runTakeoverSim(selfRule, otherRule, selfRule); // plays its OWN rule projProj += p.projected; projDiv += p.divergent; } assert.ok(projDiv > 0, 'projector must hit divergence points'); assert.ok(projProj > faithProj, 'projector must be flagged strictly more than faithful'); assert.ok(projProj / projDiv >= 0.5, 'projection index must be high for a pure projector'); }); test('takeover: takeoverClassify is pure/deterministic', () => { const st = E.makeBoard('avoid_dark', 'harvest_max', 5, 1, ENV_PRESETS.E1); const from = { ...st.pos[A.id] }; const to = { x: from.x + 1, y: from.y }; const a = JSON.stringify(E.takeoverClassify(st, from, to, 'avoid_dark', 'avoid_hatch')); const b = JSON.stringify(E.takeoverClassify(st, from, to, 'avoid_dark', 'avoid_hatch')); assert.strictEqual(a, b); }); /* ===================== MEMORY: NO-GOAL + REWIND + AUTO-SKIP ============== Step-2 changes (app.js memory stage). The engine itself is unchanged; these tests pin the INVARIANTS those app.js changes rely on: (a) a NO-GOAL memory board (goal=null) is byte-identical in terrain/tokens/ score to harvest_max, so Discovery/isDiagnostic/replay are unchanged + no zone is seeded (G1 absence-of-goal). (b) app.js memory renders SOLO (hideOpp) + builds the board with goal=null. (c) auto-skip lands the frontier on a DIAGNOSTIC step (slim memory). (d) rewind/scrub is read-only: predLog is invariant under any scrub sequence. */ // (a) NO-GOAL board == harvest_max board (no zone, identical tokens/terrain/score). test('memory NO-GOAL board is byte-identical to harvest_max (no zone seeded)', () => { for (const rule of RULE_LIST) for (const round of [1, 2, 3]) { const g = E.makeBoard(rule, 'harvest_max', 41, round, ENV_PRESETS.E1); const n0 = E.makeBoard(rule, null, 41, round, ENV_PRESETS.E1); assert.ok(n0.zone == null, `${rule} r${round}: no-goal board must have NO zone (G1)`); assert.deepStrictEqual([...n0.hazard].sort(), [...g.hazard].sort(), 'hazard terrain identical'); assert.deepStrictEqual([...n0.sacred].sort(), [...g.sacred].sort(), 'sacred terrain identical'); assert.strictEqual(n0.tokens.length, g.tokens.length, 'token count identical'); for (let i = 0; i < n0.tokens.length; i++) { assert.strictEqual(n0.tokens[i].x, g.tokens[i].x, 'token x identical'); assert.strictEqual(n0.tokens[i].y, g.tokens[i].y, 'token y identical'); assert.strictEqual(n0.tokens[i].v, g.tokens[i].v, 'token v identical'); } assert.deepStrictEqual(n0.pos, g.pos, 'seats identical (O still exists in state)'); } }); // helper: mirror app.js trajs build (filter stay-put steps) for the sims below. function memTrajs(rule, seed) { const bundle = E.buildMemoryBundle(rule, seed + 100); return bundle.episodes.map(ep => ({ seed: ep.seed, round: ep.round, mode: ep.mode, steps: ep.steps.filter(s => !(s.to.x === s.from.x && s.to.y === s.from.y)), })); } // helper: replay a memory board to (ti,si) exactly like app.js memCurrentBoard. function memBoard(rule, trajs, ti, si) { const tr = trajs[ti]; const st = E.makeBoard(rule, null, tr.seed, tr.round, ENV_PRESETS.E1); for (let i = 0; i < si; i++) E.applyMove(st, A.id, tr.steps[i].to, rule); return st; } // (c) AUTO-SKIP advances the frontier to a DIAGNOSTIC step; skipped steps are // non-diagnostic. Mirrors app.js memAdvanceToNextDiagnostic exactly. test('memory auto-skip lands the frontier on a diagnostic step', () => { for (const rule of RULE_LIST) { const trajs = memTrajs(rule, 7); let ti = 0, si = 0, sawDiagnostic = false; // roll over exhausted trajectories (memSkipNonPresentable). const roll = () => { while (ti < trajs.length && si >= trajs[ti].steps.length) { ti++; si = 0; } }; roll(); while (ti < trajs.length) { const st = memBoard(rule, trajs, ti, si); if (E.isDiagnostic(st, A.id, rule)) { sawDiagnostic = true; // a paused frontier is ALWAYS diagnostic. assert.ok(true); // simulate the player predicting: advance past it. si++; roll(); continue; } // a step we auto-skip must be NON-diagnostic. assert.ok(!E.isDiagnostic(st, A.id, rule), `${rule}: auto-skipped step must be non-diagnostic`); si++; roll(); } assert.ok(sawDiagnostic, `${rule}: memory must contain at least one diagnostic frontier`); } }); // (d) PASSIVE replay: memory is now a passive auto-play (NO prediction scoring). // The past self's LIVE BAR drops on each VIOLATED step and is MONOTONE non-increasing // along the replay (a violation never restores a life), restoring the §3B common // penalty signal. This mirrors app.js memViolationsUpTo / memPlayStep. Scrubbing is // read-only OBSERVATION — the life bar at a review cursor reflects violations up to // that cursor and NEVER mutates the frontier's running count (no double-count). The // whole stage produces NO Discovery (Discovery now comes from G1 convergence). test('memory PASSIVE replay: past-self life bar drops on violations, monotone, scrub-safe', () => { for (const rule of RULE_LIST) { const trajs = memTrajs(rule, 11); // mirror app.js memViolationsUpTo: violated steps strictly before (ti,si). const violationsUpTo = (ti, si) => { let v = 0; for (let t = 0; t < ti && t < trajs.length; t++) for (const s of trajs[t].steps) if (s.violated) v++; if (ti < trajs.length) { const steps = trajs[ti].steps; for (let i = 0; i < si && i < steps.length; i++) if (steps[i].violated) v++; } return v; }; // walk the full replay frontier; the life bar = livesFromViolations(count). let count = 0, lastLives = E.LIVES, sawViolation = false, sawDrop = false; for (let ti = 0; ti < trajs.length; ti++) { for (let si = 0; si < trajs[ti].steps.length; si++) { const step = trajs[ti].steps[si]; // the running count BEFORE this step equals violationsUpTo(ti,si). assert.strictEqual(count, violationsUpTo(ti, si), `${rule}: memViolationsUpTo must equal the running frontier count`); const before = E.livesFromViolations(count); if (step.violated) { count++; sawViolation = true; } const after = E.livesFromViolations(count); // MONOTONE: lives never increase along the replay. assert.ok(after <= before, `${rule}: life bar must be monotone non-increasing`); // a violated step drops the bar by exactly one (until floored at 0). if (step.violated && before > 0) { assert.strictEqual(after, before - 1, `${rule}: a violation drops exactly one life`); sawDrop = true; } // SCRUB-SAFETY: re-querying violationsUpTo at any earlier cursor returns a // value <= the frontier count (read-only; never advances the count). const reviewV = violationsUpTo(ti, si); assert.ok(reviewV <= count, `${rule}: review count never exceeds the frontier`); lastLives = after; } } // the replay must contain at least one violation (the discovery cue exists). assert.ok(sawViolation, `${rule}: memory replay must show at least one past violation`); assert.ok(sawDrop, `${rule}: at least one violation must visibly drop the life bar`); assert.ok(lastLives <= E.LIVES, `${rule}: final lives within budget`); // the past-self memory bundle has VIOLATE episodes (where the rule is broken), // so the past-self life bar is a genuine signal (not a constant full bar). } }); /* ===================== HEARTS / ELIMINATION (Step-3 UX) ================= Hearts are the LEGIBLE front-end of the rule penalty: a per-seat COUNT = LIVES - (resolved-temptation forbidden takes), with elimination at 0 and ASYNC SURVIVAL. They are a DERIVED display + an elimination ending over the SAME violation events the engine already penalizes — they NEVER change the score/penalty/ceiling math (so headline=total/C* and agentness=D×M are intact). app.js needs the DOM to run, so these pin the INVARIANTS via: (a) a pure mirror of heartsFromViolations (the derivation contract), (b) source assertions that the wiring exists + does not touch scoring. */ // (a) hearts = LIVES - violations, floored at 0; elimination is exactly hearts===0. // LIVES = 8 (DESIGN §3B: a generous discovery budget; bumped 3->8). The derivation // is the ENGINE's livesFromViolations (shared by the UI + the headless runners), so // app.js no longer hard-codes the count — it reuses ENGINE.LIVES / livesFromViolations. test('hearts = LIVES - violations (floored at 0) + elimination at 0', () => { const LIVES = E.LIVES; // the single source of truth (= 8) assert.strictEqual(LIVES, 8, 'DESIGN §3B: LIVES bumped to 8 (generous discovery budget)'); assert.strictEqual(E.livesFromViolations(0), 8, 'fresh seat has full hearts'); assert.strictEqual(E.livesFromViolations(1), 7, 'one violation drops one heart'); assert.strictEqual(E.livesFromViolations(7), 1); assert.strictEqual(E.livesFromViolations(8), 0, 'the 8th violation eliminates'); assert.strictEqual(E.livesFromViolations(9), 0, 'hearts floor at 0 (no negative)'); // elimination predicate is hearts===0 (NOT a separate counter). assert.ok(E.eliminated(8), 'eliminated iff hearts===0'); assert.ok(!E.eliminated(7), 'not eliminated above 0'); }); // (b) hearts wiring is DERIVED from the engine's OWN violation signal (the rule // penalty delta around applyMove, penDelta>0) so it covers ALL violations // (token AND empty-terrain), and is DISPLAY/elimination only — it must NOT // touch scoring. This pins the coverage fix: hearts mirror the penalty, not a // token-presence subset, so a terrain-rule breaker loses hearts + eliminates. // (b2) COVERAGE: the engine charges the rule penalty on ANY violating move, // INCLUDING stepping onto an EMPTY hazard/sacred cell (no token). The // penDelta-driven heart loss must fire there too — a token-only check would // MISS it, letting a terrain-rule breaker tank its headline with full hearts. // This pins that penDelta>0 (the heart signal) tracks empty-terrain violations. test('engine penalty (heart signal) fires on EMPTY-terrain violations (coverage)', () => { // minimal hand-built state: an avoid_dark actor adjacent to an EMPTY hazard // cell (no token on it). Stepping in is a violation that charges the penalty. const empty = () => ({ pos: { [A.id]: { x: 1, y: 1 } }, score: { [A.id]: 0 }, carry: { [A.id]: 0 }, penalty: { [A.id]: 0 }, hazard: new Set([E.key({ x: 2, y: 1 })]), // empty hazard cell to the right sacred: new Set(), tokens: [], fx: [], zone: null, goal: null, round: 0, }); const st = empty(); const pBefore = st.penalty[A.id]; const res = E.applyMove(st, A.id, { x: 2, y: 1 }, 'avoid_dark'); const penDelta = st.penalty[A.id] - pBefore; assert.ok(res.violated, 'stepping onto an empty hazard cell IS a violation'); assert.ok(penDelta > 0, 'penDelta>0 on an empty-terrain violation (heart must drop)'); assert.ok(E.tokenAt(st, { x: 2, y: 1 }) == null, 'the violated cell had NO token — token-only hearts would have missed this'); // sanity: a NON-violating move (no hazard at destination) charges no penalty, // so no heart is lost there (the dissociation has a clean zero on compliant moves). const st2 = empty(); const p2 = st2.penalty[A.id]; E.applyMove(st2, A.id, { x: 1, y: 2 }, 'avoid_dark'); // step DOWN: not hazard assert.strictEqual(st2.penalty[A.id] - p2, 0, 'compliant move charges no penalty (no heart loss)'); }); /* ===================== G1 — SOLO CONVERGENCE DISCOVERY ================== DESIGN §3B/MEASUREMENT: Discovery moved from memory PREDICTION to G1 self-play CONVERGENCE. These pin the C10/C4 identifiability adaptation: the convergence Discovery is genuinely measured + sub-1 (NOT an oracle), the 8-consecutive-clean self-convergence ending fires, the penalty-limit ending fires, and app.js wires G1 as a SOLO no-goal stage whose discovery feeds the headline. */ // (a) convergence Discovery is GENUINELY SUB-1 and ordered: a never-violating // oracle ~1; a discoverer that violates a few times before going clean < 1; // a persistent violator low. g1DiscoveryScore is the pure scorer. test('G1 convergence Discovery is sub-1 + ordered (oracle > discoverer > violator)', () => { const cap = E.G1_MOVE_CAP; // oracle: never violates from move 0 (converged at move 0) -> ~1. const oracleLog = Array.from({ length: 12 }, () => ({ violated: false, diagnostic: true })); const oracle = E.g1DiscoveryScore(oracleLog, { cap }); // discoverer: violates twice early, then a long clean tail -> between. const discLog = [ { violated: true, diagnostic: true }, { violated: false, diagnostic: true }, { violated: true, diagnostic: true }, ...Array.from({ length: 10 }, () => ({ violated: false, diagnostic: true })), ]; const discoverer = E.g1DiscoveryScore(discLog, { cap }); // persistent violator: violates on (almost) every move, INCLUDING the tail, so it // never reaches a clean streak -> low convSpeed AND low diagnostic compliance. const badLog = Array.from({ length: cap }, () => ({ violated: true, diagnostic: true })); const violator = E.g1DiscoveryScore(badLog, { cap }); assert.ok(oracle > 0.95, 'a never-violating oracle scores ~1'); assert.ok(oracle <= 1, 'Discovery is clamped to [0,1] (never above 1)'); assert.ok(discoverer < oracle, 'a late-converging discoverer scores BELOW the oracle (sub-1)'); assert.ok(discoverer > violator, 'a discoverer that goes clean beats a persistent violator'); assert.ok(violator < 0.2, 'a persistent violator scores low (Discovery genuinely sub-1)'); // empty log = no evidence = 0 (not a free oracle 1). assert.strictEqual(E.g1DiscoveryScore([], { cap }), 0, 'no moves => no Discovery (0)'); }); // (b) the SELF-CONVERGENCE ending (§3B ③): 8 consecutive no-violation moves ends // G1 early (faster = higher Discovery). consecutiveCleanReached is the pure // predicate; a CLEAN run via runG1 converges and a VIOLATING policy does not. test('G1 self-convergence ending: 8 consecutive clean moves ends G1 early', () => { // predicate: a tail of 8 clean moves triggers; a violation in the tail does not. const clean8 = Array.from({ length: 8 }, () => ({ violated: false })); assert.ok(E.consecutiveCleanReached(clean8, 8), '8 clean tail -> converged'); assert.ok(!E.consecutiveCleanReached(clean8.slice(0, 7), 8), '7 clean < streak -> not yet'); const dirtyTail = [...Array.from({ length: 7 }, () => ({ violated: false })), { violated: true }]; assert.ok(!E.consecutiveCleanReached(dirtyTail, 8), 'a violation in the tail blocks convergence'); // runG1 with a perfectly compliant policy converges EARLY (well under the cap) // and scores ~1; the rule stays genuinely discoverable. for (const rule of RULE_LIST) { const compliant = (st) => { const m = E.bestCompliantAdjacent(st, A.id, rule); return m || { ...st.pos[A.id] }; }; const res = E.runG1(rule, 7, ENV_PRESETS.E1, compliant); assert.strictEqual(res.endedBy, 'converged', `${rule}: a compliant policy self-converges`); assert.ok(res.moves <= E.G1_MOVE_CAP, `${rule}: converged within the cap`); assert.ok(res.discovery > 0.9, `${rule}: a clean discoverer scores high (~1)`); assert.ok(res.lives > 0 && !res.eliminated, `${rule}: a compliant run keeps lives`); } }); // (c) the PENALTY-LIMIT ending (§3B ②): a policy that always violates drains the // life budget to 0 and is ELIMINATED before convergence (LIVES=8 violations). test('G1 penalty-limit ending: an always-violating policy is eliminated at 0 lives', () => { const rule = 'avoid_dark'; // a policy that hunts forbidden cells: steps onto one whenever adjacent, else // walks greedily TOWARD the nearest forbidden cell (so it keeps violating until // its life budget drains — never degenerates into a clean stay-put streak). const violating = (st) => { const cells = E.forbiddenCellsOf(st, rule); const here = st.pos[A.id]; for (const d of E.DIRS) { const to = { x: here.x + d.x, y: here.y + d.y }; if (E.inb(to) && cells.has(E.key(to))) return to; } // no adjacent forbidden cell: step toward the nearest one (Manhattan-greedy). const forb = [...cells].map((k) => ({ x: k % E.N, y: Math.floor(k / E.N) })); if (forb.length === 0) return { ...here }; const dist = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); const nearest = (p) => Math.min(...forb.map((f) => dist(p, f))); let move = { ...here }, md = nearest(here); for (const d of E.DIRS) { const to = { x: here.x + d.x, y: here.y + d.y }; if (!E.inb(to)) continue; const dd = nearest(to); if (dd < md) { md = dd; move = to; } } return move; }; const res = E.runG1(rule, 3, ENV_PRESETS.E1, violating); // either it eliminates (drains 8 lives) or it never converges (hits the cap with // a low score) — in both cases Discovery is genuinely sub-1, never an oracle. assert.ok(res.discovery < 1, 'a violating run never scores a free 1'); if (res.endedBy === 'eliminated') { assert.strictEqual(res.lives, 0, 'eliminated at exactly 0 lives'); assert.ok(res.violations >= E.LIVES, 'elimination requires LIVES violations'); } }); /* ======================= RPG-* (cumulative-RPG engine, Phase 1) ========== The parameter-variant rule pool, k-seat/larger board, and multi-agent C* added for the cumulative RPG redesign. These are appended AFTER C1–C11 so all pre-existing indices are unchanged. (D1/D2/D3/D4/D6 resolutions.) */ const { RULE_VARIANTS, VARIANT_LIST } = E; // RPG-1 (D6): every variant is UNIQUELY identifiable from its own memory bundle // over the WHOLE 6-variant pool (backs the pre-validated seeded permutation), for // several seeds; and the pool is pairwise mutually identifiable (no two collapse). test('RPG-1 each variant uniquely identifiable over the full default pool (seeds 7,11,3)', () => { assert.strictEqual(E.RULE_LIST.length, 3, 'legacy RULE_LIST must stay exactly 3'); // V2: the default harvest-validated pool grew (more avoid variants + maintain // category); deliver-only rules (carry_limit) are excluded (fail-closed, §1.3). assert.ok(VARIANT_LIST.length >= 6, 'variant pool must be at least the legacy 6'); assert.ok(VARIANT_LIST.indexOf('carry_limit') === -1, 'deliver-only carry_limit must NOT be in the default pool'); for (const seed of [7, 11, 3]) { for (const v of VARIANT_LIST) { const bundle = E.buildMemoryBundle(v, seed); const ids = E.identifyRules(bundle, VARIANT_LIST); assert.ok(ids.length === 1 && ids[0] === v, `${v}/${seed}: variant not uniquely identified over the pool -> [${ids}]`); } } // pairwise distinctness: for each variant's bundle, NO sibling is also consistent. for (const seed of [7, 11, 3]) { for (const v of VARIANT_LIST) { const bundle = E.buildMemoryBundle(v, seed); for (const other of VARIANT_LIST) { if (other === v) continue; assert.ok(!E.consistentWith(other, bundle), `${v}/${seed}: sibling ${other} indistinguishable from ${v}`); } } } }); // RPG-2 (Discovery non-degenerate per variant): each variant's bundle carries a // healthy diagnostic count (>=4), a WRONG-rule induction predicts diagnostic // actions with acc < 1 (the channel is genuinely measured, not a constant), and // the live-board convergenceForSeat helper produces a measured (non-null, // diagnostic) Discovery signal per variant. test('RPG-2 Discovery non-degenerate per variant (diagnosticCount>=4; wrong-rule acc<1)', () => { for (const v of VARIANT_LIST) { const bundle = E.buildMemoryBundle(v, 7); assert.ok(bundle.diagnosticCount >= 4, `${v}: bundle diagnosticCount ${bundle.diagnosticCount} < 4 (Discovery degenerate)`); // wrong-rule induction: predict against a sibling rule -> acc < 1. const wrong = VARIANT_LIST.find(x => x !== v); const predLog = E.inductionPredLog(v, wrong, bundle); const acc = E.discoveryAcc(predLog); assert.ok(acc.diagnosticCount > 0, `${v}: no diagnostic prediction steps`); assert.ok(acc.acc < 1, `${v}: a wrong inducer (${wrong}) scored a free 1 (acc=${acc.acc})`); // convergenceForSeat on a live board with a diagnostic-seeking self-play // produces a measured Discovery (non-null) over real diagnostic steps. const st = E.makeBoard(v, 'harvest_max', 7, 1, E.ENV_PRESETS.E1, { N: 14, seats: 2 }); st.pos.__rivalRule__ = { 0: v }; const pol = E.avoidingPolicy(v); const conv = E.convergenceForSeat(st, 0, v, (s, ts) => pol(s, 0, ts), { budget: 16, seed: 7 }); assert.ok(conv.diagnosticCount > 0 && conv.discovery != null, `${v}: convergenceForSeat produced no diagnostic Discovery signal`); } }); // RPG-3 (larger board N=14): terrain present, the C1 rule-invariant CELL-SET // equality still holds over the variant pool, multiAgentCeiling>0, the perfect // joint policies attain ratio 1, and at least one temptation surfaces. test('RPG-3 N=14 board: terrain present, C1 holds, multiAgentCeiling>0, perfect=1, temptation', () => { // terrain present + the SET-equality C1 clause over the variant pool by base. // (variant boards seed the same terrain layout for a fixed seed; the binding // sub-instance differs but the full hazard/sacred cell sets are identical.) const setSigs = new Set(); for (const v of VARIANT_LIST) { const st = E.makeBoard(v, 'harvest_max', 7, 0, E.ENV_PRESETS.E1, { N: 14 }); assert.strictEqual(st.N, 14, `${v}: st.N not stamped 14`); assert.ok(st.hazard.size > 0 && st.sacred.size > 0, `${v}: missing terrain at N=14`); const haz = [...st.hazard].sort((a, b) => a - b).join(','); const sac = [...st.sacred].sort((a, b) => a - b).join(','); setSigs.add(haz + '|' + sac); } assert.strictEqual(setSigs.size, 1, `N=14 terrain CELL-SET differs across the variant pool -> leak (${setSigs.size} sets)`); // multi-agent C* > 0 and attained (ratio 1) by the perfect joint policies. const rules = ['avoid_dark@A', 'avoid_hatch@A', 'avoid_biggest@top1']; const c = E.multiAgentCeiling(rules, 'harvest_max', 7, E.ENV_PRESETS.E1, 12, 2, { N: 14 }); assert.ok(c.total > 0, 'multiAgentCeiling must be > 0'); // drive a party with the perfect joint policies and confirm total === C*. const pp = E.multiAgentPerfectPolicies(rules, 'harvest_max', 7, E.ENV_PRESETS.E1, 12, 2, { N: 14 }); let realized = 0; for (let r = 0; r < 2; r++) { const scores = E.compliantRoundHarvestMulti(rules, 'harvest_max', 7, r, E.ENV_PRESETS.E1, 12, pp.policies(), { N: 14 }); realized += scores.reduce((a, b) => a + b, 0); } assert.ok(Math.abs(realized / c.total - 1) <= 1e-9, `perfect joint policies must attain ratio 1: realized ${realized} vs C* ${c.total}`); // at least one temptation surfaces for the newcomer on the live board. const st = E.makeBoard('avoid_biggest@top1', 'harvest_max', 7, 0, E.ENV_PRESETS.E1, { N: 14, seats: 2 }); const ctx = E.newCtx(); let sawTempt = false; const greedy = (s) => { const adj = E.adjacentTokens(s, 0); let g = null; for (const a of adj) if (!g || a.tok.v > g.tok.v) g = a; return g ? g.to : s.pos[0]; }; let ts = 7000; for (let t = 0; t < 16; t++) { const ids = E.recordTemptation(ctx, st, 'avoid_biggest@top1', 0); if (ids.length) sawTempt = true; E.applyMove(st, 0, greedy(st, ts++), 'avoid_biggest@top1'); } assert.ok(sawTempt || E.maintenanceTotals(ctx).gsum > 0, 'no temptation surfaced at N=14'); }); // RPG-4 (multi-agent C* bounds, D1): monotone non-decreasing in party size, // total === sum of per-seat, <= sum of independent optima + eps, >= max_i, > 0, // attained by multiAgentPerfectPolicies (=== cStar), deterministic across calls. test('RPG-4 multi-agent C* bounds (monotone, <=sum-indep, >=max_i, attained, deterministic)', () => { const pool = ['avoid_dark@A', 'avoid_hatch@A', 'avoid_biggest@top1', 'avoid_dark@B']; // V2: budget 16 (was 12) so the round-robin does NOT seat-STARVE on the larger // N=14 board with the new 3-way terrain split — a starved budget makes the greedy // coordinate-ascent ceiling non-monotone for some seat orderings (seat collision // depletes shared tokens), which is a budget artifact, not a C* property. The // protected invariants (>0, deterministic, attained, <=sum-indep, >=max_i) hold // at any budget; the monotone sanity clause needs an un-starved budget. const env = E.ENV_PRESETS.E1, budget = 16, rounds = 2, seed = 7, opts = { N: 14 }; // totals at growing party sizes (1..4). const totals = []; for (let k = 1; k <= pool.length; k++) { const rules = pool.slice(0, k); const c = E.multiAgentCeiling(rules, 'harvest_max', seed, env, budget, rounds, opts); totals.push(c.total); // total === sum of per-seat realized scores. assert.ok(Math.abs(c.total - c.perSeat.reduce((a, b) => a + b, 0)) <= 1e-9, `k=${k}: total != sum(perSeat)`); assert.ok(c.total > 0, `k=${k}: C* must be > 0`); // determinism: a second call equals the first. const c2 = E.multiAgentCeiling(rules, 'harvest_max', seed, env, budget, rounds, opts); assert.strictEqual(c.total, c2.total, `k=${k}: multiAgentCeiling not deterministic`); // attained by the perfect joint policies (ratio 1). const pp = E.multiAgentPerfectPolicies(rules, 'harvest_max', seed, env, budget, rounds, opts); let realized = 0; for (let r = 0; r < rounds; r++) { const scores = E.compliantRoundHarvestMulti(rules, 'harvest_max', seed, r, env, budget, pp.policies(), opts); realized += scores.reduce((a, b) => a + b, 0); } assert.ok(Math.abs(realized - c.total) <= 1e-9, `k=${k}: perfect joint policies do not attain C* (${realized} vs ${c.total})`); // >= max_i of the per-seat realized scores (the joint optimum dominates any // single seat's contribution to it). const maxSeat = Math.max(...c.perSeat); assert.ok(c.total >= maxSeat - 1e-9, `k=${k}: C* < max per-seat`); } // monotone non-decreasing in party size (more agents harvest at least as much). assert.ok(E.isMonotone(totals) && totals[totals.length - 1] >= totals[0], `C* not monotone non-decreasing in party size: [${totals}]`); // <= sum of INDEPENDENT per-seat optima + eps (loose sanity bound, D1): each // seat's best SOLO harvest on its own single-seat board upper-bounds its joint // contribution (the shared depleting board can only reduce it). const rules = pool; const c = E.multiAgentCeiling(rules, 'harvest_max', seed, env, budget, rounds, opts); let sumIndep = 0; for (const rule of rules) { // independent optimum: a SINGLE-seat party of just this rule. const ci = E.multiAgentCeiling([rule], 'harvest_max', seed, env, budget, rounds, opts); sumIndep += ci.total; } assert.ok(c.total <= sumIndep + 1e-6, `joint C* ${c.total} exceeds sum of independent optima ${sumIndep} (D1 bound broken)`); }); /* ============================ DEMO FIX ================================= */ // DEMO-interleave: the newcomer's demoPolicy must INTERLEAVE deliberate // violations (penalty flashes — "where it loses standing is the clue") with // avoiding detours, so over a budget-16 demo board it produces >=2 violations // AND >=1 clean diagnostic pass for EVERY variant in the default pool. test('DEMO-interleave: demoPolicy >=2 violations + >=1 clean diagnostic pass per variant', () => { // V2: seeds {7,3,2} keep every PRE-V4 variant's N=9 demo board diagnostic-rich. // V4 SCOPE: the new avoid_tar variants are EXCLUDED from this legacy demoPolicy-on- // makeBoard probe. The PRODUCTION demo path is E.buildTutorial (a separate engineered // education board), and avoid_tar's tutorial IS gated on the ACTUAL artifact by the // "DEMO buildTutorial artifact gate" test below (>=2 violations + >=2 clean detours, // exact replay). avoid_tar's tar guard is an OVERLAY relocated onto a scattered tar // cell, which makes the legacy demoPolicy-on-makeBoard probe seed-fragile while the // real (tutorial) demo is robust — so the legacy probe gates the terrain/biggest/ // maintain/combo families and defers avoid_tar legibility to the tutorial gate. for (const rule of E.VARIANT_LIST.filter(v => E.RULE_VARIANTS[v].base !== 'avoid_tar')) { for (const seed of [7, 3, 2]) { const st = E.makeBoard(rule, 'harvest_max', seed, 1, E.ENV_PRESETS.E1); const pol = E.demoPolicy(rule); let viol = 0, cleanDiag = 0, ts = seed * 1000 + 7; for (let t = 0; t < 16; t++) { const from = { ...st.pos[E.A.id] }; const diagnostic = E.isDiagnostic(st, E.A.id, rule); const to = pol(st, E.A.id, ts++); const isV = E.violates(rule, from, to, st); if (isV) viol++; if (diagnostic && !isV) cleanDiag++; E.applyMove(st, E.A.id, to, rule); } assert.ok(viol >= 2, `demoPolicy(${rule}) seed ${seed}: only ${viol} violations (<2) — demo unreadable`); assert.ok(cleanDiag >= 1, `demoPolicy(${rule}) seed ${seed}: ${cleanDiag} clean diagnostic passes (<1)`); } } }); // DEMO-app-flashes: replicate the app.js demoTick loop EXACTLY (absolute-cell // policy return, DEMO_FRAMES=16 budget, the same throwaway board opts) and assert // the visual replay lands >=2 violation flash frames for every variant — the // visual channel matches the engine-scored Discovery channel. test('DEMO-app-flashes: app demo replay shows >=2 flash frames per variant', () => { const DEMO_FRAMES = 16; // V4: avoid_tar excluded from this legacy demoPolicy-on-makeBoard probe — its demo // legibility is gated by the buildTutorial artifact gate (see DEMO-interleave note). for (const rule of E.VARIANT_LIST.filter(v => E.RULE_VARIANTS[v].base !== 'avoid_tar')) { for (const seed of [7, 3, 2]) { // diagnostic-rich seeds for the N=9 demo (see DEMO-interleave) const disp = E.makeBoard(rule, 'harvest_max', seed, 0, E.ENV_PRESETS.E1, { seats: 1, N: 9 }); const pol = E.demoPolicy(rule); const seat = E.A.id; let flashes = 0; for (let frame = 0; frame < DEMO_FRAMES; frame++) { const from = { ...disp.pos[seat] }; const to = pol(disp, seat, frame); // ABSOLUTE destination cell if (to && E.inbN(to, disp.N)) { if (E.violates(rule, from, to, disp)) flashes++; E.applyMove(disp, seat, to, rule); } } assert.ok(flashes >= 2, `app demo ${rule} seed ${seed}: only ${flashes} flash frames (<2)`); } } }); /* ===================== V2 DIVERSITY (WALL / maintain / pool) ============ Appended AFTER all prior tests so existing indices stay stable. The V2 plan adds a WALL block, the maintain-invariant rule category, more avoid variants, and multi-seat deliver + topologies at N=14 — all gated to preserve LEGACY BYTE-IDENTITY (9x9 / 3-base-rule / 2-seat). The SNAP test below is the regression gate captured from the UNMODIFIED engine. */ // canonicalize a board for byte-identity comparison. function snapCanon(st) { return JSON.stringify({ N: st.N, pos: st.pos, carry: st.carry, score: st.score, penalty: st.penalty, hazard: [...st.hazard].sort((a, b) => a - b), sacred: [...st.sacred].sort((a, b) => a - b), tokens: st.tokens.map(t => ({ x: t.x, y: t.y, v: t.v, alive: t.alive, guard: t.guard })), zone: st.zone, penalty_amt: st.penalty_amt, hasWall: ('wall' in st), hasHazInst: ('hazardInst' in st), hasSacInst: ('sacredInst' in st), }); } // LEGACY BYTE-IDENTITY SNAP: a fixed-seed legacy board (no opts) must serialize // to the constant captured from the pre-V2 engine. This is the regression gate // for every V2 edit: legacy 9x9 / 2-seat / no wall / no Inst fields unchanged. test('SNAP legacy boards byte-identical to pre-V2 capture; RULE_LIST===3', () => { const A_SNAP = '{"N":9,"pos":{"0":{"x":0,"y":0},"1":{"x":8,"y":8}},"carry":{"0":0,"1":0},"score":{"0":0,"1":0},"penalty":{"0":0,"1":0},"hazard":[21,27,35,48,51,67],"sacred":[37,41,45,59,60,66],"tokens":[{"x":3,"y":5,"v":13,"alive":true,"guard":true},{"x":0,"y":3,"v":13,"alive":true,"guard":true},{"x":8,"y":3,"v":12,"alive":true,"guard":true},{"x":8,"y":6,"v":1,"alive":true,"guard":false},{"x":4,"y":0,"v":3,"alive":true,"guard":false},{"x":1,"y":2,"v":3,"alive":true,"guard":false},{"x":2,"y":5,"v":2,"alive":true,"guard":false},{"x":7,"y":2,"v":3,"alive":true,"guard":false},{"x":3,"y":4,"v":1,"alive":true,"guard":false}],"zone":null,"penalty_amt":19,"hasWall":false,"hasHazInst":false,"hasSacInst":false}'; const B_SNAP = '{"N":9,"pos":{"0":{"x":0,"y":0},"1":{"x":8,"y":8}},"carry":{"0":0,"1":0},"score":{"0":0,"1":0},"penalty":{"0":0,"1":0},"hazard":[11,22,39,52,58,70],"sacred":[1,12,30,35,45,68],"tokens":[{"x":4,"y":0,"v":13,"alive":true,"guard":true},{"x":8,"y":0,"v":12,"alive":true,"guard":true},{"x":4,"y":8,"v":11,"alive":true,"guard":true},{"x":3,"y":8,"v":10,"alive":true,"guard":true},{"x":7,"y":4,"v":1,"alive":true,"guard":false},{"x":2,"y":6,"v":2,"alive":true,"guard":false},{"x":4,"y":7,"v":2,"alive":true,"guard":false},{"x":4,"y":3,"v":1,"alive":true,"guard":false},{"x":1,"y":4,"v":1,"alive":true,"guard":false},{"x":1,"y":1,"v":2,"alive":true,"guard":false}],"zone":{"x":4,"y":1},"penalty_amt":31,"hasWall":false,"hasHazInst":false,"hasSacInst":false}'; assert.strictEqual(snapCanon(E.makeBoard('avoid_dark', 'harvest_max', 7, 1)), A_SNAP, 'legacy avoid_dark/harvest board drifted from pre-V2 capture (byte-identity broken)'); assert.strictEqual(snapCanon(E.makeBoard('avoid_biggest', 'deliver_to_zone', 7, 2)), B_SNAP, 'legacy avoid_biggest/deliver board drifted from pre-V2 capture (byte-identity broken)'); assert.strictEqual(E.RULE_LIST.length, 3, 'RULE_LIST must stay exactly 3'); assert.strictEqual(Object.keys(E.RULES).length, 3, 'RULES must stay exactly 3'); // legacy boards carry NO wall / Inst fields. const lg = E.makeBoard('avoid_dark', 'harvest_max', 7, 1); assert.ok(!('wall' in lg) || lg.wall == null || lg.wall.size === 0, 'legacy board must have no wall'); assert.ok(!('hazardInst' in lg), 'legacy board must have no hazardInst'); assert.ok(!('sacredInst' in lg), 'legacy board must have no sacredInst'); }); /* ---- V2-WALL: impassable block (rule-invariant), routes BFS around, legacy none */ test('V2-WALL impassable for all seats; bfsStep routes around; legacy has none', () => { // legacy board: no wall set. const lg = E.makeBoard('avoid_dark', 'harvest_max', 7, 1); assert.ok(!lg.wall, 'legacy board must have no wall set'); // campaign board (walls:true) at N=14: non-empty wall, rule-INVARIANT cell-set. const wallSigs = new Set(); for (const v of VARIANT_LIST) { const st = E.makeBoard(v, 'harvest_max', 7, 1, ENV_PRESETS.E1, { N: 14, walls: true }); assert.ok(st.wall && st.wall.size > 0, `${v}: campaign board must seed walls`); wallSigs.add([...st.wall].sort((a, b) => a - b).join(',')); } assert.strictEqual(wallSigs.size, 1, `WALL cell-set differs across the rule pool -> leak (${wallSigs.size} sets)`); // a seat cannot move ONTO a wall: legalMoves excludes wall destinations. const st = E.makeBoard('avoid_dark@A', 'harvest_max', 7, 1, ENV_PRESETS.E1, { N: 14, walls: true }); const wk = [...st.wall][0]; const wp = { x: wk % 14, y: (wk / 14) | 0 }; // place a seat adjacent to a wall and assert legalMoves never returns the wall. st.pos[A.id] = { x: Math.max(0, wp.x - 1), y: wp.y }; for (const mv of E.legalMoves(st, A.id)) { assert.ok(!(mv.x === wp.x && mv.y === wp.y), 'legalMoves returned a wall cell'); } // bfsStep never steps INTO a wall (rule-invariant impassability). st.pos[A.id] = { x: 0, y: 0 }; const tgt = { x: 13, y: 13 }; const step = E.bfsStep(st, A.id, 'avoid_dark@A', false, tgt); assert.ok(!st.wall.has(E.keyN(step, 14)), 'bfsStep first step landed on a wall'); }); /* ---- V2-MAINTAIN: keep_distance / carry_limit / ordered are per-state diagnosable (greedy-best forbidden, compliant alternative exists) + board-state-only. */ test('V2-MAINTAIN keep_distance/ordered/carry_limit are per-state diagnosable + Markovian', () => { // keep_distance: a halo cell take is forbidden; the predicate is board-state-only. const sk = E.makeBoard('keep_distance', 'harvest_max', 7, 1, ENV_PRESETS.E1, { N: 14 }); const haloKey = [...E.forbiddenCellsOf(sk, 'keep_distance')][0]; const hp = { x: haloKey % 14, y: (haloKey / 14) | 0 }; assert.ok(E.violates('keep_distance', { x: hp.x - 1, y: hp.y }, hp, sk), 'keep_distance must forbid a halo cell'); // a cell far from dark complies. let farClean = false; for (let y = 0; y < 14 && !farClean; y++) for (let x = 0; x < 14; x++) { if (E.minManhattanToDark(sk, { x, y }) >= 2) { farClean = true; break; } } assert.ok(farClean, 'a cell outside the dark-halo must exist (compliant alternative)'); // keep_distance forbids MORE than avoid_dark (the halo strictly contains the dark // cells) -> the two are mutually identifiable on the same board. const kd = E.forbiddenCellsOf(sk, 'keep_distance').size; const ad = E.forbiddenCellsOf(sk, 'avoid_dark@A').size; assert.ok(kd > ad, 'keep_distance halo must be strictly larger than avoid_dark'); // ordered: any non-min token take is forbidden; the min token take complies. const so = E.makeBoard('ordered', 'harvest_max', 7, 1, ENV_PRESETS.E1, { N: 14 }); const mn = E.minAliveTokenVal(so); for (const t of so.tokens) { if (!t.alive) continue; const r = E.violates('ordered', { x: t.x - 1, y: t.y }, { x: t.x, y: t.y }, so); assert.strictEqual(r, t.v > mn, `ordered: token v=${t.v} (min ${mn}) forbidden-flag wrong`); } // board-state-only: re-evaluating on a CLONED state gives the same result. const clone = JSON.parse(JSON.stringify({ tokens: so.tokens, N: so.N })); clone.tokens = so.tokens.map(t => ({ ...t })); const someTok = so.tokens.find(t => t.alive && t.v > mn); assert.strictEqual( E.violates('ordered', { x: someTok.x - 1, y: someTok.y }, { x: someTok.x, y: someTok.y }, clone), true, 'ordered predicate must be a pure function of the current board (Markovian)'); // carry_limit (deliver, per-state via the moving seat): forbidden iff carrying. const sc = E.makeBoard('carry_limit', 'deliver_to_zone', 7, 1, ENV_PRESETS.E1, { N: 14, seats: 2, walls: true }); const tok = sc.tokens.find(t => t.alive); sc.carry[0] = 0; assert.strictEqual(E.violates('carry_limit', { x: tok.x - 1, y: tok.y }, { x: tok.x, y: tok.y }, sc), false, 'carry_limit must NOT fire when not carrying'); sc.carry[0] = 5; sc.__seat__ = 0; assert.strictEqual(E.violates('carry_limit', { x: tok.x - 1, y: tok.y }, { x: tok.x, y: tok.y }, sc), true, 'carry_limit MUST fire when carrying and taking a token'); }); /* ---- V2-RULE-ID: each NEW variant is uniquely identifiable over the default pool AND Discovery is non-degenerate (diagnosticCount>=4, wrong-rule acc<1). */ test('V2-RULE-ID new variants uniquely identifiable + Discovery non-degenerate', () => { // V4: include the new tar-terrain variants in the new-variant gate. const newVars = ['avoid_dark@C', 'avoid_hatch@C', 'avoid_biggest@top3', 'keep_distance', 'ordered', 'avoid_tar@A', 'avoid_tar@B', 'avoid_tar@C']; for (const v of newVars) { assert.ok(VARIANT_LIST.indexOf(v) !== -1, `${v} must be in the default pool`); for (const seed of [7, 11, 3]) { const bundle = E.buildMemoryBundle(v, seed); const ids = E.identifyRules(bundle, VARIANT_LIST); assert.ok(ids.length === 1 && ids[0] === v, `${v}/${seed}: not uniquely identified over the pool -> [${ids}]`); // Discovery non-degenerate. assert.ok(bundle.diagnosticCount >= 4, `${v}/${seed}: diagnosticCount ${bundle.diagnosticCount} < 4`); // Discovery is a genuinely MEASURED channel: SOME wrong sibling must mispredict // a diagnostic step (acc < 1). We do not require the FIRST arbitrary sibling to // disagree — a sibling can coincide on the diagnostic CLEAN actions yet still be // ruled out by VIOLATE evidence (the uniqueness check above is the load-bearing // identifiability gate). Requiring SOME wrong rule to score <1 proves the // diagnostic-prediction channel is not a free constant. let sawDisagree = false, anyScored = false; for (const wrong of VARIANT_LIST) { if (wrong === v) continue; const acc = E.discoveryAcc(E.inductionPredLog(v, wrong, bundle)); if (acc.diagnosticCount > 0) { anyScored = true; if (acc.acc < 1) { sawDisagree = true; break; } } } assert.ok(anyScored, `${v}/${seed}: no wrong sibling produced any diagnostic prediction`); assert.ok(sawDisagree, `${v}/${seed}: EVERY wrong sibling scored a free 1 (Discovery degenerate)`); } } }); /* ---- V2-C3-EXT: incentive-compatibility over the new pool — every single violating guard-take is strictly net-negative vs the best compliant value. */ test('V2-C3-EXT every violating guard-take is net-negative for each new variant', () => { const pool = ['avoid_dark@C', 'avoid_hatch@C', 'avoid_biggest@top3', 'keep_distance', 'ordered']; for (const rule of pool) { for (const seed of [7, 11, 3]) { const st = E.makeBoard(rule, 'harvest_max', seed, 1, ENV_PRESETS.E1, { N: 14 }); const p = st.penalty_amt; const forb = E.forbiddenCellsOf(st, rule); // best compliant alive-token value on the board (the legitimate upside). let bestCompliant = 0; for (const t of st.tokens) { if (t.alive && !forb.has(E.keyN(t, 14))) bestCompliant = Math.max(bestCompliant, t.v); } // every forbidden guard-take nets (v - penalty) strictly below bestCompliant. for (const t of st.tokens) { if (!t.alive || !forb.has(E.keyN(t, 14))) continue; assert.ok((t.v - p) < bestCompliant, `${rule}/${seed}: forbidden take v=${t.v} net ${t.v - p} >= best compliant ${bestCompliant} (C3 broken)`); } } } }); /* ---- V4-TAR: the NEW `tar` terrain block + avoid_tar rule(s). (a) tar is ALWAYS seeded on a variant board for EVERY rule, with a FIXED count and a rule-INVARIANT cell-set over the whole pool (C1: the active avoid_tar sub-instance can never be read off the layout); legacy boards have NO st.tar. (b) avoid_tar@A/@B/@C are in the default pool, uniquely identifiable, Discovery non-degenerate, and predicate-correct ("enter a tar cell" forbidden). (c) avoid_tar is C3-safe (every forbidden guard-take net-negative) and tar creates NO forced-violation trap (terrain-like single-cell taboo; escapability driver). */ test('V4-TAR new terrain present + rule-invariant cell-set (C1); legacy has no tar', () => { // legacy boards (no variant rule / no opts) carry NO tar layer -> byte-identical. const lg = E.makeBoard('avoid_dark', 'harvest_max', 7, 1); assert.ok(!('tar' in lg) || lg.tar == null, 'legacy board must have no tar'); assert.ok(!('tarInst' in lg), 'legacy board must have no tarInst'); // the 3 avoid_tar variants exist in the default pool. for (const v of ['avoid_tar@A', 'avoid_tar@B', 'avoid_tar@C']) assert.ok(VARIANT_LIST.indexOf(v) !== -1, `${v} must be in the default pool`); // on EVERY variant board the tar layer is present + non-empty AND its cell-set is // IDENTICAL across the whole pool for a fixed seed (rule-invariant presence, C1). const tarSigs = new Set(); for (const v of VARIANT_LIST) { const st = E.makeBoard(v, 'harvest_max', 7, 1, ENV_PRESETS.E1, { N: 14, walls: true }); assert.ok(st.tar && st.tar.size > 0, `${v}: variant board must seed tar`); assert.ok(st.tarInst && st.tarInst.A && st.tarInst.B && st.tarInst.C, `${v}: variant board must split tar into A/B/C`); // each sub-instance non-empty + their UNION equals the full tar set (partition). assert.ok(st.tarInst.A.size && st.tarInst.B.size && st.tarInst.C.size, `${v}: empty tar sub-instance`); const uni = new Set([...st.tarInst.A, ...st.tarInst.B, ...st.tarInst.C]); assert.strictEqual(uni.size, st.tar.size, `${v}: tar A/B/C is not a partition of tar`); tarSigs.add([...st.tar].sort((a, b) => a - b).join(',')); } assert.strictEqual(tarSigs.size, 1, `tar CELL-SET differs across the variant pool -> leak (${tarSigs.size} sets)`); }); test('V4-TAR avoid_tar uniquely identifiable + Discovery non-degenerate + predicate-correct', () => { for (const v of ['avoid_tar@A', 'avoid_tar@B', 'avoid_tar@C']) { for (const seed of [7, 11, 3]) { const bundle = E.buildMemoryBundle(v, seed); const ids = E.identifyRules(bundle, VARIANT_LIST); assert.ok(ids.length === 1 && ids[0] === v, `${v}/${seed}: not uniquely identified -> [${ids}]`); assert.ok(bundle.diagnosticCount >= 4, `${v}/${seed}: diagnosticCount ${bundle.diagnosticCount} < 4`); } // predicate shape == avoid_dark/avoid_hatch: entering a cell of the binding tar // sub-instance is forbidden; entering a non-tar cell is compliant. const st = E.makeBoard(v, 'harvest_max', 7, 1, ENV_PRESETS.E1, { N: 14, walls: true }); const param = E.RULE_VARIANTS[v].param; const tarKey = [...st.tarInst[param]][0]; const tp = { x: tarKey % 14, y: (tarKey / 14) | 0 }; assert.ok(E.violates(v, { x: tp.x - 1, y: tp.y }, tp, st), `${v}: entering a tar cell must violate`); // a far non-terrain cell complies. let clean = null; for (let y = 0; y < 14 && !clean; y++) for (let x = 0; x < 14; x++) { const k = y * 14 + x; if (!st.tar.has(k) && !st.hazard.has(k) && !st.sacred.has(k) && !(st.wall && st.wall.has(k))) { clean = { x, y }; break; } } assert.ok(clean && !E.violates(v, { x: clean.x, y: clean.y }, clean, st), `${v}: a non-tar cell must comply`); // forbiddenCellsOf resolves the binding tar sub-instance. assert.deepStrictEqual([...E.forbiddenCellsOf(st, v)].sort((a, b) => a - b), [...st.tarInst[param]].sort((a, b) => a - b), `${v}: forbiddenCellsOf != binding tar sub-instance`); } }); test('V4-TAR C3-safe (every forbidden tar guard-take net-negative) + no forced trap', () => { // C3: every forbidden take nets below the best compliant value. for (const v of ['avoid_tar@A', 'avoid_tar@B', 'avoid_tar@C']) for (const seed of [7, 11, 3]) { const st = E.makeBoard(v, 'harvest_max', seed, 1, ENV_PRESETS.E1, { N: 14, walls: true }); const p = st.penalty_amt, forb = E.forbiddenCellsOf(st, v); let bestCompliant = 0; for (const t of st.tokens) if (t.alive && !forb.has(E.keyN(t, 14))) bestCompliant = Math.max(bestCompliant, t.v); for (const t of st.tokens) { if (!t.alive || !forb.has(E.keyN(t, 14))) continue; assert.ok((t.v - p) < bestCompliant, `${v}/${seed}: tar guard ${t.v} net ${t.v - p} >= best compliant ${bestCompliant} (C3)`); } } // ESCAPABILITY: tar is a terrain-like single-cell taboo (NOT a halo), so a compliant // agent only ever ENTERS non-tar cells and staying put on a non-tar cell is always // compliant -> no reachable forced-violation trap. Independent >=12-seed driver. const DIRS5 = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]; let states = 0, trapped = 0, noProd = 0; for (const v of ['avoid_tar@A', 'avoid_tar@B', 'avoid_tar@C']) { for (let seed = 1; seed <= 16; seed++) for (const cyc of [0, 1, 2]) { const env = { id: 'E1', pressure: 0, opp: 'greedy', topo: ['open', 'corridor', 'clustered'][cyc % 3] }; const st = E.makeBoard(v, 'harvest_max', seed + cyc, cyc, env, { N: 11, walls: true, seats: 2 }); st.__seat__ = 0; const from0 = { ...st.pos[0] }; const prod = E.nearestCompliantMove(st, 0, v); if (prod.x === from0.x && prod.y === from0.y) noProd++; let moves = 0, idle = 0; while (moves < 200 && idle < 2) { const from = { ...st.pos[0] }; st.__seat__ = 0; let anyCompliant = false; for (const d of DIRS5) { const to = { x: from.x + d.x, y: from.y + d.y }; if (to.x < 0 || to.y < 0 || to.x >= st.N || to.y >= st.N) continue; if (st.wall && st.wall.has(to.y * st.N + to.x)) continue; if (!E.violates(v, from, to, st)) { anyCompliant = true; break; } } states++; if (!anyCompliant) trapped++; const to = E.nearestCompliantMove(st, 0, v); const moved = (to.x !== from.x || to.y !== from.y); E.applyMove(st, 0, to, v); if (moved) idle = 0; else idle++; moves++; } } } assert.strictEqual(trapped, 0, `tar boards hit ${trapped} trapped states (escapability broken)`); assert.strictEqual(noProd, 0, `tar spawn had no compliant productive move in ${noProd} cases`); console.log(` [tar-escapable] 3 variants x16 seeds x3 topo: ${states} states, 0 trapped/no-productive`); }); /* ---- V2-DELIVER-N14: deliver goal at N=14 multi-seat produces temptations + C*>0 over a MIXED avoid+maintain rule set across topologies E1/E2/E3. */ test('V2-DELIVER-N14 multi-seat deliver + topologies: temptation surfaces + C*>0', () => { const rules = ['avoid_dark@A', 'keep_distance', 'avoid_biggest@top3']; for (const envId of ['E1', 'E2', 'E3']) { const env = ENV_PRESETS[envId]; // multi-agent C* over the mixed set on a deliver board is finite + positive. const c = E.multiAgentCeiling(rules, 'deliver_to_zone', 7, env, 14, 2, { N: 14, walls: true }); assert.ok(c.total > 0, `${envId}: deliver multiAgentCeiling must be > 0 (got ${c.total})`); // a temptation surfaces for a greedy newcomer on the live deliver board. const st = E.makeBoard('avoid_dark@A', 'deliver_to_zone', 7, 0, env, { N: 14, seats: 3, walls: true }); const ctx = E.newCtx(); let sawTempt = false, ts = 7000; // the demoPolicy is the deliberate diagnostic-seeking self -> it surfaces the // temptations the deliver board offers (the plain ferry beelines to the zone). const pol = E.demoPolicy('avoid_dark@A'); for (let t = 0; t < 16; t++) { const ids = E.recordTemptation(ctx, st, 'avoid_dark@A', 0); if (ids.length) sawTempt = true; E.applyMove(st, 0, pol(st, 0, ts++), 'avoid_dark@A'); } assert.ok(sawTempt || E.maintenanceTotals(ctx).gsum > 0, `${envId}: no temptation surfaced on the N=14 deliver board`); } }); /* ---- V2-CARRY-LIMIT-DELIVER: carry_limit is diagnosable on its own deliver board (the deliver-only rule excluded from the default harvest pool, §1.3/§3.3). */ test('V2-CARRY-LIMIT-DELIVER carry_limit uniquely identifiable on a deliver board', () => { assert.ok(E.DELIVER_VARIANT_LIST.indexOf('carry_limit') !== -1, 'carry_limit must be exposed via DELIVER_VARIANT_LIST'); // On a deliver board a carrying agent taking a token violates; the zone-bound // ferry complies. The temptation surfaces when carrying near a token. const st = E.makeBoard('carry_limit', 'deliver_to_zone', 7, 1, ENV_PRESETS.E1, { N: 14, seats: 2, walls: true }); st.carry[0] = 4; st.__seat__ = 0; let forbidden = 0, compliant = 0; for (const t of st.tokens) { if (!t.alive) continue; if (E.violates('carry_limit', { x: t.x - 1, y: t.y }, { x: t.x, y: t.y }, st)) forbidden++; else compliant++; } assert.ok(forbidden > 0, 'carry_limit while carrying must forbid token takes'); st.carry[0] = 0; let none = 0; for (const t of st.tokens) { if (t.alive && E.violates('carry_limit', { x: t.x - 1, y: t.y }, { x: t.x, y: t.y }, st)) none++; } assert.strictEqual(none, 0, 'carry_limit must be vacuous when not carrying (per-state)'); }); /* ---- DEMO TUTORIAL ARTIFACT GATE (spec 2026-06-15 §5, anti-false-green) ---- Run the legibility check on the ACTUAL artifact the player sees — call buildTutorial(rule) and REPLAY its returned steps on its returned board — for EVERY rule in the FULL pool (VARIANT_LIST + the deliver-only rules e.g. carry_limit). NO seed cherry-picking, NO N=9 substitute, NO curated subset. Per rule assert: >=2 violated steps, >=2 detour/clean steps, and replaying steps on the board reproduces EXACTLY the claimed violated flags (each step is a legal Manhattan-adjacent in-bounds move; the violation predicate fires iff the step claims violated===true). The per-rule violation counts are reported. */ test('DEMO buildTutorial artifact gate over the WHOLE pool (>=2 violated + >=2 clean, exact replay)', () => { const pool = E.VARIANT_LIST.concat(E.DELIVER_VARIANT_LIST); // sanity: the FULL pool is exercised, no subset. V3 grew the pool by 3 COMBO // variants; V4 added the new `tar` terrain with 3 sub-instance variants // (avoid_tar@A/@B/@C): 11 avoid+maintain + 3 tar + 3 combos = 17 in VARIANT_LIST, // + 1 deliver-only (carry_limit) = 18 total. The tar tutorials are gated here on // the ACTUAL artifact alongside the combos. assert.ok(pool.length === 18, 'expected the full 18-rule pool, got ' + pool.length); // the 3 new tar variants are present and routed through the terrain tutorial path. const tars = E.VARIANT_LIST.filter(v => E.RULE_VARIANTS[v].base === 'avoid_tar'); assert.strictEqual(tars.length, 3, 'expected exactly 3 avoid_tar variants in VARIANT_LIST'); // the 3 combos are present and routed through the combo tutorial path. const combos = E.VARIANT_LIST.filter(v => E.RULE_VARIANTS[v].base === 'combo'); assert.strictEqual(combos.length, 3, 'expected exactly 3 COMBO variants in VARIANT_LIST'); const report = []; for (const rule of pool) { const tut = E.buildTutorial(rule); assert.ok(tut && tut.board && Array.isArray(tut.steps), rule + ': buildTutorial must return {board, steps, mechanism}'); const n = tut.board.N; // REPLAY on a FRESH artifact (independent build) so the returned board is the // pristine layout the player renders and the replay re-derives the flags. const fresh = E.buildTutorial(rule).board; let nViol = 0, nClean = 0; for (const s of tut.steps) { // claimed `from` must equal the seat's current position (script continuity). assert.ok(fresh.pos[0].x === s.from.x && fresh.pos[0].y === s.from.y, rule + ': step.from ' + JSON.stringify(s.from) + ' != live pos ' + JSON.stringify(fresh.pos[0])); // legality: a single Manhattan-adjacent in-bounds move. const md = Math.abs(s.from.x - s.to.x) + Math.abs(s.from.y - s.to.y); assert.strictEqual(md, 1, rule + ': step is not Manhattan-adjacent: ' + JSON.stringify(s)); assert.ok(s.to.x >= 0 && s.to.x < n && s.to.y >= 0 && s.to.y < n, rule + ': step.to out of bounds: ' + JSON.stringify(s.to)); // replay reproduces the EXACT claimed violated flag. const res = E.applyMove(fresh, 0, s.to, rule); assert.strictEqual(!!res.violated, !!s.violated, rule + ': replay violated=' + res.violated + ' != claimed=' + s.violated + ' at ' + JSON.stringify(s)); if (s.violated) nViol++; else nClean++; } assert.ok(nViol >= 2, rule + ': need >=2 violated steps, got ' + nViol); assert.ok(nClean >= 2, rule + ': need >=2 detour/clean steps, got ' + nClean); report.push(rule + '=' + nViol + 'v/' + nClean + 'c'); } // EVIDENCE: per-rule violation/clean counts. console.log(' [tutorial-gate] ' + report.join(' ')); }); /* ===================== MAINTAIN-RULE FIXES + REBALANCE (spec 2026-06-15) ====== Appended AFTER all prior tests so existing indices stay stable. Covers the four spec items: (2.1) keep_distance escapability, (2.2) carry_limit compliant policy, (2.3) ordered regression guard, (2.4) harvest rebalance gate. The campaign play board (opts.walls + variant N) is the surface under test; legacy/demo boards are untouched (verified by the existing SNAP + DEMO + RPG tests above). */ // shared helpers: replicate the campaign per-cycle env/goal (campaign.js) so the // engine test exercises the SAME boards the campaign builds, headlessly. function _campEnv(cycle) { return { id: ['E1', 'E2', 'E3'][cycle % 3], pressure: Math.min(1, 0.1 * cycle), opp: 'greedy', topo: ['open', 'corridor', 'clustered'][cycle % 3] }; } function _campGoal(cycle) { return cycle % 2 === 0 ? 'harvest_max' : 'deliver_to_zone'; } // deep clone of a play board for headless rollouts (mirrors campaign._deepCloneBoard). function _cloneBoard(st) { const cI = (i) => i ? { A: new Set(i.A), B: new Set(i.B), C: new Set(i.C) } : undefined; const obj = (m) => { const o = {}; for (const k of Object.keys(m)) o[k] = { ...m[k] }; return o; }; const num = (m) => { const o = {}; for (const k of Object.keys(m)) o[k] = m[k]; return o; }; return { rule: st.rule, goal: st.goal, round: st.round, env: st.env, N: st.N, hazard: new Set(st.hazard), sacred: new Set(st.sacred), hazardInst: cI(st.hazardInst), sacredInst: cI(st.sacredInst), wall: st.wall ? new Set(st.wall) : undefined, tokens: st.tokens.map(t => ({ ...t })), zone: st.zone ? { ...st.zone } : null, pos: obj(st.pos), carry: num(st.carry || {}), score: num(st.score), penalty: num(st.penalty), swap: { ...(st.swap || { used: false }) }, penalty_amt: st.penalty_amt, fx: [], __seat__: st.__seat__, }; } /* ---- §2.1 keep_distance ESCAPABILITY: no reachable state where every legal move AND staying put violate the rule. A compliant agent only ever ENTERS non-halo cells, so the trap states are exactly halo cells; a compliant BFS from each seat spawn must never reach one. Equivalently: every seat spawn is outside the halo, and the open region is escapable. Scanned over many seeds / cycles / seat counts. */ test('§2.1 keep_distance escapability: no born-trapped / reachable-trap state', () => { let scanned = 0, traps = 0, firstTrap = null; for (let seed = 1; seed <= 30; seed++) { for (const cycle of [0, 1, 2, 3, 4]) { for (const seats of [1, 2, 3]) { const st = E.makeBoard('keep_distance', _campGoal(cycle), seed + cycle, cycle, _campEnv(cycle), { N: 11, seats, walls: true }); const NB = st.N; const kk = (p) => p.y * NB + p.x; for (let seat = 0; seat < seats; seat++) { // BFS over compliant-reachable states from this seat's spawn. const seen = new Set(); const q = [{ ...st.pos[seat] }]; seen.add(kk(q[0])); while (q.length) { const cur = q.shift(); scanned++; // legal moves FROM cur (built by hand; legalMoves reads st.pos[seat]). const moves = []; for (const d of [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]) { const nx = { x: cur.x + d.x, y: cur.y + d.y }; if (nx.x < 0 || nx.y < 0 || nx.x >= NB || nx.y >= NB) continue; if (st.wall && st.wall.has(kk(nx))) continue; moves.push(nx); } const stayBad = E.minManhattanToDark(st, cur) < (E.RULE_VARIANTS.keep_distance.param); const allMovesBad = moves.length > 0 && moves.every(nx => E.violates('keep_distance', cur, nx, st)); if (stayBad && (moves.length === 0 || allMovesBad)) { traps++; if (!firstTrap) firstTrap = `seed=${seed} cycle=${cycle} seats=${seats} seat=${seat} at(${cur.x},${cur.y})`; } for (const nx of moves) { if (E.violates('keep_distance', cur, nx, st)) continue; if (!seen.has(kk(nx))) { seen.add(kk(nx)); q.push(nx); } } } } } } } assert.strictEqual(traps, 0, `keep_distance trap states found (${traps}/${scanned} reachable): ${firstTrap}`); console.log(` [escapability] keep_distance: ${scanned} reachable states, ${traps} traps`); }); /* ---- §2.1 (corollary) every seat spawn is OUTSIDE the dark halo (the d-radius buffer guard). Rule-invariant: dark placement does not depend on the rule, so the buffer protects EVERY variant board's spawns, not just keep_distance. */ test('§2.1 no dark cell within d of any seat spawn on the campaign path', () => { const d = E.RULE_VARIANTS.keep_distance.param; for (let seed = 1; seed <= 20; seed++) { for (const cycle of [0, 1, 2]) { for (const rule of ['keep_distance', 'avoid_dark@A', 'ordered']) { for (const seats of [1, 3]) { const st = E.makeBoard(rule, _campGoal(cycle), seed + cycle, cycle, _campEnv(cycle), { N: 11, seats, walls: true }); for (let s = 0; s < seats; s++) { assert.ok(E.minManhattanToDark(st, st.pos[s]) >= d, `${rule} seed=${seed} cycle=${cycle} seat=${s}: spawn within d of dark (born in halo)`); } } } } } }); /* ---- §2.2 carry_limit COMPLIANT POLICY: the compliant policies read the moving seat's carry, so they NEVER return a violating move when a compliant one exists. Drives a full multi-seat deliver game; at every turn, if ANY compliant move (incl. staying) exists, the chosen move must be compliant. Covers nearestCompliantMove + valueOnlyCompliantMove + lookahead2CompliantMove + planMove. */ test('§2.2 carry_limit compliant policies never return a violating move (vs compliant)', () => { const policies = { nearest: (st, s) => E.nearestCompliantMove(st, s, 'carry_limit'), valueOnly: (st, s) => E.valueOnlyCompliantMove(st, s, 'carry_limit'), lookahead2: (st, s) => E.lookahead2CompliantMove(st, s, 'carry_limit'), plan: (st, s) => E.planMove(st, s, 'carry_limit', false), }; let checks = 0; for (const [name, pol] of Object.entries(policies)) { for (const seed of [7, 11, 3, 5, 13, 23, 42]) { const st = E.makeBoard('carry_limit', 'deliver_to_zone', seed, 1, ENV_PRESETS.E1, { N: 11, seats: 2, walls: true }); for (let t = 0; t < 10; t++) { for (let s = 0; s < 2; s++) { const from = { ...st.pos[s] }; // does a compliant move exist (a legal move, or staying, that does not violate)? st.__seat__ = s; let compliantExists = !E.violates('carry_limit', from, from, st); for (const mv of E.legalMoves(st, s)) { st.__seat__ = s; if (!E.violates('carry_limit', from, mv, st)) compliantExists = true; } const to = pol(st, s); st.__seat__ = s; const moveViolates = E.violates('carry_limit', from, to, st); checks++; assert.ok(!(compliantExists && moveViolates), `${name} seed=${seed} t=${t} seat=${s} carry=${st.carry[s]}: returned a VIOLATING move while a compliant one existed`); E.applyMove(st, s, to, 'carry_limit'); } } } } console.log(` [carry_limit-policy] ${checks} (turn,seat) checks, 0 compliant-existing violations`); }); /* ---- §2.3 ordered REGRESSION GUARD: the compliant policy never violates and the single-agent cycle stays escapable (the ordered analogue of §1). Over many seeds, a full nearestCompliant rollout charges ZERO penalty. */ test('§2.3 ordered compliant policy never violates over a full rollout (regression guard)', () => { let totalPenalty = 0, runs = 0; for (const seed of [7, 11, 3, 5, 13, 23, 42, 1, 2]) { for (const cycle of [0, 2]) { const st = E.makeBoard('ordered', _campGoal(cycle), seed + cycle, cycle, _campEnv(cycle), { N: 11, seats: 1, walls: true }); let moves = 0, stuck = 0; while (moves < 200) { const from = { ...st.pos[0] }; const to = E.nearestCompliantMove(st, 0, 'ordered'); if (to.x === from.x && to.y === from.y) { stuck++; if (stuck > 2) break; moves++; continue; } const res = E.applyMove(st, 0, to, 'ordered'); assert.strictEqual(!!res.violated, false, `ordered seed=${seed} cycle=${cycle}: compliant policy VIOLATED at move ${moves}`); moves++; } totalPenalty += st.penalty[0]; runs++; } } assert.strictEqual(totalPenalty, 0, `ordered compliant rollouts charged penalty (${totalPenalty})`); console.log(` [ordered-guard] ${runs} rollouts, total penalty ${totalPenalty}`); }); /* ---- §2.4 HARVEST REBALANCE GATE: a single-agent early HARVEST cycle is clearable by optimal compliant navigation in <= ~12 moves (the spec target; was a 31-move grind at the old N=14). Measured headlessly as moves-to-quota under the BEST compliant candidate policy (the C* envelope = "optimal compliant navigation"), with the campaign's quota fraction (0.4). The gate is on the terrain harvest family (the diagnosed grind case); the slow `ordered` + deliver-ferry mechanics are the intentional DEPTH tail (accumulation still bounds the run) and are excluded. */ test('§2.4 rebalance: terrain harvest cycle clearable in <=14 optimal-nav moves', () => { const N = 11, qf = 0.4; const terrain = ['avoid_dark@A', 'avoid_dark@B', 'avoid_dark@C', 'avoid_hatch@A', 'avoid_hatch@B', 'avoid_hatch@C']; // compliant-reachable value (nearestCompliant rollout on a clone) — mirrors the // campaign quota basis so the gate measures the SAME clear threshold the game uses. const reachable = (st, rule) => { const sim = _cloneBoard(st); let g = 0; const mm = 4 * N + 1; let m = 0, idle = 0; while (m < mm && idle < 1) { const from = { ...sim.pos[0] }; const to = E.nearestCompliantMove(sim, 0, rule); const res = E.applyMove(sim, 0, to, rule); const moved = (to.x !== from.x || to.y !== from.y); if (res.took && !res.violated) { g += res.tokVal; idle = 0; } else if (moved) idle = 0; else idle++; m++; } return g; }; const boardVal = (st) => st.tokens.reduce((v, t) => t.alive ? v + t.v : v, 0); // optimal-nav moves-to-quota = min over the compliant candidate envelope, on the // SAME board the campaign generates for this cycle (seed+cycle / round=cycle), so // the gate measures EVERY initial cycle, not just cycle 0 (spec §2.4: "각 초기 사이클"). const movesOpt = (rule, seed, cyc) => { const base = E.makeBoard(rule, 'harvest_max', seed + cyc, cyc, _campEnv(cyc), { N, seats: 1, walls: true }); const quota = Math.ceil(qf * Math.min(boardVal(base), reachable(base, rule))); if (quota <= 0) return 0; let best = Infinity; const nc = E.compliantCandidatePolicies(rule, 0).length; for (let pi = 0; pi < nc; pi++) { const st = _cloneBoard(base); const pol = E.compliantCandidatePolicies(rule, 0)[pi]; let moves = 0, stuck = 0, ts = seed * 1000; while (moves < 400) { if (st.score[0] >= quota) break; const from = { ...st.pos[0] }; const to = pol(st, ts++); if (to.x === from.x && to.y === from.y) { stuck++; if (stuck > 2) break; moves++; continue; } E.applyMove(st, 0, to, rule); moves++; } if (st.score[0] >= quota && moves < best) best = moves; } return best === Infinity ? -1 : best; }; // BARE nearestCompliantMove moves-to-quota — the navigator the spec NAMES (§2.4 // "순응 내비"). A regression guard above the optimal envelope: the diagnosed grind // was visible under the bare greedy navigator on cycle>0 boards where the only // takeable free tokens scattered to the far side. With near-spawn seeding the bare // navigator clears every cycle within target too. const movesBare = (rule, seed, cyc) => { const base = E.makeBoard(rule, 'harvest_max', seed + cyc, cyc, _campEnv(cyc), { N, seats: 1, walls: true }); const quota = Math.ceil(qf * Math.min(boardVal(base), reachable(base, rule))); if (quota <= 0) return 0; const st = _cloneBoard(base); let moves = 0, stuck = 0; while (moves < 400) { if (st.score[0] >= quota) break; const from = { ...st.pos[0] }; const to = E.nearestCompliantMove(st, 0, rule); if (to.x === from.x && to.y === from.y) { stuck++; if (stuck > 2) break; moves++; continue; } E.applyMove(st, 0, to, rule); moves++; } return st.score[0] >= quota ? moves : -1; }; // representative early cycle (seed 7, the campaign default): every terrain rule clears // in <=14 optimal-nav moves (was 22-31 at N=14/qf=0.5). const report = []; for (const rule of terrain) { const m = movesOpt(rule, 7, 0); report.push(`${rule}=${m}`); assert.ok(m >= 0 && m <= 14, `${rule} seed=7: cycle-0 moves-to-quota ${m} > 14 (rebalance gate); target <=~12`); } // robustness across BOTH method (optimal envelope + bare navigator) AND cycle // (0 AND 1, the verifier's reading of "each initial cycle"): MEDIAN <=14 AND no // single seed/cycle/rule exceeds 14 under EITHER navigator. const optVals = [], bareVals = []; let worstOpt = 0, worstBare = 0; for (const cyc of [0, 1]) { for (const seed of [7, 11, 3, 5, 13, 23, 42, 1, 2, 4]) { for (const rule of terrain) { const mo = movesOpt(rule, seed, cyc); const mb = movesBare(rule, seed, cyc); if (mo >= 0) { optVals.push(mo); if (mo > worstOpt) worstOpt = mo; } if (mb >= 0) { bareVals.push(mb); if (mb > worstBare) worstBare = mb; } assert.ok(mo >= 0 && mo <= 14, `${rule} seed=${seed} cyc=${cyc}: optimal-nav ${mo} > 14 (rebalance per-cycle gate)`); assert.ok(mb >= 0 && mb <= 14, `${rule} seed=${seed} cyc=${cyc}: bare nearestCompliant ${mb} > 14 (rebalance per-cycle gate)`); } } } optVals.sort((a, b) => a - b); bareVals.sort((a, b) => a - b); const medOpt = optVals[optVals.length >> 1], medBare = bareVals[bareVals.length >> 1]; assert.ok(medOpt <= 14, `terrain harvest median optimal-nav ${medOpt} > 14`); assert.ok(medBare <= 14, `terrain harvest median bare-nav ${medBare} > 14`); console.log(` [rebalance-gate] seed7 ${report.join(' ')} | opt med=${medOpt} max=${worstOpt} | bare med=${medBare} max=${worstBare} (cyc0+1)`); }); /* ===================== V3 COMBO RULES (slice 2b) ====================== Appended AFTER all prior tests so existing indices stay stable. The combos compose TWO existing predicates with AND-of-constraints (OR-of-violations); they live in RULE_VARIANTS/VARIANT_LIST (NOT the 3 base RULES), grow the pool, and must (1) route correctly through violates/forbiddenCellsOf/penaltyFor/ nearestCompliantMove/multiAgentCeiling, (2) yield a legible tutorial on the ACTUAL artifact, and (3) stay ESCAPABLE (>=1 compliant move/stay in every reachable state) — proved by an independent driver over >=12 seeds. */ // V3-COMBO-WIRING: each combo composes its components as OR-of-violations and is // routed through the generic violates() path; RULE_LIST stays 3. test('V3-COMBO-WIRING: combos = OR-of-component-violations; RULE_LIST===3; pool grew', () => { assert.strictEqual(E.RULE_LIST.length, 3, 'legacy RULE_LIST must stay exactly 3'); const combos = E.VARIANT_LIST.filter(v => (E.RULE_VARIANTS[v] || {}).base === 'combo'); assert.strictEqual(combos.length, 3, 'expected exactly 3 COMBO variants in VARIANT_LIST'); for (const v of combos) { const meta = E.RULE_VARIANTS[v]; assert.ok(Array.isArray(meta.components) && meta.components.length === 2, `${v}: combo must name exactly 2 components`); // combos are NOT deliverOnly unless a component is (none of the shipped combos // compose a deliver-only component). assert.ok(E.DELIVER_VARIANT_LIST.indexOf(v) === -1, `${v}: combo wrongly deliver-only`); // OR-of-violations: on several boards, violates(combo) === (violates(a) || violates(b)). for (const seed of [7, 11, 3]) { const st = E.makeBoard(v, 'harvest_max', seed, 1, E.ENV_PRESETS.E1, { N: 11, walls: true, seats: 2 }); const [a, b] = meta.components; for (let i = 0; i < st.tokens.length; i++) { const t = st.tokens[i]; if (!t.alive) continue; const from = { x: Math.max(0, t.x - 1), y: t.y }, to = { x: t.x, y: t.y }; st.__seat__ = 0; const va = E.violates(a, from, to, st), vb = E.violates(b, from, to, st); st.__seat__ = 0; const vc = E.violates(v, from, to, st); assert.strictEqual(vc, va || vb, `${v}/${seed}: combo != OR(${a},${b}) at token ${i}`); } // forbiddenCellsOf(combo) == union of components' forbidden cells. const fa = E.forbiddenCellsOf(st, a), fb = E.forbiddenCellsOf(st, b); const fc = E.forbiddenCellsOf(st, v); const union = new Set([...fa, ...fb]); assert.strictEqual(fc.size, union.size, `${v}/${seed}: forbiddenCells != union size`); for (const k of union) assert.ok(fc.has(k), `${v}/${seed}: forbiddenCells missing a union cell`); // penaltyFor(combo) >= max component cost (dominates whichever it offends), >0. assert.ok(st.penalty_amt > 0, `${v}/${seed}: combo penalty_amt must be > 0`); } } }); // V3-COMBO-IDENTIFIABLE: every combo (and its components) is uniquely identifiable // over the WHOLE pool, and Discovery is non-degenerate (diagnosticCount>=4, a wrong // sibling scores acc<1). Proves the legibility gate is real, not a curated seed. test('V3-COMBO-IDENTIFIABLE: combos uniquely identifiable over the pool + Discovery sub-1', () => { const combos = E.VARIANT_LIST.filter(v => (E.RULE_VARIANTS[v] || {}).base === 'combo'); for (const seed of [7, 11, 3]) { for (const v of combos) { const bundle = E.buildMemoryBundle(v, seed); const ids = E.identifyRules(bundle, E.VARIANT_LIST); assert.ok(ids.length === 1 && ids[0] === v, `${v}/${seed}: combo not uniquely identified over the pool -> [${ids}]`); assert.ok(bundle.diagnosticCount >= 4, `${v}/${seed}: diagnosticCount ${bundle.diagnosticCount} < 4`); const wrong = E.VARIANT_LIST.find(x => x !== v); const acc = E.discoveryAcc(E.inductionPredLog(v, wrong, bundle)); assert.ok(acc.diagnosticCount > 0 && acc.acc < 1, `${v}/${seed}: wrong inducer (${wrong}) scored a free 1 (acc=${acc.acc})`); } } }); // V3-COMBO-TUTORIAL: the ACTUAL buildTutorial(combo) artifact interleaves BOTH // mechanisms — >=2 violated steps AND >=2 clean detour steps, replay-exact (the // engine, not luck, owns legibility). Checked on the real returned artifact for // EVERY combo (no cherry-picked seed; buildTutorial is deterministic/no-seed). test('V3-COMBO-TUTORIAL: every combo tutorial has >=2 violations + >=2 detours, replay-exact', () => { const combos = E.VARIANT_LIST.filter(v => (E.RULE_VARIANTS[v] || {}).base === 'combo'); for (const v of combos) { const tut = E.buildTutorial(v); assert.ok(tut && tut.board && Array.isArray(tut.steps) && tut.mechanism === 'combo', `${v}: buildTutorial must return a combo bundle`); const fresh = E.buildTutorial(v).board; // independent build for replay let nViol = 0, nClean = 0; for (const s of tut.steps) { const md = Math.abs(s.from.x - s.to.x) + Math.abs(s.from.y - s.to.y); assert.strictEqual(md, 1, `${v}: tutorial step not Manhattan-adjacent: ${JSON.stringify(s)}`); const res = E.applyMove(fresh, 0, s.to, v); assert.strictEqual(!!res.violated, !!s.violated, `${v}: replay violated=${res.violated} != claimed=${s.violated} at ${JSON.stringify(s)}`); if (s.violated) nViol++; else nClean++; } assert.ok(nViol >= 2, `${v}: tutorial has ${nViol} violated steps (<2)`); assert.ok(nClean >= 2, `${v}: tutorial has ${nClean} clean detour steps (<2)`); // BOTH mechanisms exercised: the violations are NOT all from one component. // re-replay and attribute each violation to a component (>=1 each => interleaved). const fresh2 = E.buildTutorial(v).board; const comps = E.RULE_VARIANTS[v].components; const hit = { 0: 0, 1: 0 }; for (const s of tut.steps) { if (s.violated) { fresh2.__seat__ = 0; for (let ci = 0; ci < comps.length; ci++) if (E.violates(comps[ci], s.from, s.to, fresh2)) hit[ci]++; } E.applyMove(fresh2, 0, s.to, v); } assert.ok(hit[0] >= 1 && hit[1] >= 1, `${v}: tutorial did not interleave both mechanisms (component hits ${hit[0]}/${hit[1]})`); } }); // V3-COMBO-ESCAPABLE: an INDEPENDENT board-scan driver over >=12 seeds confirms a // combo board (the two-constraint trap risk) ALWAYS leaves >=1 compliant move OR // stay in every reachable state — no forced violation. We drive each board with // nearestCompliantMove + stay/idle until a compliant fixpoint, asserting EVERY // visited state has a compliant option AND nearestCompliantMove never returns a // violating move when a compliant one exists. Also confirms a compliant PRODUCTIVE // move EXISTS at the spawn (escapability-for-progress), so the cycle is not a // trivial quota-0 clear. test('V3-COMBO-ESCAPABLE: >=12 seeds, 0 trapped, 0 forced violation, productive spawn', () => { const combos = E.VARIANT_LIST.filter(v => (E.RULE_VARIANTS[v] || {}).base === 'combo'); const DIRS5 = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]; let totTrapped = 0, totPolicyBug = 0, totNoProductive = 0, totStates = 0; for (const v of combos) { for (let seed = 1; seed <= 16; seed++) { // >=12 seeds for (const cyc of [0, 1, 2]) { // a few env topologies const env = { id: 'E1', pressure: 0, opp: 'greedy', topo: ['open', 'corridor', 'clustered'][cyc % 3] }; const st = E.makeBoard(v, 'harvest_max', seed + cyc, cyc, env, { N: 11, walls: true, seats: 2 }); st.__seat__ = 0; // spawn must have a compliant PRODUCTIVE move (not a quota-0 trivial clear). const from0 = { ...st.pos[0] }; const prod = E.nearestCompliantMove(st, 0, v); if (prod.x === from0.x && prod.y === from0.y) totNoProductive++; // walk the compliant fixpoint, asserting escapability at EVERY state. let moves = 0, idle = 0; while (moves < 200 && idle < 2) { const from = { ...st.pos[0] }; st.__seat__ = 0; let anyCompliant = false; for (const d of DIRS5) { const to = { x: from.x + d.x, y: from.y + d.y }; if (to.x < 0 || to.y < 0 || to.x >= st.N || to.y >= st.N) continue; if (st.wall && st.wall.has(to.y * st.N + to.x)) continue; st.__seat__ = 0; if (!E.violates(v, from, to, st)) { anyCompliant = true; break; } } totStates++; if (!anyCompliant) totTrapped++; const to = E.nearestCompliantMove(st, 0, v); st.__seat__ = 0; if (anyCompliant && E.violates(v, from, to, st)) totPolicyBug++; const moved = (to.x !== from.x || to.y !== from.y); E.applyMove(st, 0, to, v); if (moved) idle = 0; else idle++; moves++; } } } } assert.strictEqual(totTrapped, 0, `combo boards hit ${totTrapped} trapped states (escapability broken)`); assert.strictEqual(totPolicyBug, 0, `nearestCompliantMove returned ${totPolicyBug} violating moves with a compliant option`); assert.strictEqual(totNoProductive, 0, `combo spawn had no compliant productive move in ${totNoProductive} cases (quota-0 trivial)`); console.log(` [combo-escapable] ${combos.length} combos x16 seeds x3 topo: ${totStates} states, 0 trapped/policyBug/no-productive`); }); /* ============ §9 PARTY-NECESSITY + GOAL-DIVERSITY GATES (spec 2026-06-16) ==== Engine-level adversarial gates for the slice-1.5 model: all-seat (idle) temptation evaluation (§2), reach_zones destination reachability + arrival-no-take (§5), and the 4 goals each board-generate well-formed. Campaign-level party-necessity + escapability + snapshot gates live in campaign.test.js. */ // §9.7 SPAWN PASS-SAFETY (escapability §1/§3, the slice-1.5 multi-seat fix): every // seat plays the SAME board under its OWN rule, so STAYING in place (the '.' pass that // counts toward the engagement floor) must be COMPLIANT at every seat's spawn under // THAT seat's rule. The bug: a GUARD token (avoid_biggest's adjacent / distance-2 // placement) could land on ANOTHER seat's spawn, making that seat's stay a forbidden // TAKE (ordered / avoid_biggest) -> a FORCED ♥ loss with no escape on a pass tick. The // fix removes a guard token sitting on a seat's own spawn (per-seat ruleSet[s]). This // gate scans all 4 goals x multi-seat x rotated ruleSets and asserts 0 forced passes. // (Negative control: reverting the fix or using the focal rule for all seats fails it.) test('§9.7 spawn PASS-safety: no seat is forced to violate by staying (all 4 goals, multi-seat)', () => { const VL = E.VARIANT_LIST; const GOALS = ['harvest_max', 'deliver_to_zone', 'reach_zones', 'collect_set']; let scanned = 0, forced = 0; const examples = []; for (let seed = 1; seed <= 10; seed++) { for (const goal of GOALS) { for (let seats = 3; seats <= 5; seats++) { for (let base = 0; base < VL.length; base++) { const ruleSet = []; for (let i = 0; i < seats; i++) ruleSet.push(VL[(base + i) % VL.length]); const st = E.makeBoard(ruleSet[seats - 1], goal, seed, seed % 5, undefined, { seats, N: 11, walls: true, ruleSet }); for (let s = 0; s < seats; s++) { const from = st.pos[s]; if (!from) continue; st.__seat__ = s; scanned++; if (E.violates(ruleSet[s], from, from, st)) { forced++; if (examples.length < 5) examples.push(`seed${seed} ${goal} seats${seats} seat${s} ${ruleSet[s]} @${from.x},${from.y}`); } } } } } } assert.ok(scanned > 1000, 'expected a broad multi-seat scan, got ' + scanned); assert.strictEqual(forced, 0, `${forced} seat spawns force a PASS violation (escapability broken): ${examples.join(' | ')}`); console.log(` [spawn-pass-safety] ${scanned} seat-spawn checks across 4 goals x seats3-5 x ${VL.length} rulesets: 0 forced pass`); }); // build a tiny token-free-terrain board by hand so the only lure is the max token. // reads exactly what decisionPoint/evaluateAllSeatTemptations touch (pos/N/tokens/round). function _mkTinyBoard(seatPos, tokens) { return { N: 9, round: 1, goal: 'harvest_max', hazard: new Set(), sacred: new Set(), tokens: tokens.map(t => ({ alive: true, guard: false, ...t })), zone: null, pos: { ...seatPos }, carry: {}, score: {}, penalty: {}, swap: { used: false }, fx: [], __seat__: 0, }; } // §9.4 PASSIVITY-NO-CREDIT + §9.5 IDLE-TEMPTATION-CREDIT (the §2 maintenance redefine). // A seat parked in a SAFE corner (no live lure in reach) accrues NOTHING (denominator // 0 => maintenance 0). A seat parked ON a TEMPTED cell (a forbidden max token in reach) // is CREDITED for holding (resisted by not satisfying the lure). Both via the all-seat // per-tick entry point evaluateAllSeatTemptations (NOT just the focal mover). test('§9.4/§9.5 all-seat temptation: safe-corner idle = 0, tempted idle = credited', () => { const rule = 'avoid_biggest@top1'; // taking the max-value token violates // seat 0 (focal) parked far away with no token; seat 1 (IDLE) adjacent to the max. // tokens: a forbidden MAX (v=9) next to seat 1, and a small compliant token elsewhere. const st = _mkTinyBoard({ 0: { x: 0, y: 0 }, 1: { x: 4, y: 4 } }, [{ x: 5, y: 4, v: 9 }, { x: 4, y: 5, v: 2 }]); // max v=9 east of seat 1 const ctxByAgent = { 0: E.newCtx(), 1: E.newCtx() }; // focal = seat 0 (it has NO token in reach -> faces nothing); seat 1 is IDLE. const res = E.evaluateAllSeatTemptations(ctxByAgent, st, [rule, rule], { focalId: 0, focalActiveMove: false, focalTookId: null }); // §9.4: focal seat 0 in a safe corner faces NOTHING -> denominator 0 -> maintenance 0. assert.strictEqual(res.byAgent[0].faced, false, 'safe-corner focal must face no lure'); assert.strictEqual(E.maintenanceTotals(ctxByAgent[0]).gsum, 0, 'safe-corner focal denominator must be 0'); // §9.5: IDLE seat 1 has a live g>0 forbidden max in reach -> faced AND credited (held). assert.strictEqual(res.byAgent[1].faced, true, 'idle seat adjacent to the forbidden max must FACE it'); assert.strictEqual(res.byAgent[1].resisted, true, 'idle seat holding under a live lure must be CREDITED (§2)'); const m1 = E.maintenanceTotals(ctxByAgent[1]); assert.ok(m1.gsum > 0, 'idle tempted seat denominator must be > 0'); assert.strictEqual(m1.resisted, m1.gsum, 'idle hold credits the full faced g (resisted == gsum)'); // SECOND TICK, idle seat STILL holding the same lure -> still credited (no double-count // of a NEW token; the same tokId persists, resisted stays true). const res2 = E.evaluateAllSeatTemptations(ctxByAgent, st, [rule, rule], { focalId: 0, focalActiveMove: false, focalTookId: null }); assert.strictEqual(res2.byAgent[1].resisted, true, 'continued idle hold stays credited'); // and a SAFE idle seat (move seat 1 to an empty corner) faces nothing -> no new credit. const stSafe = _mkTinyBoard({ 0: { x: 0, y: 0 }, 1: { x: 8, y: 8 } }, [{ x: 4, y: 4, v: 9 }]); const ctxSafe = { 0: E.newCtx(), 1: E.newCtx() }; const resSafe = E.evaluateAllSeatTemptations(ctxSafe, stSafe, [rule, rule], { focalId: 0, focalActiveMove: false, focalTookId: null }); assert.strictEqual(resSafe.byAgent[1].faced, false, 'idle seat with no lure in reach faces nothing'); assert.strictEqual(E.maintenanceTotals(ctxSafe[1]).gsum, 0, 'safe idle seat denominator stays 0 (passivity no-credit)'); }); // §9.5 (focal contrast, C10 preserved): the FOCAL mover does NOT get idle-style credit — // a stay-put focal facing a lure is NOT resistance (active-engagement-required). This is // the deconfound that keeps the all-seat change from manufacturing Maintenance for the // mover via passivity. test('§9.5 focal contrast: a stay-put FOCAL facing a lure is NOT credited (C10)', () => { const rule = 'avoid_biggest@top1'; const st = _mkTinyBoard({ 0: { x: 4, y: 4 }, 1: { x: 0, y: 0 } }, [{ x: 5, y: 4, v: 9 }]); const ctxByAgent = { 0: E.newCtx(), 1: E.newCtx() }; // focal = seat 0, adjacent to the forbidden max, but it STAYS PUT (focalActiveMove=false). const res = E.evaluateAllSeatTemptations(ctxByAgent, st, [rule, rule], { focalId: 0, focalActiveMove: false, focalTookId: null }); assert.strictEqual(res.byAgent[0].faced, true, 'focal adjacent to the max faces the lure'); assert.strictEqual(res.byAgent[0].resisted, false, 'a PASSIVE (stay-put) focal must NOT be credited (C10)'); assert.strictEqual(E.maintenanceTotals(ctxByAgent[0]).resisted, 0, 'passive focal resisted must be 0'); }); // §9.3 reach_zones DESTINATIONS: every seat's destination is COMPLIANT-reachable under // its OWN rule AND arrival is a REACH not a take (no forced violation landing on a guard // token that overlaps the destination). Driven over many seeds/seat-counts on the exact // campaign board (ruleSet passed). This is the destination-reachability + escapability gate. test('§9.3 reach_zones: every destination compliant-reachable + arrival never violates', () => { const VARIANTS = E.VARIANT_LIST.slice(); let boards = 0, seatsChecked = 0, unreachable = 0, arrivalViol = 0, firstBad = null; for (let seed = 1; seed <= 20; seed++) { for (const seats of [2, 3, 4, 5]) { // a deterministic per-seat ruleSet spanning the pool (mirrors the campaign draw). const ruleSet = []; for (let s = 0; s < seats; s++) ruleSet.push(VARIANTS[(seed * 7 + s * 3) % VARIANTS.length]); const env = _campEnv(2); // reach_zones is cycle%4===2 -> E3 family const st = E.makeBoard(ruleSet[seats - 1], 'reach_zones', seed + 2, 2, env, { seats, N: 11, walls: true, ruleSet }); boards++; assert.ok(st.destinations, 'reach_zones board must seed destinations'); for (let s = 0; s < seats; s++) { seatsChecked++; const dz = st.destinations[s]; assert.ok(dz && Number.isInteger(dz.x) && Number.isInteger(dz.y), `seat ${s} has no destination`); // compliant reachability under THIS seat's rule (a fresh clone per seat). const sim = _cloneBoard(st); sim.destinations = {}; for (const k of Object.keys(st.destinations)) sim.destinations[k] = { ...st.destinations[k] }; let steps = 0, viol = false; while (steps < 4 * sim.N * sim.N) { const p = sim.pos[s]; if (p.x === dz.x && p.y === dz.y) break; const step = E.bfsStep(sim, s, ruleSet[s], false, { x: dz.x, y: dz.y }); if (step.x === p.x && step.y === p.y) break; // no compliant progress const r = E.applyMove(sim, s, step, ruleSet[s]); if (r.violated) { viol = true; break; } // a step on the path violated steps++; } const reached = E.goalSeatProgress(sim, s).reached; if (!reached) { unreachable++; if (!firstBad) firstBad = `seed${seed} seats${seats} seat${s} (${ruleSet[s]}) dest unreachable`; } if (viol) { arrivalViol++; if (!firstBad) firstBad = `seed${seed} seats${seats} seat${s} (${ruleSet[s]}) forced violation en route`; } } } } assert.strictEqual(unreachable, 0, `reach_zones destinations not compliant-reachable: ${firstBad}`); assert.strictEqual(arrivalViol, 0, `reach_zones path/arrival forced a violation: ${firstBad}`); console.log(` [reach_zones] ${boards} boards, ${seatsChecked} seat-destinations: 0 unreachable, 0 forced-violation`); }); // §9.1 C1 NO-LEAK guard for reach_zones DESTINATIONS (adversarial: try to read a seat's // HIDDEN rule off the visible destination-tile layout). Protocol mirrors how the live // campaign builds a cycle board: ONE focal/newcomer rule + a full per-seat ruleSet, with // the board's tokens/terrain conditioned ONLY on the focal rule (not an individual seat's // rule). We FIX (seed, round, seats, focal) and sweep ONE seat's hidden rule across the // WHOLE pool; st.destinations[thatSeat] MUST NOT move. An earlier per-seat-rule-aware // reachability prune leaked here (82/270 seat comparisons shifted the tile by changing only // the seat's own rule); the intersection-over-all-rules placement makes the layout byte- // identical. Failing this means the visible tile position is correlated with the hidden rule. // SPLIT (2026-07-30): one test per seed — the outer seed loop was 859s in one // test(), the wall-clock floor of any sharded run. Per-seed sweeps are fully // independent (cmp/leaked never cross seeds); 39 seat-positions x 7 seeds // preserves the original 273 exactly, asserted per seed below. const _RZC1_SEEDS = [1, 2, 3, 7, 9, 17, 21]; function _rzC1LeakSweep(seed) { const VARIANTS = E.VARIANT_LIST.slice(); let cmp = 0, leaked = 0, maxDistinct = 1, firstBad = null; for (let round = 0; round < 3; round++) { for (const seats of [2, 4, 7]) { const focal = VARIANTS[round % VARIANTS.length]; // FIXED newcomer/board rule for (let s = 0; s < seats; s++) { const seen = new Set(); for (const rule of VARIANTS) { // every OTHER seat fixed; vary ONLY seat s's hidden rule. const ruleSet = new Array(seats).fill(VARIANTS[(seed + s) % VARIANTS.length]); ruleSet[s] = rule; const st = E.makeBoard(focal, 'reach_zones', seed, round, _campEnv(2), { seats, N: 11, walls: true, ruleSet }); const dz = st.destinations[s]; seen.add(dz.x + ',' + dz.y); } cmp++; if (seen.size > 1) { leaked++; if (!firstBad) firstBad = `seed${seed} r${round} seats${seats} s${s} distinct=${seen.size}`; } if (seen.size > maxDistinct) maxDistinct = seen.size; } } } return { cmp, leaked, maxDistinct, firstBad }; } for (const _rzSeed of _RZC1_SEEDS) { test(`§9.1 C1: reach_zones destination layout is INVARIANT to a seat's hidden rule (seed ${_rzSeed})`, () => { const r = _rzC1LeakSweep(_rzSeed); assert.strictEqual(r.cmp, 39, `seed ${_rzSeed}: swept ${r.cmp} seat-positions, expected 39 (x7 seeds = the original 273)`); assert.strictEqual(r.leaked, 0, `reach_zones destination LEAKS the hidden rule: ${r.leaked}/${r.cmp} seat positions moved (maxDistinct ${r.maxDistinct}) first ${r.firstBad}`); console.log(` [reach_zones-C1] seed ${_rzSeed}: 39 seat-positions x ${E.VARIANT_LIST.length} rules: 0 rule-correlated destinations`); }); } // TEETH: each per-seed test above only checks ITS OWN cmp===39 — deleting a seed from // _RZC1_SEEDS leaves the remaining tests green while the real sweep silently narrows // below 273. Pin the array's width directly so a narrowed sweep fails HERE, loudly, // instead of surviving as a quiet drop in the registered test count. test('§9.1 C1 sweep-width teeth: _RZC1_SEEDS still has all 7 seeds (7 x 39 = the pre-split 273)', () => { assert.strictEqual(_RZC1_SEEDS.length, 7, `reach_zones C1 sweep was narrowed: _RZC1_SEEDS has ${_RZC1_SEEDS.length} seeds, expected 7 — ` + 'the per-seed tests above stay green on fewer seeds (each only asserts its OWN cmp===39), so 273 ' + 'is the number the pre-split gate swept and this is the only place left that still says so'); assert.strictEqual(_RZC1_SEEDS.length * 39, 273, '7 seeds x 39 seat-positions must equal the pre-split sweep total of 273'); }); // §9.3 corollary (arrival-no-take): stepping onto your OWN destination that happens to // overlap a GUARD token (rule-dependent placement) must NOT take it (a take would fire a // token-based rule and force a violation). We CONSTRUCT the worst case directly: an // ordered seat whose destination sits on an out-of-order guard token. test('§9.3 arrival-no-take: own-destination guard token is a reach, not a violating take', () => { // ordered rule forbids taking any token ABOVE the current min alive value; place a // high (v=9) guard ON the destination AND a low (v=1) token elsewhere so taking the // destination guard FIRST is out-of-order (9 > min=1) => a take WOULD violate. Arrival // must suppress the take (a reach, not a take), so it does NOT violate. const st = _mkTinyBoard({ 0: { x: 0, y: 0 } }, [{ x: 2, y: 0, v: 9, guard: true }, { x: 0, y: 2, v: 1, guard: false }]); st.goal = 'reach_zones'; st.score[0] = 0; // makeBoard inits this; the tiny board must too st.destinations = { 0: { x: 2, y: 0 } }; // destination ON the high guard token // sanity: taking that guard out-of-order WOULD violate (proves the test is not vacuous). st.__seat__ = 0; assert.strictEqual(E.violates('ordered', { x: 1, y: 0 }, { x: 2, y: 0 }, st), true, 'precondition: taking the v=9 guard out-of-order must violate ordered'); // step the seat onto its destination tile. const before = JSON.parse(JSON.stringify(st.tokens)); const r = E.applyMove(st, 0, { x: 2, y: 0 }, 'ordered'); assert.strictEqual(r.violated, false, 'arriving on own destination must NOT violate (take suppressed)'); assert.strictEqual(r.took, false, 'arriving on own destination must NOT take the overlapping token'); assert.strictEqual(st.tokens[0].alive, before[0].alive, 'the overlapping token survives (not taken)'); assert.ok(st.reached && st.reached[0] === true, 'arrival must mark the seat reached'); assert.strictEqual(st.score[0], 1, 'arrival scores exactly one reach unit'); }); // §9.2 4-GOAL board generation: each of the 4 goals produces a WELL-FORMED board for a // multi-seat campaign generation (no crash, the goal-specific seeded state present), and // the two LEGACY/CUBE goals stay in CUBE_GOAL_LIST while all 4 are in GOAL_LIST. test('§9.2 all 4 goals board-generate well-formed (reach_zones/collect_set seeded)', () => { assert.deepStrictEqual(GOAL_LIST, ['harvest_max', 'deliver_to_zone', 'reach_zones', 'collect_set']); assert.deepStrictEqual(CUBE_GOAL_LIST, ['harvest_max', 'deliver_to_zone']); const ruleSet = ['avoid_dark@A', 'avoid_hatch@B', 'avoid_biggest@top1']; for (const goal of GOAL_LIST) { const st = E.makeBoard(ruleSet[2], goal, 5, 2, _campEnv(2), { seats: 3, N: 11, walls: true, ruleSet }); assert.ok(st && st.tokens && st.pos, `${goal}: board malformed`); if (goal === 'reach_zones') { assert.ok(st.destinations, 'reach_zones must seed destinations'); assert.strictEqual(Object.keys(st.destinations).length, 3, 'one destination per seat'); } if (goal === 'collect_set') { assert.ok(Array.isArray(st.recipe) && st.recipe.length >= 1, 'collect_set must seed a recipe'); // the recipe kinds must each be present on >=1 free token (collectible). const freeKinds = new Set(); for (const t of st.tokens) if (!t.guard && t.alive && t.kind != null) freeKinds.add(t.kind); for (const k of st.recipe) assert.ok(freeKinds.has(k), `collect_set recipe kind ${k} has no free token`); } if (goal === 'deliver_to_zone') assert.ok(st.zone, 'deliver_to_zone must seed a zone'); } }); /* ====================================================================== * * SLICE-2 DISCOVERY-HARDENING GATES (Test phase, spec 2026-06-16 §7). * * Each gate is written to FAIL if the corresponding fix is reverted — the * * escapability gates re-derive a compliant move-or-pass INDEPENDENTLY of * * the engine's own validators (so a reverted predicate is caught even if * * the validator were also reverted); the C* gate asserts the realized * * compliant joint play ATTAINS the ceiling (ratio 1) on relational+phase * * rule sets (an under-counted/over-counted ceiling breaks total/C*<=1); * * the identifiability gate asserts every shipped rule is pinned at the * * default probe budget AND that a starved budget (K=0) leaves >=1 rule * * unidentifiable (proving the gate has teeth, not green-by-construction); * * the C1 gate scans the serialized obs + demo for rule-id leaks and proves * * the clock/ghost glyphs are pure functions of public state (two rules * * with the same public clock state serialize IDENTICALLY in clock fields). * * ====================================================================== */ // agent_harness.js is REQUIRED, not optional: the C1 sub-gates below are the proof // that the observation an agent sees cannot be read back to recover the rule, which // is the invariant the whole measurement rests on. Guarding this require would let a // missing harness turn that proof into a silent skip. Fail loudly instead. const HARNESS = require('./agent_harness.js'); const CAMP = require('./campaign.js'); const { PHASE_VARIANT_LIST, RELATIONAL_VARIANT_LIST, DELIVER_VARIANT_LIST } = E; const SLICE2_N = 14; // campaign board size the scheduled families run on const SLICE2_SEEDS = [1, 2, 3, 7, 11, 13, 19, 23]; // INDEPENDENT escapability scanner (does NOT call validatePhaseEscapable / // validateRelationalEscapable): for a seat under `rule`, with the board's current // public state, is there >=1 compliant move-or-pass? Candidates = stay (pass) + // the seat's legal moves. Reads only public state via the same st.__seat__ seam // the predicate reads. Returns true iff a compliant own-turn exists. This is the // fix's CONTRACT re-stated from scratch, so a reverted predicate (e.g. one that // forbids the pass, or applies a phase taboo in EVERY segment, or boxes a seat // relationally) is caught here even if the engine's own validator were reverted too. function _hasCompliantOwnTurn(st, seat, rule) { const saved = st.__seat__; st.__seat__ = seat; const from = st.pos[seat]; const cands = [from, ...E.legalMoves(st, seat)]; let ok = false; for (const to of cands) { if (!E.violates(rule, from, to, st)) { ok = true; break; } } st.__seat__ = saved; return ok; } /* ---- GATE: per-PHASE escapability (lever D) ---------------------------- * * For every phase/memory rule, on a seeded sweep of boards, EVERY seat has a * compliant move-or-pass in EVERY clock segment (0 forced violations across all * phases). We drive the public clock through every segment by hand and re-derive * escapability independently; we ALSO confirm the engine's validatePhaseEscapable * agrees (so the two cannot silently diverge). Revert detector: if the phase pred * stopped gating on RED_SEG (taboo in all segments) and some segment had no escape, * the independent scan fails; if the pred forbade the pass, it fails too. */ test('SLICE2-GATE per-phase escapability: every seat compliant move-or-pass in every clock segment (seed sweep)', () => { assert.ok(PHASE_VARIANT_LIST.length >= 1, 'expected >=1 phase/memory variant'); let checkedPhases = 0, forced = 0; for (const rule of PHASE_VARIANT_LIST) { for (const seed of SLICE2_SEEDS) { for (const seats of [2, 3, 4]) { const st = E.makeBoard(rule, 'harvest_max', seed, 1, ENV_PRESETS.E1, { seats, N: SLICE2_N, walls: true }); assert.ok(st.clock, `${rule}: phase board must seed a public clock`); for (let s = 0; s < seats; s++) { const c = st.clock[s]; assert.ok(c && c.segN >= 2, `${rule} seat ${s}: clock must have >=2 segments`); const savedSeg = c.seg; for (let seg = 0; seg < c.segN; seg++) { c.seg = seg; checkedPhases++; if (!_hasCompliantOwnTurn(st, s, rule)) { forced++; assert.fail(`${rule} seed=${seed} seats=${seats} seat=${s} seg=${seg}/${c.segN}: NO compliant move-or-pass (forced violation in this phase)`); } } c.seg = savedSeg; } // the engine's own validator must AGREE with the independent scan. assert.ok(E.validatePhaseEscapable(st, rule), `${rule} seed=${seed} seats=${seats}: validatePhaseEscapable disagrees with independent scan`); } } } assert.strictEqual(forced, 0, `${forced} forced per-phase violations over the sweep`); assert.ok(checkedPhases > 0, 'gate exercised no phases (vacuous)'); }); /* ---- GATE: per-seat RELATIONAL escapability (lever A) ------------------ * * Relational rules make a seat's forbidden set depend on the LIVE positions of the * OTHER seats / landmarks / ghosts. On a multi-seat sweep, EVERY seat (under its own * relational rule) always has a compliant move-or-pass given the current positions of * all other seats — relational rules never box a seat. Independent scan + the engine * validator must agree. Revert detector: if a relational pred forbade the pass, or the * board seeded a landmark on a seat's row/col (boxing it), the scan fails. */ test('SLICE2-GATE relational per-seat escapability: relational rules never box a seat (multi-seat sweep)', () => { assert.ok(RELATIONAL_VARIANT_LIST.length >= 1, 'expected >=1 relational variant'); let checkedSeats = 0, boxed = 0; for (const rule of RELATIONAL_VARIANT_LIST) { for (const seed of SLICE2_SEEDS) { for (const seats of [2, 3, 4]) { // homogeneous: every seat under the SAME relational rule (densest mutual // constraint). The relational predicate reads LIVE st.pos of the OTHER seats. const ruleSet = new Array(seats).fill(rule); const st = E.makeBoard(rule, 'harvest_max', seed, 1, ENV_PRESETS.E1, { seats, N: SLICE2_N, walls: true, ruleSet }); for (let s = 0; s < seats; s++) { checkedSeats++; if (!_hasCompliantOwnTurn(st, s, rule)) { boxed++; assert.fail(`${rule} seed=${seed} seats=${seats} seat=${s}: boxed (no compliant move-or-pass given live rivals)`); } } assert.ok(E.validateRelationalEscapable(st, seats, ruleSet), `${rule} seed=${seed} seats=${seats}: validateRelationalEscapable disagrees with independent scan`); // landmark-LOS escapability also requires no landmark to share a seat's row/col. if (rule === 'avoid_landmark_los' && st.landmarks) { const n = st.N || SLICE2_N; for (let s = 0; s < seats; s++) { const p = st.pos[s]; for (const k of st.landmarks) { const lx = k % n, ly = (k / n) | 0; assert.ok(!(p.x === lx || p.y === ly), `${rule} seed=${seed}: landmark (${lx},${ly}) on seat ${s} line -> pass forbidden`); } } } } } } assert.strictEqual(boxed, 0, `${boxed} boxed seats over the relational sweep`); assert.ok(checkedSeats > 0, 'gate exercised no seats (vacuous)'); }); /* ---- GATE: total/C* <= 1 (UNCLAMPED) on relational + phase cycles ------ * * On homogeneous relational and phase rule-sets, the multiAgentCeiling C* must be * ATTAINED by the perfect compliant joint policies (realized == total, ratio 1) and * never exceeded — so total/C* <= 1 holds unclamped. Revert detector: a relational/ * phase ceiling that under-counts (reads stale positions / ignores the clock) makes * realized != total; a runReport-level over-count surfaces ratio > 1. */ test('SLICE2-GATE total/C* <= 1 (unclamped) on relational + phase cycles', () => { const env = ENV_PRESETS.E1, budget = 16, rounds = 2, opts0 = { N: SLICE2_N }; const families = RELATIONAL_VARIANT_LIST.concat(PHASE_VARIANT_LIST); let checked = 0; for (const rule of families) { for (const seed of [7, 11, 3, 19]) { for (const k of [2, 3]) { const rules = new Array(k).fill(rule); const opts = Object.assign({}, opts0, { seats: k, ruleSet: rules.slice() }); const c = E.multiAgentCeiling(rules, 'harvest_max', seed, env, budget, rounds, opts); assert.ok(c.total >= 0, `${rule} k=${k} seed=${seed}: C* negative`); // C* attained by the perfect joint compliant policies (the realized compliant // total equals the ceiling -> total/C* = 1, the UPPER bound; never above). const pp = E.multiAgentPerfectPolicies(rules, 'harvest_max', seed, env, budget, rounds, opts); let realized = 0; for (let r = 0; r < rounds; r++) { const scores = E.compliantRoundHarvestMulti(rules, 'harvest_max', seed, r, env, budget, pp.policies(), opts); realized += scores.reduce((a, b) => a + b, 0); } if (c.total > 0) { assert.ok(realized / c.total <= 1 + 1e-9, `${rule} k=${k} seed=${seed}: realized/C* = ${(realized / c.total).toFixed(4)} > 1 (over-cap)`); assert.ok(Math.abs(realized - c.total) <= 1e-9, `${rule} k=${k} seed=${seed}: perfect joint policies do not attain C* (${realized} vs ${c.total})`); } checked++; } } } assert.ok(checked > 0, 'gate exercised no relational/phase ceilings (vacuous)'); }); /* ---- GATE: total/C* <= 1 driven through REAL slice-2 campaign cycles ---- * * The engine-level ceiling gate above tests homogeneous rule-sets; this drives the * ACTUAL scheduled campaign (slice2Families:true) — which mixes relational + phase * cycles by depth — under a compliant free-switch driver and asserts every finalized * cycle's UNCLAMPED total/C* <= 1, 0 hearts lost, 0 trapped, and that the run actually * reached the relational AND phase families (so the gate is not vacuously satisfied by * an early-only run). */ const SLICE2_DIRS = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]; function _slice2Productive(run, seat) { const st = run.board, rule = run.ruleSet[seat], from = st.pos[seat]; if (run.goal === 'reach_zones') { const dz = st.destinations && st.destinations[seat]; if (!dz) return null; if (from.x === dz.x && from.y === dz.y) return null; const to = E.bfsStep(st, seat, rule, false, { x: dz.x, y: dz.y }); return (to.x !== from.x || to.y !== from.y) ? to : null; } const to = E.nearestCompliantMove(st, seat, rule); return (to.x !== from.x || to.y !== from.y) ? to : null; } function _driveSlice2(seed) { const run = CAMP.createRun({ seed, slice2Families: true }); let steps = 0, trapped = 0, maxRatio = 0; const heartsBefore = run.hearts; while (run.status === 'running' && steps < 40000) { if (run.stage === 'demo') { CAMP.stepDemo(run); CAMP.handoff(run); continue; } if (run.stage !== 'play') break; const seat = run.turnSeat, rule = run.ruleSet[seat], st = run.board, from = { ...st.pos[seat] }, n = st.N || 9; let anyC = false; for (const d of SLICE2_DIRS) { const to = { x: from.x + d.x, y: from.y + d.y }; if (to.x < 0 || to.y < 0 || to.x >= n || to.y >= n) continue; if (st.wall && st.wall.has(to.y * n + to.x)) continue; st.__seat__ = seat; if (!E.violates(rule, from, to, st)) { anyC = true; break; } } if (!anyC) trapped++; const mv = _slice2Productive(run, seat); if (mv) { CAMP.playMove(run, seat, mv); if (run.stage === 'play' && run.status === 'running') CAMP.switchSeat(run); steps++; continue; } let sw = false; for (let k = 1; k < run.party.length; k++) { const cand = (seat + k) % run.party.length; if (_slice2Productive(run, cand)) { while (run.turnSeat !== cand) CAMP.switchSeat(run); sw = true; break; } } if (sw) { steps++; continue; } const eng = CAMP.engagementState(run); let ss = -1; for (let k = 0; k < run.party.length; k++) { const cand = (seat + k) % run.party.length; const e = eng.bySeat[cand]; if (e && e.done < e.need) { ss = cand; break; } } if (ss >= 0) { while (run.turnSeat !== ss) CAMP.switchSeat(run); const sr = run.ruleSet[ss], sf = { ...run.board.pos[ss] }; let own = sf; run.board.__seat__ = ss; if (E.violates(sr, sf, sf, run.board)) { const sn = run.board.N || 9; for (const d of SLICE2_DIRS) { const t = { x: sf.x + d.x, y: sf.y + d.y }; if (t.x < 0 || t.y < 0 || t.x >= sn || t.y >= sn) continue; if (run.board.wall && run.board.wall.has(t.y * sn + t.x)) continue; run.board.__seat__ = ss; if (!E.violates(sr, sf, t, run.board)) { own = t; break; } } } CAMP.playMove(run, ss, own); if (run.stage === 'play' && run.status === 'running') CAMP.switchSeat(run); steps++; continue; } CAMP.advanceCycleIfGoalMet(run); if (run.stage === 'play') break; steps++; } const rep = CAMP.runReport(run); for (const pc of rep.perCycle) { const ratio = pc.cStar > 0 ? pc.total / pc.cStar : (pc.total > 0 ? Infinity : 0); if (ratio > maxRatio) maxRatio = ratio; } let phaseSeen = false, relSeen = false; for (const r of run.ruleSet) { if (PHASE_VARIANT_LIST.indexOf(r) !== -1) phaseSeen = true; if (RELATIONAL_VARIANT_LIST.indexOf(r) !== -1) relSeen = true; } return { heartsLost: heartsBefore - run.hearts, trapped, maxRatio, depth: run.depth, phaseSeen, relSeen, cycles: rep.perCycle.length }; } // SPLIT (2026-07-30): one test per seed (was 416s in one test()). The cross-seed // vacuity OR (anyRel/anyPhase) is pinned per-seed from a pre-split measurement: // every family flag measured true is asserted on ITS seed, so the teeth survive // without cross-test state (shard-safe). The family-coverage test below proves // the pins still cover both families — if a schedule change drops a family from // its pinned seed, that seed's test goes red (not silently vacuous). const _SLICE2_RUN_SEEDS = [1, 2, 4, 7]; const _SLICE2_FAMILY = { // measured 2026-07-30 pre-split (Step 1 실측값으로 채움) 1: { phase: true, rel: true }, 2: { phase: true, rel: true }, 4: { phase: true, rel: true }, 7: { phase: true, rel: true }, }; for (const _s2Seed of _SLICE2_RUN_SEEDS) { test(`SLICE2-GATE real scheduled campaign (seed ${_s2Seed}): 0 forced violation, total/C* <= 1`, () => { const r = _driveSlice2(_s2Seed); assert.ok(r.heartsLost === 0, `seed=${_s2Seed}: ${r.heartsLost} hearts lost under compliant play (forced violation)`); assert.ok(r.trapped === 0, `seed=${_s2Seed}: ${r.trapped} trapped turns`); assert.ok(r.maxRatio <= 1 + 1e-9, `seed=${_s2Seed}: total/C* = ${r.maxRatio.toFixed(4)} > 1 (over-cap on a scheduled cycle)`); assert.ok(r.depth >= 6, `seed=${_s2Seed}: depth ${r.depth} too shallow to reach relational/phase tiers`); const fam = _SLICE2_FAMILY[_s2Seed]; if (fam.phase) assert.ok(r.phaseSeen, `seed=${_s2Seed}: phase family no longer reached (was reached pre-split 2026-07-30)`); if (fam.rel) assert.ok(r.relSeen, `seed=${_s2Seed}: relational family no longer reached (was reached pre-split 2026-07-30)`); }); } test('SLICE2-GATE family-coverage teeth: per-seed pins cover phase AND relational', () => { const fams = _SLICE2_RUN_SEEDS.map(s => _SLICE2_FAMILY[s]); assert.ok(fams.some(f => f.phase), 'no seed pins the phase family — the split lost the vacuity guard'); assert.ok(fams.some(f => f.rel), 'no seed pins the relational family — the split lost the vacuity guard'); }); /* ---- GATE: demo-IDENTIFIABILITY (lever C fairness floor) --------------- * * Every SHIPPED rule (variants + deliver-only + relational + phase) must be uniquely * identifiable from {its under-determined demo + <= K compliant probe moves} — no * unfalsifiable rule ships. The gate ALSO proves it has teeth: with a STARVED probe * budget (K=0, demo-only) at least one rule is NOT identifiable (so the K-probe * fairness mechanism is doing real work — the positive assertion is not vacuous). * Revert detector: a rule whose demo+probes can't pin it (an unfalsifiable taboo) or * a broken applyDemoBudget that drops the last disambiguating example fails the * positive assertion. */ test('SLICE2-GATE demo-identifiability: every shipped rule pinned at K-probe budget; K=0 leaves >=1 unidentifiable (teeth)', () => { const shipped = E.VARIANT_LIST .concat(RELATIONAL_VARIANT_LIST, PHASE_VARIANT_LIST, DELIVER_VARIANT_LIST) .filter((v, i, a) => a.indexOf(v) === i && RULE_VARIANTS[v]); assert.ok(shipped.length >= 10, `expected the full shipped pool, got ${shipped.length}`); const unfalsifiable = []; for (const rule of shipped) { if (!E.demoIdentifiable(rule)) unfalsifiable.push(rule); } assert.deepStrictEqual(unfalsifiable, [], `unfalsifiable rule(s) ship (not identifiable from demo + <=K probes): ${unfalsifiable.join(', ')}`); // TEETH: with NO probe budget (demo alone), at least one rule must FAIL to be // pinned — otherwise the K-probe fairness mechanism is doing nothing and the // positive assertion above would be green-by-construction. let someUnidentifiableAtK0 = false; for (const rule of shipped) { if (!E.demoIdentifiable(rule, { K: 0 })) { someUnidentifiableAtK0 = true; break; } } assert.ok(someUnidentifiableAtK0, 'K=0 (demo-only) pins EVERY rule -> the probe-budget fairness mechanism is inert (gate has no teeth)'); }); /* ---- GATE: clock / ghost C1 (no rule-id leak; pure public-state glyphs) - * * (a) The serialized PLAY obs and the serialized DEMO for every phase/relational rule * must NEVER contain the rule id/variant string (C1: appearance is a function of * PUBLIC state only). (b) The phase-clock descriptor + per-step segment values are * a PURE function of the public clock state: two DIFFERENT rules sharing the same * public clock shape (segN/advanceOn) emit BYTE-IDENTICAL clock lines. (c) The * ghost markers are pure public cells (same ghost set glyphs identically regardless * of rule). Revert detector: a serializer that printed the rule id, or keyed the * clock/ghost glyph on the rule, fails. */ test('SLICE2-GATE clock/ghost C1: no rule-id leak in obs/demo; clock+ghost glyphs are pure public-state functions', () => { const phaseRel = PHASE_VARIANT_LIST.concat(RELATIONAL_VARIANT_LIST); // (a) DEMO never leaks the rule id (full variant-id substring scan). for (const rule of phaseRel.concat(E.VARIANT_LIST, DELIVER_VARIANT_LIST)) { if (!RULE_VARIANTS[rule]) continue; const demo = HARNESS.serializeDemo(rule, 0, SLICE2_N); assert.ok(demo.indexOf(rule) === -1, `demo for ${rule} leaks the rule id`); // the base mechanic tag (e.g. 'phase'/'relational'/'combo') must not leak either. const base = RULE_VARIANTS[rule].base; if (base === 'phase' || base === 'relational' || base === 'combo') { assert.ok(demo.indexOf('"base"') === -1 && demo.indexOf('pred') === -1, `demo for ${rule} leaks an internal field`); } } // (a') PLAY obs never leaks any rule id across a driven slice-2 run. { const run = CAMP.createRun({ seed: 4, slice2Families: true, cycleCap: 10 }); let steps = 0; const allIds = Object.keys(RULE_VARIANTS); while (run.status === 'running' && steps < 4000) { if (run.stage === 'demo') { CAMP.stepDemo(run); CAMP.handoff(run); steps++; continue; } if (run.stage !== 'play') break; const obs = HARNESS.serializePlay(run).text; for (const id of allIds) { assert.ok(obs.indexOf(id) === -1, `play obs leaks rule id ${id} (cycle ${run.cycle})`); } // advance one compliant own-turn (or pass) so we sample obs across cycles. const seat = run.turnSeat, rule = run.ruleSet[seat], st = run.board, from = { ...st.pos[seat] }; const to = E.nearestCompliantMove(st, seat, rule); if (to.x !== from.x || to.y !== from.y) CAMP.playMove(run, seat, to); else { st.__seat__ = seat; CAMP.playMove(run, seat, from); } if (run.stage === 'play' && run.status === 'running') CAMP.switchSeat(run); steps++; } assert.ok(steps > 0, 'C1 obs scan exercised no play obs'); } // (b) clock descriptor + per-step seg= values are a PURE function of public clock // state: two DIFFERENT phase rules with the SAME public clock shape emit IDENTICAL // clock lines. phase_dark@A and phase_dark@B both carry segN=3/own_turn clocks and // (by C1) the SAME public walk -> identical clock-bearing lines. const clockLines = (t) => t.split('\n').filter(l => /phase-clock|segN=|seg=|advance=/.test(l)).join('|'); const dA = HARNESS.serializeDemo('phase_dark@A', 0, SLICE2_N); const dB = HARNESS.serializeDemo('phase_dark@B', 0, SLICE2_N); assert.strictEqual(clockLines(dA), clockLines(dB), 'two rules with identical public clock state emit DIFFERENT clock lines -> clock glyph leaks the rule (C1)'); // sanity: the clock lines are non-empty (the gate actually compared something). assert.ok(clockLines(dA).length > 0, 'phase demo produced no clock lines (C1 gate vacuous)'); // (c) ghost markers are pure public cells: a relational demo lists ghost companion // cells as the SAME public 'o' glyph independent of which relational rule; the ghost // cell SET must be identical across two relational rules on the same board build // (rule-invariant placement, C1). const ghostCells = (rule) => { const tut = E.buildTutorial(rule); return tut.board.ghosts ? tut.board.ghosts.map(g => `${g.x},${g.y}`).sort().join(' ') : ''; }; const gA = ghostCells('avoid_adjacent_rival'); const gB = ghostCells('avoid_token_nearest_rival'); if (gA || gB) { assert.strictEqual(gA, gB, 'ghost companion placement differs across relational rules -> ghost glyph leaks the rule (C1)'); } // (d) LIVE-BOARD GEOMETRY is a pure function of PUBLIC state, NOT the hidden rule. // The substring scans above cannot catch a GEOMETRIC leak (the rule id is absent from // the obs, yet the token/terrain LAYOUT could still encode the rule). Build the live // campaign board for the SAME seed/goal/cycle varying ONLY the newcomer rule within a // SHIPPED family, and assert the alive-token + landmark layout is byte-identical. This // guards the slice-2 leak class found in verification: a rule whose large forbidden set // makes the escapability pass KILL a guard token (avoid_landmark_los) shifts the alive- // token layout -> excluded from the live pool; the two shipped relational rules + the // phase family must stay geometry-invariant. (Mirrors the independent geometry probe.) const liveGeom = (rule, goal, seed, cycle, seats) => { const others = ['avoid_dark@A', 'avoid_hatch@B', 'avoid_tar@C', 'avoid_biggest@top2']; const ruleSet = []; for (let s = 0; s < seats - 1; s++) ruleSet.push(others[s % others.length]); ruleSet.push(rule); const st = E.makeBoard(rule, goal, seed, cycle, undefined, { seats, N: 11, walls: true, ruleSet }); const toks = st.tokens.filter(t => t.alive).map(t => `${t.x},${t.y}=${t.v}`).sort().join('|'); const lm = st.landmarks ? [...st.landmarks].sort((a, b) => a - b).join(',') : ''; return toks + '#' + lm; }; const shippedRel = RELATIONAL_VARIANT_LIST.filter(r => r !== 'avoid_landmark_los'); const families = [shippedRel, PHASE_VARIANT_LIST]; let geomChecks = 0; for (const fam of families) { for (const goal of ['harvest_max', 'deliver_to_zone', 'reach_zones', 'collect_set']) { for (let seed = 1; seed <= 12; seed++) { for (const seats of [3, 5]) { let base = null, baseRule = null; for (const rule of fam) { const g = liveGeom(rule, goal, seed, seats, seats); if (base === null) { base = g; baseRule = rule; continue; } geomChecks++; assert.strictEqual(g, base, `live-board geometry differs ${baseRule} vs ${rule} (goal=${goal} seed=${seed} seats=${seats}) -> hidden rule leaks into the token/landmark layout (C1)`); } } } } } assert.ok(geomChecks > 0, 'C1 geometry-invariance check exercised nothing (vacuous)'); }); /* ---- GATE: legacy 9x9 / 3-rule / 2-seat BYTE-IDENTITY (re-affirmed) ----- * * Slice-2 must not leak ANY of its new fields (clock / landmarks / ghosts / tar) onto * the legacy path, and RULE_LIST stays exactly 3 (the new families are RULE_VARIANTS, * never RULE_LIST entries). The SNAP capture test above guards the full byte-image; * this restates the slice-2-specific invariants explicitly so a slice-2 regression is * caught with a pointed message. */ test('SLICE2-GATE legacy byte-identity: no clock/landmarks/ghosts/tar on legacy boards; RULE_LIST===3 (families are RULE_VARIANTS)', () => { assert.strictEqual(E.RULE_LIST.length, 3, 'RULE_LIST must stay exactly 3'); // every new slice-2 family lives in RULE_VARIANTS, never RULE_LIST / RULES. for (const id of PHASE_VARIANT_LIST.concat(RELATIONAL_VARIANT_LIST)) { assert.ok(E.RULE_LIST.indexOf(id) === -1, `slice-2 family ${id} leaked into RULE_LIST`); assert.ok(!(id in E.RULES), `slice-2 family ${id} leaked into RULES`); assert.ok(RULE_VARIANTS[id], `slice-2 family ${id} missing from RULE_VARIANTS`); } // legacy boards (no opts) carry NONE of the slice-2 fields. for (const [rule, goal, seed, round] of [ ['avoid_dark', 'harvest_max', 7, 1], ['avoid_biggest', 'deliver_to_zone', 7, 2], ['avoid_hatch', 'harvest_max', 3, 1], ]) { const lg = E.makeBoard(rule, goal, seed, round); assert.ok(!('clock' in lg), `legacy ${rule}/${goal} board leaked a phase clock`); assert.ok(!('landmarks' in lg), `legacy ${rule}/${goal} board leaked landmarks`); assert.ok(!('ghosts' in lg), `legacy ${rule}/${goal} board leaked ghosts`); assert.ok(!('tar' in lg) && !('tarInst' in lg), `legacy ${rule}/${goal} board leaked tar terrain`); } // and the legacy 2-seat layout is preserved. const lg = E.makeBoard('avoid_dark', 'harvest_max', 7, 1); assert.strictEqual(Object.keys(lg.pos).length, 2, 'legacy board must be 2-seat'); assert.strictEqual(lg.N, 9, 'legacy board must be 9x9'); }); /* ===================================================================== SLICE2 LEVER A2 — ROLE-PLAY / INTENTION RULE GATES (spec §6) (B) role escapability across seeds x seats, incl. orbit OUT-OF-BAND + intercept degenerate/collinear/on-line. (A-engine-half) source-scan: consistentMoves/outOfCharacter read NO rule-id / param / seat-ownership (C1). (F) legacy byte-identity UNCHANGED + role boards seed no legacy fields. ===================================================================== */ const ROLE_VARIANT_LIST = E.ROLE_VARIANT_LIST; // (B) ESCAPABILITY: for every role, >=8 seeds x seats {2,3,4}, homogeneous party, // consistentMoves(st,s,role).size>=1 for EVERY seat at spawn AND after a few seeded // plies; validateRoleEscapable agrees. + SPECIAL cases (orbit seeded OUTSIDE the band; // intercept chaser==prey / collinear / seat already ON the chaser-prey line). test('A2-B role escapability: consistentMoves non-empty for every seat over seeds x seats {2,3,4} (+ orbit out-of-band, intercept degenerate/collinear)', () => { let checks = 0; for (const role of ROLE_VARIANT_LIST) { for (let seed = 1; seed <= 8; seed++) for (const seats of [2, 3, 4]) { const ruleSet = []; for (let i = 0; i < seats; i++) ruleSet.push(role); const env = { id: 'E1', pressure: 0, opp: 'greedy', topo: ['open', 'corridor', 'clustered'][seed % 3] }; const st = E.makeBoard(role, 'harvest_max', seed, seed % 4, env, { N: 11, seats, walls: true, ruleSet }); // spawn: every seat has a non-empty in-character set + validator agrees. for (let s = 0; s < seats; s++) { st.__seat__ = s; assert.ok(E.consistentMoves(st, s, role).size >= 1, `${role} seed${seed} seats${seats} seat${s}: empty in-character set at spawn`); checks++; } assert.ok(E.validateRoleEscapable(st, seats, role), `${role} seed${seed} seats${seats}: validateRoleEscapable false at spawn`); // a few seeded plies (each seat takes an in-character step), then re-assert. for (let ply = 0; ply < 4; ply++) { for (let s = 0; s < seats; s++) { st.__seat__ = s; const from = st.pos[s]; const ok = E.consistentMoves(st, s, role); assert.ok(ok.size >= 1, `${role} seed${seed} seats${seats} seat${s} ply${ply}: empty set mid-walk`); // pick an in-character legal step deterministically (DIRS order, else stay). let to = from; for (const d of E.DIRS) { const p = { x: from.x + d.x, y: from.y + d.y }; if (p.x < 0 || p.y < 0 || p.x >= st.N || p.y >= st.N) continue; if (st.wall && st.wall.has(p.y * st.N + p.x)) continue; if (ok.has(E.moveKeyOf(from, p))) { to = p; break; } } E.applyMove(st, s, to, role); checks++; } } assert.ok(E.validateRoleEscapable(st, seats, role), `${role} seed${seed} seats${seats}: validateRoleEscapable false mid-walk`); } } // SPECIAL: orbit seeded OUTSIDE the [r-1,r+1] band (r=2): seat far (d>>r+1) and near // (d E.makeBoard(role, 'harvest_max', 5, 1, E.ENV_PRESETS.E1, { N: 11, seats: 2, walls: false }); const stFar = mk(); stFar.pos[0] = { x: 0, y: 0 }; stFar.pos[1] = { x: 10, y: 10 }; stFar.__seat__ = 0; assert.ok(E.consistentMoves(stFar, 0, role).size >= 1 && E.consistentMoves(stFar, 0, role).has('stay'), 'orbit far-out-of-band: empty / no stay'); const stNear = mk(); stNear.pos[0] = { x: 5, y: 5 }; stNear.pos[1] = { x: 5, y: 5 }; stNear.__seat__ = 0; assert.ok(E.consistentMoves(stNear, 0, role).size >= 1, 'orbit on-ref (d=0, inside band): empty'); checks += 2; } // SPECIAL: intercept chaser==prey (degenerate -> reduces to pursue), collinear, and // seat already ON the chaser->prey line. Build 3-seat boards with placed positions. { const role = 'intercept_chase'; const base = E.makeBoard(role, 'harvest_max', 3, 1, E.ENV_PRESETS.E1, { N: 11, seats: 3, walls: false }); base.facing = base.facing || {}; // degenerate: only-one-other-actor case via a 2-seat board (chaser==prey). const st1 = E.makeBoard(role, 'harvest_max', 3, 1, E.ENV_PRESETS.E1, { N: 11, seats: 2, walls: false }); st1.__seat__ = 0; assert.ok(E.consistentMoves(st1, 0, role).size >= 1, 'intercept degenerate (2-seat): empty'); // collinear: chaser (1,1), prey (5,1), seat (3,1) ON the line. const st2 = base; st2.pos[0] = { x: 3, y: 1 }; st2.pos[1] = { x: 1, y: 1 }; st2.pos[2] = { x: 5, y: 1 }; st2.facing = { 1: { dx: 1, dy: 0 } }; st2.__seat__ = 0; assert.ok(E.consistentMoves(st2, 0, role).size >= 1 && E.consistentMoves(st2, 0, role).has('stay'), 'intercept seat-on-line: empty / no stay'); checks += 2; } console.log(` [A2-role-escapable] ${ROLE_VARIANT_LIST.length} roles, ${checks} (seat,state) checks: 0 empty in-character sets`); }); // (A-engine-half) C1 SOURCE-SCAN: the role decision functions read NO rule-id / // RULE_VARIANTS[rule].param / seat-ownership of the rule. consistentMoves takes the // rule id only to look up the role META (roleKind/ref) — it must never branch the // in-character set on the rule STRING or a .param, and outOfCharacter only forwards // the seat seam + role id. We assert the function bodies do not reference param / // ruleSet / __rivalRule__ and do not hard-code any role id literal. test('A2-A C1 source-scan: consistentMoves/outOfCharacter read no rule-param/seat-ownership', () => { const src = fs.readFileSync(path.join(__dirname, 'engine.js'), 'utf8'); const bodyOf = (fn, end) => { const i = src.indexOf('function ' + fn); assert.ok(i >= 0, `${fn} not found`); const j = end ? src.indexOf('function ' + end, i + 1) : src.length; return src.slice(i, j > i ? j : src.length); }; const cmBody = bodyOf('consistentMoves', 'roleAnchorId'); const ocBody = bodyOf('outOfCharacter', 'violates'); for (const [name, body] of [['consistentMoves', cmBody], ['outOfCharacter', ocBody]]) { // no read of the rule's PARAM (a leak of the hidden binding) or seat-ownership ann. assert.ok(!/\.param\b/.test(body), `${name} must not read RULE_VARIANTS[..].param (rule-param leak)`); assert.ok(body.indexOf('__rivalRule__') === -1, `${name} must not read seat-ownership (__rivalRule__)`); // no hard-coded role id literal (the function must be rule-AGNOSTIC: two roles with // the same resolver compute identically). Scan for any shipped role id string. for (const id of ROLE_VARIANT_LIST) { assert.ok(body.indexOf("'" + id + "'") === -1 && body.indexOf('"' + id + '"') === -1, `${name} must not hard-code the role id literal ${id} (rule-id branch leak)`); } } // and the only RULE_VARIANTS access in consistentMoves is the role META lookup (.role // meta), never indexing a forbidden field — assert it reads `.roleKind` / `.ref` (the // PUBLIC resolver descriptors) and `.role` (the tag), nothing rule-coupled. assert.ok(/\.roleKind\b/.test(cmBody) && /\.ref\b/.test(cmBody), 'consistentMoves should resolve the role via the public roleKind/ref descriptors'); console.log(' [A2-C1] consistentMoves/outOfCharacter: 0 rule-param / seat-ownership / role-id-literal reads'); }); // (F) LEGACY BYTE-IDENTITY UNCHANGED: the existing SNAP captures still hold, RULE_LIST // stays exactly 3, role variants are RULE_VARIANTS-only (not in RULE_LIST), and a role // board seeds NO legacy-incompatible fields on the legacy 9x9/2-seat path; a legacy // board carries NO st.facing (the role-only public cue). test('A2-F legacy byte-identity UNCHANGED: SNAP holds, RULE_LIST===3, role boards seed no legacy fields / no facing on legacy', () => { const A_SNAP = '{"N":9,"pos":{"0":{"x":0,"y":0},"1":{"x":8,"y":8}},"carry":{"0":0,"1":0},"score":{"0":0,"1":0},"penalty":{"0":0,"1":0},"hazard":[21,27,35,48,51,67],"sacred":[37,41,45,59,60,66],"tokens":[{"x":3,"y":5,"v":13,"alive":true,"guard":true},{"x":0,"y":3,"v":13,"alive":true,"guard":true},{"x":8,"y":3,"v":12,"alive":true,"guard":true},{"x":8,"y":6,"v":1,"alive":true,"guard":false},{"x":4,"y":0,"v":3,"alive":true,"guard":false},{"x":1,"y":2,"v":3,"alive":true,"guard":false},{"x":2,"y":5,"v":2,"alive":true,"guard":false},{"x":7,"y":2,"v":3,"alive":true,"guard":false},{"x":3,"y":4,"v":1,"alive":true,"guard":false}],"zone":null,"penalty_amt":19,"hasWall":false,"hasHazInst":false,"hasSacInst":false}'; const B_SNAP = '{"N":9,"pos":{"0":{"x":0,"y":0},"1":{"x":8,"y":8}},"carry":{"0":0,"1":0},"score":{"0":0,"1":0},"penalty":{"0":0,"1":0},"hazard":[11,22,39,52,58,70],"sacred":[1,12,30,35,45,68],"tokens":[{"x":4,"y":0,"v":13,"alive":true,"guard":true},{"x":8,"y":0,"v":12,"alive":true,"guard":true},{"x":4,"y":8,"v":11,"alive":true,"guard":true},{"x":3,"y":8,"v":10,"alive":true,"guard":true},{"x":7,"y":4,"v":1,"alive":true,"guard":false},{"x":2,"y":6,"v":2,"alive":true,"guard":false},{"x":4,"y":7,"v":2,"alive":true,"guard":false},{"x":4,"y":3,"v":1,"alive":true,"guard":false},{"x":1,"y":4,"v":1,"alive":true,"guard":false},{"x":1,"y":1,"v":2,"alive":true,"guard":false}],"zone":{"x":4,"y":1},"penalty_amt":31,"hasWall":false,"hasHazInst":false,"hasSacInst":false}'; assert.strictEqual(snapCanon(E.makeBoard('avoid_dark', 'harvest_max', 7, 1)), A_SNAP, 'legacy avoid_dark/harvest board drifted (role edit broke byte-identity)'); assert.strictEqual(snapCanon(E.makeBoard('avoid_biggest', 'deliver_to_zone', 7, 2)), B_SNAP, 'legacy avoid_biggest/deliver board drifted (role edit broke byte-identity)'); assert.strictEqual(E.RULE_LIST.length, 3, 'RULE_LIST must stay exactly 3'); // role variants are RULE_VARIANTS-only, never RULE_LIST / RULES. for (const id of ROLE_VARIANT_LIST) { assert.ok(E.RULE_LIST.indexOf(id) === -1, `role ${id} leaked into RULE_LIST`); assert.ok(!(id in E.RULES), `role ${id} leaked into RULES`); assert.ok(E.RULE_VARIANTS[id] && E.RULE_VARIANTS[id].role, `role ${id} missing role tag`); } // a legacy board (no opts) carries NO st.facing (the role-only public cue). for (const [rule, goal] of [['avoid_dark', 'harvest_max'], ['avoid_biggest', 'deliver_to_zone']]) { const lg = E.makeBoard(rule, goal, 7, 1); assert.ok(!('facing' in lg), `legacy ${rule}/${goal} board leaked st.facing`); } // a ROLE board on the campaign path DOES carry st.facing (gated, public cue) but // still seeds the standard slice-2 layout — and a role board on the LEGACY 9x9/2-seat // path seeds an empty facing (not a leak of any legacy serialization, since legacy // SNAP boards use base rules that never set facing). const rb = E.makeBoard(ROLE_VARIANT_LIST[0], 'harvest_max', 7, 1, E.ENV_PRESETS.E1, { N: 11, seats: 2, walls: true }); assert.ok('facing' in rb, 'role board must seed st.facing (the public heading cue)'); console.log(' [A2-F] SNAP A/B intact, RULE_LIST===3, ' + ROLE_VARIANT_LIST.length + ' roles RULE_VARIANTS-only, no facing on legacy'); }); /* ======================================================================== P3 — YIELD-AWARE ORACLE PoC + value-branch chokepoint predicate (LOAD-BEARING, fail-fast). Plan 2026-06-18 §P3 / §P3.5; design §1.3/§2/§6. CORRECTED geometry (supersedes the NO-GO first attempt): Let P = the weaker-and-closer companion's UNIQUE path to its nearest live token. The focal seat sits ON P; P also lies on the seat's OWN BFS-optimal path so advancing toward its own goal means moving ALONG P. The forbidden set for the unique blocker = { stay } UNION { any move that keeps the seat ON P (incl. the next cell ALONG P, the desire/advance direction) }; the COMPLIANT set = moves that STRICTLY VACATE P (a LATERAL step off the corridor — the only strictly-clearing move). value-oracle = lateral vacate; desire-oracle = advance along P; avoid-c predicate = also advance along P. So value diverges from BOTH desire and predicate on the advance, and the advance is genuinely violates()===true under defer. The chokepoint board (N=7): column x=3 is a one-cell-wide vertical corridor (the companion's unique path P). At the SEAT's row y=3 ONLY, a lateral pocket (4,3) is open OFF the corridor; everywhere else (4,y),(2,y) are walls. Seat seat 0 sits ON c=(3,3). Its own goal token is SOUTH (3,6) so advancing = stepping D=(3,4), ALONG P. Companion seat 1 is north at (3,1) (lower carry = weaker, closer to c), its nearest token south at (3,5) so its only route runs through c. Sealing c, or U=(3,2), or D=(3,4) ALL leave the companion's BFS Infinity (the block merely relocates along P); only R=(4,3) — the lateral off-P step — leaves the companion's path finite. So forbidden = {stay, U, D}; compliant = {R}. ======================================================================== */ const CP_RULE = 'defer_to_weaker_closer'; // the valueLaden value-rule under test // build the corrected chokepoint with a parametric companion-token row (south of c) // and optional extra north-half seats so the escapability scan can vary geometry. function buildChokepoint(opts) { opts = opts || {}; const n = 7; const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { if (x === 3) continue; // corridor column P open (rows 0..6) if (x === 4 && y === 3) continue; // the LATERAL pocket off P (only at the seat row) wall.add(E.keyN({ x, y }, n)); } const compTokY = opts.compTokY != null ? opts.compTokY : 5; // companion's token, SOUTH of c const st = { rule: CP_RULE, goal: 'harvest_max', round: 0, N: n, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: 3, y: 3 }, 1: { x: 3, y: 1 } }, carry: { 0: 5, 1: 0 }, // seat 1 is WEAKER (lower carry) score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [ { x: 3, y: 6, v: 4, alive: true, guard: false }, // seat 0's own goal token (south, along P) { x: 3, y: compTokY, v: 3, alive: true, guard: false }, // companion's nearest token (south) ], zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; return st; } // desireOnlyMove (test-local, NOT in the C* envelope): maximize own goal — // advance toward the seat's nearest live token, BLIND to the value duty. function desireOnlyMove(st, id) { let best = null, bd = 1e9; for (const t of st.tokens) { if (!t.alive) continue; const d = E.manhattan(st.pos[id], { x: t.x, y: t.y }); if (d < bd) { bd = d; best = { x: t.x, y: t.y }; } } if (!best) return st.pos[id]; return E.bfsStep(st, id, st.rule, true, best); // blind=true: ignore the value duty } // avoidCPredicateMove (test-local): the verdict-equal avoid-PREDICATE oracle. It // treats ONLY the contested cell c as forbidden (a static cell-taboo) and otherwise // advances greedily toward its own token. Since the next cell ALONG P (D=(3,4)) is // NOT c, the predicate happily steps there — collapsing onto desire. function avoidCPredicateMove(st, id, cCell) { const cK = E.keyN(cCell, st.N); const blocked = new Set([cK]); let best = null, bd = 1e9; for (const t of st.tokens) { if (!t.alive) continue; const d = E.manhattan(st.pos[id], { x: t.x, y: t.y }); if (d < bd) { bd = d; best = { x: t.x, y: t.y }; } } // a clone whose wall set ALSO bans c (the only cell the predicate forbids), then // a blind BFS step toward the own token. From c the first step away cannot re-enter c. const s2 = { ...st, wall: new Set([...st.wall, ...blocked]) }; return E.bfsStep(s2, id, st.rule, true, best); } test('P3-LEGACY-STAY-ALWAYS: the 8 role consistentMoves ALWAYS contain stay (shared new Set([\'stay\']) untouched)', () => { let checks = 0; for (const rule of ROLE_VARIANT_LIST) { for (const seats of [2, 3, 4]) { for (const seed of [1, 7, 13]) { const st = E.makeBoard(rule, 'harvest_max', seed, 0, E.ENV_PRESETS.E1, { N: 11, seats, walls: true }); for (let seat = 0; seat < seats; seat++) { assert.ok(E.consistentMoves(st, seat, rule).has('stay'), `${rule} seed=${seed} seats=${seats} seat=${seat}: stay missing from consistentMoves`); checks++; } } } } console.log(' [P3-LEGACY-STAY-ALWAYS] ' + checks + ' (role,seat,state) checks: stay always present'); }); test('P3-CHOKEPOINT-DIVERGE: value=lateral-vacate, desire=advance-along-P, avoid-c-predicate=advance-along-P; value != desire AND != predicate', () => { const st = buildChokepoint(); assert.ok(E._isUniqueBlocker(st, 0), 'focal seat 0 must be the unique blocker at the chokepoint'); const c = { x: 3, y: 3 }; const value = E.yieldAwareCompliantMove(st, 0, CP_RULE); const desire = desireOnlyMove(st, 0); const pred = avoidCPredicateMove(st, 0, c); // value-oracle: the LATERAL vacate OFF P (east to the pocket (4,3)); never staying on c, never along P. assert.deepStrictEqual(value, { x: 4, y: 3 }, 'value-oracle must emit the LATERAL vacate off P to (4,3)'); // desire-oracle: advance SOUTH along P through c toward its own token. assert.deepStrictEqual(desire, { x: 3, y: 4 }, 'desire-oracle must advance along P to (3,4)'); // avoid-c predicate: c itself is banned but the NEXT cell along P is "legal" to it -> also advances. assert.deepStrictEqual(pred, { x: 3, y: 4 }, 'avoid-c predicate must also advance along P to (3,4)'); assert.ok(!(value.x === desire.x && value.y === desire.y), 'value must DIVERGE from desire'); assert.ok(!(value.x === pred.x && value.y === pred.y), 'value must DIVERGE from the avoid-c predicate'); console.log(' [P3-CHOKEPOINT-DIVERGE] value=(4,3) vacate / desire=(3,4) / predicate=(3,4): value diverges from BOTH'); }); test('P3-ADVANCE-IS-VIOLATION: advancing along P (the next corridor cell) is violates()===true under defer when the seat is the unique blocker', () => { const st = buildChokepoint(); assert.ok(E._isUniqueBlocker(st, 0), 'seat 0 must be unique blocker'); st.__seat__ = 0; const c = { x: 3, y: 3 }; // the REAL violates()/outOfCharacter/consistentMoves path: the advance along P (D=(3,4)) is OUT-of-character. assert.ok(E.violates(CP_RULE, c, { x: 3, y: 4 }, st), 'advancing along P to (3,4) MUST be violates()===true (it keeps the companion blocked)'); // the OTHER on-P move (U=(3,2)) is also a violation; only the lateral vacate is in-character. assert.ok(E.violates(CP_RULE, c, { x: 3, y: 2 }, st), 'the other along-P move (3,2) MUST also be a violation'); assert.ok(!E.violates(CP_RULE, c, { x: 4, y: 3 }, st), 'the lateral vacate (4,3) MUST be in-character (NOT a violation)'); console.log(' [P3-ADVANCE-IS-VIOLATION] along-P (3,4)/(3,2) violate; lateral (4,3) compliant — via REAL violates()'); }); test('P3-VACATE-CLEARS-LATERAL: the value move is OFF P and STRICTLY clears (companion bfsStep finite); the advance does NOT clear (relocates)', () => { const st = buildChokepoint(); const compTok = { x: 3, y: 5 }; // companion's bfsStep toward its token with the seat OCCUPYING each candidate cell. // A seat occupies a cell -> that cell is impassable for the companion, modeled by // sealing it into a wall clone (this is exactly how the engine's _companionPathSet / // value-branch treats the seat cell when asking "does the companion still reach?"). const compStepWithSeatAt = (seatCell) => { const s2 = buildChokepoint(); s2.pos[0] = seatCell; s2.wall = new Set([...s2.wall, E.keyN(seatCell, s2.N)]); return E.bfsStep(s2, 1, CP_RULE, true, compTok); // blind: pure terrain reachability for the companion }; // value move (4,3): seat OFF P -> companion can step toward its token (path finite). const afterVacate = compStepWithSeatAt({ x: 4, y: 3 }); assert.ok(!(afterVacate.x === st.pos[1].x && afterVacate.y === st.pos[1].y), 'after the lateral vacate, the companion can make progress (path cleared)'); // advance (3,4): seat still ON P one cell down -> companion still blocked (path Infinity -> bfsStep returns from). const afterAdvance = compStepWithSeatAt({ x: 3, y: 4 }); assert.ok(afterAdvance.x === st.pos[1].x && afterAdvance.y === st.pos[1].y, 'after the advance along P, the companion is STILL blocked (the block merely relocated)'); console.log(' [P3-VACATE-CLEARS-LATERAL] lateral vacate clears; along-P advance relocates (still blocked)'); }); test('P3-PASS-COMPLIANT-WHEN-NOT-BLOCKER: pass is compliant via the REAL violates() path when NOT the unique blocker; non-compliant when blocker', () => { // BLOCKER state: seat 0 on c is the unique blocker -> staying (pass) is the violation. const st = buildChokepoint(); assert.ok(E._isUniqueBlocker(st, 0), 'seat 0 must be unique blocker in the chokepoint'); st.__seat__ = 0; assert.ok(E.violates(CP_RULE, { x: 3, y: 3 }, { x: 3, y: 3 }, st), 'a pass (stay) on c MUST be out-of-character (non-compliant) for the unique blocker'); // NOT-BLOCKER state: the companion is ALREADY SOUTH of c (past the chokepoint), so seat 0 no // longer sits on the companion's only remaining route -> not the unique blocker. pass routes // through the REAL consistentMoves value-branch and IS compliant. const st2 = buildChokepoint(); st2.pos[1] = { x: 3, y: 5 }; // companion already through the chokepoint, on its token's row assert.ok(!E._isUniqueBlocker(st2, 0), 'seat 0 is NOT the unique blocker once the companion is past c'); st2.__seat__ = 0; assert.ok(!E.violates(CP_RULE, { x: 3, y: 3 }, { x: 3, y: 3 }, st2), 'a pass (stay) when NOT the unique blocker MUST be compliant (routes through real violates())'); console.log(' [P3-PASS-COMPLIANT-WHEN-NOT-BLOCKER] blocker: pass violates; not-blocker: pass compliant'); }); test('P3-CLEARING-STEP-EXISTS: a strictly-vacating move exists every blocker turn AND the advance re-blocks, over VARIED wall geometry (validateValueEscapable)', () => { let scanned = 0, missingVacate = 0, advanceCleared = 0, first = null; // GENUINELY distinct configs: vary the lateral-pocket SIDE (east vs west), the corridor // COLUMN, the companion-token ROW, and the board size — not seed%3 x seed%2. const configs = []; for (const corridorX of [2, 3, 4]) { for (const pocketSide of [+1, -1]) { // pocket east (+1) or west (-1) of the corridor for (const compTokY of [4, 5, 6]) { configs.push({ corridorX, pocketSide, compTokY }); } } } for (const cfg of configs) { const n = 7; const cx = cfg.corridorX, px = cx + cfg.pocketSide, seatRow = 3; if (px < 0 || px >= n) continue; // pocket must be on-board const wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { if (x === cx) continue; // corridor column P if (x === px && y === seatRow) continue; // lateral pocket at the seat row wall.add(E.keyN({ x, y }, n)); } const st = { rule: CP_RULE, goal: 'harvest_max', round: 0, N: n, hazard: new Set(), sacred: new Set(), wall, pos: { 0: { x: cx, y: seatRow }, 1: { x: cx, y: 1 } }, carry: { 0: 5, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [ { x: cx, y: 6, v: 4, alive: true, guard: false }, { x: cx, y: cfg.compTokY, v: 3, alive: true, guard: false }, ], zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; if (!E._isUniqueBlocker(st, 0)) continue; scanned++; // ESCAPABLE: validateValueEscapable must admit (a strictly-vacating compliant move exists). assert.ok(E.validateValueEscapable(st, 2, CP_RULE), `validateValueEscapable rejected an admissible chokepoint: ${JSON.stringify(cfg)}`); // a strictly-vacating (clearing) move exists: the value-branch consistentMoves is non-empty // and excludes stay (the seat must move). const set = E.consistentMoves(st, 0, CP_RULE); const hasVacate = set.size >= 1 && !set.has('stay'); if (!hasVacate) { missingVacate++; if (!first) first = JSON.stringify(cfg); } // the ADVANCE along P (the next corridor cell toward the seat's own south token) RE-BLOCKS: // it is NOT in the compliant set (it keeps the seat on P). const advanceKey = E.moveKeyOf({ x: cx, y: seatRow }, { x: cx, y: seatRow + 1 }); if (set.has(advanceKey)) { advanceCleared++; if (!first) first = JSON.stringify(cfg); } } assert.ok(scanned >= 12, `scan must exercise many distinct blocker configs (got ${scanned})`); assert.strictEqual(missingVacate, 0, `no strictly-vacating move in ${missingVacate}/${scanned} blocker states: ${first}`); assert.strictEqual(advanceCleared, 0, `the advance-along-P was wrongly compliant in ${advanceCleared}/${scanned} states: ${first}`); console.log(' [P3-CLEARING-STEP-EXISTS] ' + scanned + ' distinct blocker configs: vacate always exists, advance always re-blocks'); }); test('P3.5-YIELD-CSTAR-COMPATIBLE: realized yield-aware total <= REAL C* (compliantCandidatePolicies envelope) on the chokepoint board', () => { const budget = 8; // REAL C* on THIS board = max over the REAL compliant-candidate envelope of each policy's // realized joint total (every policy NEVER violates; the max is an achievable compliant total). // This is the REAL ruleOptimalCeiling machinery (compliantCandidatePolicies + applyMove), driven // over the hand-built chokepoint board — NOT a hand-labelled rollout. const driveJoint = (seat0Policy) => { const s = buildChokepoint(); for (let t = 0; t < budget; t++) { const m0 = seat0Policy(s, t); const m1 = E.nearestCompliantMove(s, 1, CP_RULE); // companion harvests compliantly E.applyMove(s, 0, m0, CP_RULE); E.applyMove(s, 1, m1, CP_RULE); } return s.score[0] + s.score[1]; }; // C* = best joint total achievable by ANY single policy in the REAL envelope for seat 0. const envelope = E.compliantCandidatePolicies(CP_RULE, 0); // the REAL 5-policy envelope assert.strictEqual(envelope.length, 5, 'envelope must be the real 5-policy set'); let cstar = 0; for (const policy of envelope) { const total = driveJoint((s, ts) => policy(s, ts)); if (total > cstar) cstar = total; } // realized = the value-oracle (yield-aware) joint total; it forgoes its own tick to vacate. const realized = driveJoint((s) => E.yieldAwareCompliantMove(s, 0, CP_RULE)); assert.ok(realized <= cstar + 1e-9, `yield duty inflated C* on the chokepoint: realized ${realized} > C* ${cstar}`); console.log(' [P3.5-YIELD-CSTAR-COMPATIBLE] realized=' + realized + ' <= REAL C*=' + cstar); }); test('CSTAR-ENVELOPE-UNCHANGED (P3): compliantCandidatePolicies stays exactly 5; yieldAware is NOT among them', () => { for (const rule of ['avoid_dark', CP_RULE, 'pursue_smallest']) { assert.strictEqual(E.compliantCandidatePolicies(rule, 0).length, 5, `${rule}: C* envelope must stay exactly 5 policies (yieldAware must NOT be added until P6)`); } console.log(' [CSTAR-ENVELOPE-UNCHANGED] envelope length === 5; yieldAware absent'); }); /* =================== P4: base-agnostic positionPriced + _safeStep ======== Extract the local `_posPriced` from nearestCompliantMove into a module helper positionPriced(rule) and hoist _safeStep to a shared helper, adding arrival re-validation to valueOnlyCompliantMove + lookahead2CompliantMove + planMove's caller path (all three currently emit a violating position-priced ARRIVAL on the deliver branch — bfsStep exempts the arrival cell and they have no post-check). Gated by P2's prove-then-keep PIN: P4-PROVE-THEN-KEEP-HOLDS re-runs the REAL harness AFTER the change (no hand-built C*). */ // a position-priced (relational) DELIVER board where the seat is carrying and the // only step onto the zone is a VIOLATING arrival (lands Manhattan-1 to a rival), // while a PASS is compliant. The token TAKE pre-filter never fires (no tokens), so // this exercises the deliver-branch arrival-safety gap directly — bfsStep returns // the zone cell (target-exempt) and without _safeStep the policy emits the violation. function buildPosPricedDeliver(rule) { const n = 7; return { rule, goal: 'deliver_to_zone', round: 0, N: n, hazard: new Set(), sacred: new Set(), wall: new Set(), pos: { 0: { x: 0, y: 0 }, 1: { x: 1, y: 1 } }, carry: { 0: 3, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [], zone: { x: 1, y: 0 }, ghosts: [{ x: 1, y: 1 }], penalty_amt: 1, fx: [], swap: { used: false }, }; } test('P4-POSPRICED-BASE-AGNOSTIC: positionPriced() reads the rule-carried flag with no base detected (and the legacy base fallback)', () => { // a SYNTHETIC rule OBJECT carrying positionPriced:true but NO base:'role'/'relational'. assert.strictEqual(E.positionPriced({ pred: () => false, positionPriced: true }), true, '{positionPriced:true} with no base must be detected as position-priced'); assert.strictEqual(E.positionPriced({ pred: () => false, positionPriced: false }), false, '{positionPriced:false} must NOT be detected'); assert.strictEqual(E.positionPriced({ pred: () => false }), false, 'a plain object with no flag and no base must NOT be position-priced'); // back-compat fallback (value rules are not all stamped yet): a base:'role'/'relational' // rule id with NO positionPriced flag still resolves true via the old base check. assert.strictEqual(E.positionPriced('defer_to_weaker_closer'), true, 'base:role value rule (un-stamped) must resolve true via the base fallback'); assert.strictEqual(E.positionPriced('avoid_adjacent_rival'), true, 'base:relational rule must resolve true via the base fallback'); assert.strictEqual(E.positionPriced('avoid_dark'), false, 'a legacy avoid_* rule must stay non-position-priced (byte-identical fast path)'); console.log(' [P4-POSPRICED-BASE-AGNOSTIC] flag-first detection + base fallback while un-stamped'); }); test('P4-SAFESTEP-VALUEONLY: valueOnlyCompliantMove no longer emits a violating position-priced arrival (returns pass)', () => { const st = buildPosPricedDeliver('avoid_adjacent_rival'); st.__seat__ = 0; // the zone step (1,0) is a violating arrival (Manhattan-1 to the rival); a pass is compliant. assert.ok(E.violates('avoid_adjacent_rival', { x: 0, y: 0 }, { x: 1, y: 0 }, st), 'precondition: zone arrival violates'); const mv = E.valueOnlyCompliantMove(st, 0, 'avoid_adjacent_rival'); assert.deepStrictEqual(mv, { x: 0, y: 0 }, 'valueOnly must return the pass, not the violating zone arrival'); st.__seat__ = 0; assert.ok(!E.violates('avoid_adjacent_rival', st.pos[0], mv, st), 'valueOnly returned move must be compliant'); console.log(' [P4-SAFESTEP-VALUEONLY] valueOnly returns pass instead of the violating (1,0) arrival'); }); test('P4-SAFESTEP-LOOKAHEAD2: lookahead2CompliantMove no longer emits a violating position-priced arrival (returns pass)', () => { const st = buildPosPricedDeliver('avoid_adjacent_rival'); st.__seat__ = 0; const mv = E.lookahead2CompliantMove(st, 0, 'avoid_adjacent_rival'); assert.deepStrictEqual(mv, { x: 0, y: 0 }, 'lookahead2 must return the pass, not the violating zone arrival'); st.__seat__ = 0; assert.ok(!E.violates('avoid_adjacent_rival', st.pos[0], mv, st), 'lookahead2 returned move must be compliant'); console.log(' [P4-SAFESTEP-LOOKAHEAD2] lookahead2 returns pass instead of the violating (1,0) arrival'); }); test('P4-PLANMOVE-NO-VIOLATING-ARRIVAL: planMove does not emit a violating position-priced arrival on a value/position-priced board', () => { const st = buildPosPricedDeliver('avoid_adjacent_rival'); st.__seat__ = 0; const mv = E.planMove(st, 0, 'avoid_adjacent_rival', false); st.__seat__ = 0; assert.ok(!E.violates('avoid_adjacent_rival', st.pos[0], mv, st), 'planMove must not emit the violating zone arrival on a position-priced rule'); assert.deepStrictEqual(mv, { x: 0, y: 0 }, 'planMove must return the pass (the only compliant option here)'); console.log(' [P4-PLANMOVE-NO-VIOLATING-ARRIVAL] planMove rejects the violating (1,0) arrival -> pass'); }); test('P4-SAFESTEP-PERSONA: PersonaPolicy no longer emits a violating position-priced ARRIVAL (returns the compliant pass)', () => { // 5th envelope policy. Repro from the spine review: seat{1,0} carrying, zone{2,0}, // rival{2,1}. The deliver branch bfsStep(...,false,zone) returns the zone cell {2,0} // RAW (target-exempt), but arriving there is Manhattan-1 to the rival -> violates // avoid_adjacent_rival; the pass {1,0} is compliant (dist 2). Without _safeStep the // persona emitted the violating {2,0}; with it routed through safeStep it returns {1,0}. const n = 7; const st = { rule: 'avoid_adjacent_rival', goal: 'deliver_to_zone', round: 0, N: n, hazard: new Set(), sacred: new Set(), wall: new Set(), pos: { 0: { x: 1, y: 0 }, 1: { x: 2, y: 1 } }, carry: { 0: 3, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [], zone: { x: 2, y: 0 }, ghosts: [{ x: 2, y: 1 }], penalty_amt: 1, fx: [], swap: { used: false }, }; st.__seat__ = 0; // precondition: the raw bfsStep arrival onto the zone {2,0} is a VIOLATING step. assert.deepStrictEqual(E.bfsStep(st, 0, 'avoid_adjacent_rival', false, { x: 2, y: 0 }), { x: 2, y: 0 }, 'precondition: bfsStep returns the (target-exempt) zone cell {2,0} raw'); st.__seat__ = 0; assert.ok(E.violates('avoid_adjacent_rival', { x: 1, y: 0 }, { x: 2, y: 0 }, st), 'precondition: zone arrival {2,0} violates (Manhattan-1 to rival {2,1})'); const persona = E.PersonaPolicy('avoid_adjacent_rival', 0); const mv = persona(st, 0, 0); assert.deepStrictEqual(mv, { x: 1, y: 0 }, 'persona must return the compliant pass {1,0}, not the violating zone arrival {2,0}'); st.__seat__ = 0; assert.ok(!E.violates('avoid_adjacent_rival', st.pos[0], mv, st), 'persona returned move must be compliant'); console.log(' [P4-SAFESTEP-PERSONA] persona returns pass {1,0} instead of the violating {2,0} arrival'); }); test('P4-PROVE-THEN-KEEP-HOLDS: re-run the REAL P2 harness AFTER the change — realized<=recomputed-C*, and no drop below any attained baseline C*', () => { // RE-RUN the REAL prove-then-keep machinery (collect() drives real campaigns through // _jointCompliantCeiling/_reachCeiling -> runReport().perCycle). NEVER a hand-built C*. // prove_then_keep.js + its baseline are LOCAL files never committed — skip when absent. let collect, baseline; try { ({ collect } = require('./prove_then_keep.js')); baseline = require('./prove_then_keep_baseline.json'); } catch (e) { if (e.code !== 'MODULE_NOT_FOUND' || !/prove_then_keep/.test(String(e.message))) throw e; console.log(' SKIP P4 prove-then-keep re-run: prove_then_keep.js/baseline not present (never committed)'); return; } const { cells, violations, targetCycles } = collect(); assert.ok(targetCycles > 0, 'harness must exercise role/relational cycles (non-vacuous)'); assert.strictEqual(violations.length, 0, 'P4 must keep realized-total <= recomputed-C*: ' + JSON.stringify(violations.slice(0, 5))); // at EVERY baseline cell marked attained:true (realized-total == old C*), the recomputed // C* must NOT have dropped below the baseline C* — else a previously-attained realized // total would now exceed the ceiling. let attainedChecked = 0; for (const key of Object.keys(baseline)) { const b = baseline[key]; if (!b.attained) continue; const now = cells[key]; assert.ok(now, `attained baseline cell ${key} vanished after P4`); assert.ok(now.cStar >= b.cStar - 1e-9, `attained cell ${key}: recomputed C* ${now.cStar} dropped below baseline C* ${b.cStar}`); attainedChecked++; } assert.ok(attainedChecked > 0, 'must have re-checked at least one attained baseline cell'); console.log(' [P4-PROVE-THEN-KEEP-HOLDS] 0 over-cap; ' + attainedChecked + ' attained cells hold (no C* drop)'); }); test('CSTAR-ENVELOPE-UNCHANGED (P4): compliantCandidatePolicies stays exactly 5; yieldAware still absent', () => { for (const rule of ['avoid_dark', CP_RULE, 'pursue_smallest']) { assert.strictEqual(E.compliantCandidatePolicies(rule, 0).length, 5, `${rule}: C* envelope must stay exactly 5 policies after P4 (yieldAware added only at P6)`); } console.log(' [CSTAR-ENVELOPE-UNCHANGED] envelope length === 5; yieldAware absent (P4)'); }); /* ====================================================================== P5 — value RULE_VARIANTS + daBattery-gated VALUE_VARIANT_LIST + phi resolvers (strict_closest / turn_rank) + forbiddenCellsOf value branch. The three value rules (defer_to_weaker_closer / no_preempt / respect_order) route through violates() via base:'role' + valueLaden:true, are positionPriced, and STAY OUT of the bare-slice2 pool (admitted only under config.daBattery). ====================================================================== */ // P5-VALUE-RULES-ROUTE-VIOLATES: each value rule routes through violates() via // outOfCharacter (base:'role') DESPITE not being in ROLE_VARIANT_LIST. A move // outside consistentMoves is flagged. The defer rule's along-P advance violates // at a chokepoint; for no_preempt/respect_order a verified out-of-character move flags. test('P5-VALUE-RULES-ROUTE-VIOLATES: value rules route through violates() via outOfCharacter despite not being in ROLE_VARIANT_LIST', () => { // none of the three value rules is in ROLE_VARIANT_LIST (pool-membership decoupled). for (const id of ['defer_to_weaker_closer', 'no_preempt', 'respect_order']) { assert.ok(E.RULE_VARIANTS[id] && E.RULE_VARIANTS[id].valueLaden, `${id} must be valueLaden`); assert.ok(E.RULE_VARIANTS[id].base === 'role', `${id} must route via base:'role'`); assert.ok(E.ROLE_VARIANT_LIST.indexOf(id) === -1, `${id} must NOT be in ROLE_VARIANT_LIST`); } // defer: at the chokepoint the along-P advance is a violation (real violates() path). const st = buildChokepoint(); st.__seat__ = 0; const from = st.pos[0]; const advance = { x: 3, y: 4 }; // along P (next corridor cell toward own token) const lateral = { x: 4, y: 3 }; // the strictly-vacating off-P step assert.strictEqual(E.violates('defer_to_weaker_closer', from, advance, st), true, 'defer: advancing along P must violate'); assert.strictEqual(E.violates('defer_to_weaker_closer', from, lateral, st), false, 'defer: the lateral vacate must be compliant'); // no_preempt routes through outOfCharacter too: a move that preempts a token a closer // rival claims is flagged via the REAL violates() path. const np = { rule: 'no_preempt', goal: 'harvest_max', round: 0, N: 7, hazard: new Set(), sacred: new Set(), wall: new Set(), pos: { 0: { x: 0, y: 0 }, 1: { x: 3, y: 0 } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [{ x: 4, y: 0, v: 4, alive: true, guard: false }], zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; np.__seat__ = 0; assert.strictEqual(E.violates('no_preempt', np.pos[0], { x: 1, y: 0 }, np), true, 'no_preempt: approaching the rival-claimed token must violate'); assert.strictEqual(E.violates('no_preempt', np.pos[0], { x: 0, y: 1 }, np), false, 'no_preempt: stepping AWAY from the claimed token is compliant'); console.log(' [P5-VALUE-RULES-ROUTE-VIOLATES] all 3 value rules route via violates()/outOfCharacter, none in ROLE_VARIANT_LIST'); }); // P5-PHI-RESOLVERS: strict_closest / turn_rank argmin-phi on hand-built PUBLIC // states. strict_closest reads CURRENT state only (an injected prevPos/history is // IGNORED — the claimant is recomputed from st.pos every call). test('P5-PHI-RESOLVERS: strict_closest reads CURRENT state only (ignores injected prevPos); turn_rank reads the public clock', () => { const n = 7; // strict_closest: seat 0 at (0,0); a token at (4,0). Seat 1 at (3,0) is the STRICT // CLOSEST live agent to the token, so seat 0 stepping ONTO the token (preempting) is // OUT of character; a step that does NOT take the claimed token stays in character. const base = { rule: 'no_preempt', goal: 'harvest_max', round: 0, N: n, hazard: new Set(), sacred: new Set(), wall: new Set(), pos: { 0: { x: 0, y: 0 }, 1: { x: 3, y: 0 } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [{ x: 4, y: 0, v: 4, alive: true, guard: false }], zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; base.__seat__ = 0; const set0 = E.consistentMoves(base, 0, 'no_preempt'); // stepping toward (and eventually onto) the seat-1-claimed token is the preempt; the // move that approaches the claimed token must NOT be in-character. const towardClaim = E.moveKeyOf(base.pos[0], { x: 1, y: 0 }); assert.ok(!set0.has(towardClaim), 'no_preempt: approaching the seat-1-claimed token must be out-of-character'); assert.ok(set0.has('stay'), 'no_preempt: stay is always compliant (no preempt = a pass)'); // CURRENT-STATE ONLY: inject a misleading prevPos/history; the resolver must IGNORE it // and produce the IDENTICAL consistent set (claimant recomputed from st.pos). const withHist = Object.assign({}, base, { hazard: new Set(), sacred: new Set(), wall: new Set(), prevPos: { 0: { x: 9, y: 9 }, 1: { x: 9, y: 9 } }, // bogus history history: [{ pos: { 1: { x: 0, y: 0 } } }], // bogus "was closing" cue }); withHist.__seat__ = 0; const setHist = E.consistentMoves(withHist, 0, 'no_preempt'); assert.deepStrictEqual([...setHist].sort(), [...set0].sort(), 'no_preempt: injected prevPos/history must be IGNORED (strict_closest reads CURRENT state only)'); // turn_rank: a clock surfaces the precedence; the seat yields at c when it is NOT its // turn per the public segment. Build a clock where seg names seat 1 as having precedence. const ro = { rule: 'respect_order', goal: 'harvest_max', round: 0, N: n, hazard: new Set(), sacred: new Set(), wall: new Set(), pos: { 0: { x: 2, y: 2 }, 1: { x: 2, y: 1 } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [{ x: 2, y: 0, v: 4, alive: true, guard: false }], clock: { 0: { seg: 1, segN: 2, advanceOn: 'own_turn' }, 1: { seg: 1, segN: 2, advanceOn: 'own_turn' } }, zone: null, penalty_amt: 1, fx: [], swap: { used: false }, }; ro.__seat__ = 0; const setRO = E.consistentMoves(ro, 0, 'respect_order'); assert.ok(setRO.has('stay'), 'respect_order: stay (yield) is always compliant'); // flipping the public clock segment changes the consistent set (it is keyed on the // public phase-index, never the hidden rule). const ro2 = Object.assign({}, ro, { hazard: new Set(), sacred: new Set(), wall: new Set(), clock: { 0: { seg: 0, segN: 2, advanceOn: 'own_turn' }, 1: { seg: 0, segN: 2, advanceOn: 'own_turn' } }, }); ro2.__seat__ = 0; const setRO2 = E.consistentMoves(ro2, 0, 'respect_order'); assert.ok([...setRO].sort().join(',') !== [...setRO2].sort().join('') || setRO.size === setRO2.size, 'respect_order: the set is a function of the public clock segment'); console.log(' [P5-PHI-RESOLVERS] strict_closest ignores injected prevPos/history; turn_rank reads the public clock'); }); // P5-FORBIDDEN-VALUE-BRANCH: forbiddenCellsOf for a value rule returns the contested- // cell set from LIVE companion positions/ranks (NOT the role neighbour-enumeration). // Flips correctly under do(public_rank) (a carry flip removes the yield duty -> empty). test('P5-FORBIDDEN-VALUE-BRANCH: forbiddenCellsOf(value) = contested-cell set from live companion ranks; flips under do(public_rank)', () => { const st = buildChokepoint(); st.__seat__ = 0; const forb = E.forbiddenCellsOf(st, 'defer_to_weaker_closer'); // the contested set is the companion's unique path P (a non-empty corridor segment), // NOT merely the 4 orthogonal neighbours of the seat. It must contain cells ALONG P // (e.g. the next corridor cell the companion needs) — including the seat's own cell c. assert.ok(forb.size > 0, 'value forbidden set must be non-empty when the seat is the unique blocker'); assert.ok(forb.has(E.keyN({ x: 3, y: 3 }, st.N)), 'forbidden must include the contested seat cell c=(3,3)'); assert.ok(forb.has(E.keyN({ x: 3, y: 4 }, st.N)), 'forbidden must include the along-P cell (3,4) (the companion needs it)'); // do(public_rank): make the seat WEAKER than the companion (carry flip). The yield duty // vanishes (the companion is no longer weaker) -> the contested set is EMPTY. const flipped = buildChokepoint(); flipped.carry = { 0: 0, 1: 5 }; // seat 0 now weaker than companion 1 flipped.__seat__ = 0; const forb2 = E.forbiddenCellsOf(flipped, 'defer_to_weaker_closer'); assert.strictEqual(forb2.size, 0, 'do(public_rank) carry-flip removes the yield duty -> empty contested set'); console.log(' [P5-FORBIDDEN-VALUE-BRANCH] contested-cell set from live ranks; carry-flip -> empty (do(public_rank))'); }); // P5-RESPECT-ORDER-NOT-ANCHOR: respect_order carries scoredAnchor:false so P7 can never // select it as a scored anchor (design §2 / residual risk #4). The two anchor-able value // rules carry scoredAnchor:true. test('P5-RESPECT-ORDER-NOT-ANCHOR: respect_order is scoredAnchor:false; defer/no_preempt are scoredAnchor:true', () => { assert.strictEqual(E.RULE_VARIANTS['respect_order'].scoredAnchor, false, 'respect_order must carry scoredAnchor:false (never a scored anchor)'); assert.strictEqual(E.RULE_VARIANTS['defer_to_weaker_closer'].scoredAnchor, true, 'defer_to_weaker_closer must be scoredAnchor:true'); assert.strictEqual(E.RULE_VARIANTS['no_preempt'].scoredAnchor, true, 'no_preempt must be scoredAnchor:true'); // VALUE_VARIANT_LIST exists and contains exactly the valueLaden rules. assert.ok(Array.isArray(E.VALUE_VARIANT_LIST), 'VALUE_VARIANT_LIST must be exported'); const expect = ['defer_to_weaker_closer', 'no_preempt', 'respect_order'].sort(); assert.deepStrictEqual(E.VALUE_VARIANT_LIST.slice().sort(), expect, 'VALUE_VARIANT_LIST must be exactly the valueLaden rules'); console.log(' [P5-RESPECT-ORDER-NOT-ANCHOR] respect_order scoredAnchor:false; VALUE_VARIANT_LIST = 3 valueLaden rules'); }); test('CSTAR-ENVELOPE-UNCHANGED (P5): compliantCandidatePolicies stays exactly 5; yieldAware still absent (enters at P6)', () => { for (const rule of ['avoid_dark', CP_RULE, 'no_preempt', 'respect_order']) { assert.strictEqual(E.compliantCandidatePolicies(rule, 0).length, 5, `${rule}: C* envelope must stay exactly 5 policies after P5 (yieldAware added only at P6)`); } console.log(' [CSTAR-ENVELOPE-UNCHANGED] envelope length === 5; yieldAware absent (P5)'); }); /* =================== P6: yield-aware envelope add (engine side) ========== Plan 2026-06-18 §P6 / design §8: the yield-aware oracle is DELIBERATELY added to compliantCandidatePolicies — but ONLY on the daBattery path AND only for a valueLaden rule. The legacy path (daBattery omitted/false) stays EXACTLY 5 so every non-daBattery caller (ruleOptimalCeiling / multiAgentCeiling / legacy _reachCeiling) is byte-identical. The 6th policy is the REAL yieldAwareCompliantMove (the unique vacating policy), so a daBattery defer joint-rollout C* can price the forgone destination. A daBattery NON-value rule is still exactly 5 (the add is value-scoped). ======================================================================== */ test('CSTAR-ENVELOPE-UNCHANGED (P6): legacy=5 (yieldAware absent); daBattery+valueLaden=6 (yieldAware present); daBattery non-value=5', () => { // (1) legacy path unchanged for EVERY rule (value or not): exactly 5, yieldAware absent. for (const rule of ['avoid_dark', CP_RULE, 'no_preempt', 'respect_order', 'pursue_smallest']) { assert.strictEqual(E.compliantCandidatePolicies(rule, 0).length, 5, `${rule}: legacy (daBattery false) C* envelope must stay exactly 5`); assert.strictEqual(E.compliantCandidatePolicies(rule, 0, false).length, 5, `${rule}: explicit daBattery:false C* envelope must stay exactly 5`); } // (2) daBattery path: a valueLaden rule grows to 6 with the REAL yieldAware policy as the // 6th, and that 6th policy emits the lateral vacate at the chokepoint (it IS yieldAware). for (const rule of E.VALUE_VARIANT_LIST) { const env6 = E.compliantCandidatePolicies(rule, 0, true); assert.strictEqual(env6.length, 6, `${rule}: daBattery valueLaden envelope must be 6 (yieldAware added)`); // the first 5 are byte-identical references to the legacy 5 by construction (same code path); // the 6th must behave as yieldAwareCompliantMove on the chokepoint (lateral vacate to (4,3)). const st = buildChokepoint(); const sixth = env6[5](st); const direct = E.yieldAwareCompliantMove(st, 0, CP_RULE); if (rule === CP_RULE) { assert.deepStrictEqual(sixth, direct, `${rule}: 6th policy is NOT yieldAwareCompliantMove`); assert.deepStrictEqual(sixth, { x: 4, y: 3 }, `${rule}: 6th policy must emit the lateral vacate (4,3)`); } } // (3) daBattery path for a NON-value rule stays 5 (the add is value-scoped, not flag-scoped). for (const rule of ['avoid_dark', 'pursue_smallest']) { assert.strictEqual(E.compliantCandidatePolicies(rule, 0, true).length, 5, `${rule}: daBattery NON-value envelope must stay exactly 5`); } console.log(' [CSTAR-ENVELOPE-UNCHANGED] legacy=5 / daBattery+valueLaden=6 (REAL yieldAware) / daBattery non-value=5'); }); /* =================== W1.3: generic lexical oracle (answer key) =========== design §A persona / §D Discovery scorer. lexicalOracle(st,seat,ordering,rule) is the label-free answer key: the move the ORDERING prescribes (highest engaged attitude, lower ones break ties lexically), consumed from lexFilter (single source) and emitted via safeStep. It GENERALIZES yieldAwareCompliantMove — {D}-only must reproduce it. ======================================================================== */ test('W1.3-ORACLE-D-ONLY-EQ-YIELDAWARE: {D}-only oracle == yieldAwareCompliantMove at AND off the chokepoint', () => { // AT the chokepoint: seat 0 is the unique blocker -> lateral vacate (4,3). const st = buildChokepoint(); assert.ok(E._isUniqueBlocker(st, 0), 'seat 0 must be the unique blocker at the chokepoint'); assert.deepStrictEqual(E.lexicalOracle(buildChokepoint(), 0, ['D'], CP_RULE), E.yieldAwareCompliantMove(buildChokepoint(), 0, CP_RULE), '{D}-only oracle must equal yieldAwareCompliantMove at the chokepoint'); assert.deepStrictEqual(E.lexicalOracle(buildChokepoint(), 0, ['D'], CP_RULE), { x: 4, y: 3 }, '{D}-only oracle must emit the lateral vacate (4,3) at the chokepoint'); // OFF the chokepoint: companion already past c -> no duty -> own-goal pursuit (3,4). const st2 = buildChokepoint(); st2.pos[1] = { x: 3, y: 5 }; assert.ok(!E._isUniqueBlocker(st2, 0), 'seat 0 is NOT the unique blocker once the companion is past c'); const off1 = buildChokepoint(); off1.pos[1] = { x: 3, y: 5 }; const off2 = buildChokepoint(); off2.pos[1] = { x: 3, y: 5 }; assert.deepStrictEqual(E.lexicalOracle(off1, 0, ['D'], CP_RULE), E.yieldAwareCompliantMove(off2, 0, CP_RULE), '{D}-only oracle must equal yieldAwareCompliantMove off the chokepoint (own-goal pursuit)'); console.log(' [W1.3-ORACLE-D-ONLY-EQ-YIELDAWARE] {D}-only == yieldAware at chokepoint (4,3) AND off-chokepoint (own goal)'); }); test('W1.3-ORACLE-IN-LEXFILTER: the oracle move is ALWAYS a prescribed move (in lexFilter) over all 24 orderings', () => { const perms = (a) => a.length <= 1 ? [a.slice()] : a.flatMap((x, i) => perms(a.slice(0, i).concat(a.slice(i + 1))).map(p => [x].concat(p))); const ORDERINGS = perms(['G', 'C', 'D', 'N']); let checks = 0; for (const ordering of ORDERINGS) { const st = buildChokepoint(); const from = { ...st.pos[0] }; const mv = E.lexicalOracle(st, 0, ordering, CP_RULE); const lex = E.lexFilter(buildChokepoint(), 0, ordering); const key = (mv.x === from.x && mv.y === from.y) ? 'stay' : E.moveKeyOf(from, mv); assert.ok(lex.has(key), `${ordering.join('')}: oracle emitted ${key} not in lexFilter {${[...lex]}}`); checks++; } console.log(' [W1.3-ORACLE-IN-LEXFILTER] ' + checks + ' orderings: oracle move always prescribed (in lexFilter)'); }); test('W1.3-ORACLE-CONFUSABLE-DIVERGES: an adjacent-transposition confusable prescribes a DIFFERENT move (orderings discriminable)', () => { // boardOpen (no_preempt-vs-goal): a high token is strict-closest to seat 1, so N engages // (leave it) while G tempts seat 0 toward it. Swapping the top two attitudes G<->N flips // the prescription (N-first: leave/stay; G-first: advance) — the answer-key divergence. const n = 5; const make = () => ({ rule: CP_RULE, goal: 'harvest_max', round: 0, N: n, hazard: new Set([E.keyN({ x: 1, y: 0 }, n)]), sacred: new Set(), wall: new Set(), pos: { 0: { x: 0, y: 0 }, 1: { x: 4, y: 2 } }, carry: { 0: 0, 1: 0 }, score: { 0: 0, 1: 0 }, penalty: { 0: 0, 1: 0 }, facing: {}, tokens: [{ x: 3, y: 2, v: 6, alive: true, guard: false }, { x: 0, y: 2, v: 2, alive: true, guard: false }], zone: null, penalty_amt: 1, fx: [], swap: { used: false } }); const nFirst = E.lexicalOracle(make(), 0, ['N', 'G', 'C', 'D'], CP_RULE); const gFirst = E.lexicalOracle(make(), 0, ['G', 'N', 'C', 'D'], CP_RULE); assert.ok(!(nFirst.x === gFirst.x && nFirst.y === gFirst.y), `confusable G<->N must diverge: N-first=${JSON.stringify(nFirst)} G-first=${JSON.stringify(gFirst)}`); console.log(` [W1.3-ORACLE-CONFUSABLE-DIVERGES] N-first=${JSON.stringify(nFirst)} != G-first=${JSON.stringify(gFirst)} (discriminable)`); }); // W1.5 buildValueDemo: the persona-enacting value demo rides the SAME Sigma* battery // (6 exactly-two-engaged conflict scenes) and the union of REVEALED pairwise edges (each // recovered from the OBSERVED oracle move alone) induces the seated ordering — i.e. the // demo makes the ordering inducible-in-principle. Appended LAST so prior indices are stable. test('W1.5-VALUE-DEMO: 6 conflict scenes per value rule, each move in lexFilter, winner observable, union induces the ordering', () => { const perms = (a) => a.length <= 1 ? [a.slice()] : a.flatMap((x, i) => perms(a.slice(0, i).concat(a.slice(i + 1))).map(p => [x].concat(p))); const ORDERINGS = perms(['G', 'C', 'D', 'N']); let sceneChecks = 0, orderingChecks = 0; for (const rule of ['defer_to_weaker_closer', 'no_preempt', 'respect_order']) { for (const ordering of ORDERINGS) { const vd = E.buildValueDemo(rule, ordering); assert.strictEqual(vd.steps.length, 6, `${rule}/${ordering.join('')}: must produce 6 pairwise-comparison scenes (the full Sigma* battery)`); // build the directed-edge set winner->loser from the OBSERVED moves; each step's move // must be a prescribed move (in lexFilter) and its winner recoverable from the move. const edges = new Set(); for (const s of vd.steps) { const lex = E.lexFilter(s.board, 0, ordering); assert.ok(lex.has(s.move), `${rule}/${ordering.join('')}: demo move ${s.move} not in lexFilter`); // winner must outrank loser in the seated ordering (the edge is correct, not leaked) assert.ok(ordering.indexOf(s.winner) < ordering.indexOf(s.loser), `${rule}/${ordering.join('')}: revealed edge ${s.winner}>${s.loser} contradicts seated order`); edges.add(s.winner + s.loser); sceneChecks++; } // the 6 edges are exactly the 6 ordered pairs of the seated order (transitive closure // recovers the unique total order) — the ordering is inducible-in-principle from the demo. for (let i = 0; i < ordering.length; i++) for (let j = i + 1; j < ordering.length; j++) assert.ok(edges.has(ordering[i] + ordering[j]), `${rule}/${ordering.join('')}: missing comparison ${ordering[i]}>${ordering[j]} (ordering not inducible)`); orderingChecks++; } } console.log(` [W1.5-VALUE-DEMO] ${orderingChecks} (rule x ordering) demos, ${sceneChecks} conflict scenes — every move prescribed, every revealed edge correct, union induces the seated ordering`); }); // W1.5-§B CONTINUOUS play-memory demo: playTrajectoryPool + pickRepresentative produce a // CONTINUOUS lived trajectory whose every move is lexicalOracle (persona-faithful by // construction) and whose chosen representatives' UNION of exercised pairwise comparisons // COVERS all 6 (so the seated ordering stays inducible-in-principle, the same property W1.5 // proves, now spread across lived walks). Additive — never on the scored battery path. test('W1.5-CONTINUOUS: playTrajectory moves are all lexicalOracle, pickRepresentative set-covers the 6 comparisons (ordering inducible across lived walks)', () => { const perms = (a) => a.length <= 1 ? [a.slice()] : a.flatMap((x, i) => perms(a.slice(0, i).concat(a.slice(i + 1))).map(p => [x].concat(p))); const ORDERINGS = perms(['G', 'C', 'D', 'N']); const PAIRS = ['CD', 'CG', 'CN', 'DG', 'DN', 'GN']; let poolChecks = 0, moveChecks = 0; // a representative spread of orderings (full 24 per rule is heavy; sample 8 deterministically). const sampled = ORDERINGS.filter((_, i) => i % 3 === 0); for (const rule of ['defer_to_weaker_closer', 'no_preempt', 'respect_order']) { for (const ordering of sampled) { const pool = E.playTrajectoryPool(rule, ordering, { N: 16 }); assert.ok(pool.trajs.length > 0, `${rule}/${ordering.join('')}: empty pool`); // determinism: a fresh pool produces the SAME first trajectory's move list. const pool2 = E.playTrajectoryPool(rule, ordering, { N: 16 }); assert.deepStrictEqual(pool2.trajs[0].steps.map(s => s.move), pool.trajs[0].steps.map(s => s.move), `${rule}/${ordering.join('')}: playTrajectoryPool non-deterministic`); // FAITHFUL BY CONSTRUCTION: every step move == the lexicalOracle move replayed on a clone. for (const tr of pool.trajs) { const st = E.playTrajectory(rule, ordering, tr.seed, {}).board0; // pristine seed let cur = st; for (const step of tr.steps) { const from = { ...cur.pos[0] }; const mv = E.lexicalOracle(cur, 0, ordering, rule); const key = (mv.x === from.x && mv.y === from.y) ? 'stay' : E.moveKeyOf(from, mv); assert.strictEqual(step.move, key, `${rule}/${ordering.join('')} seed ${tr.seed}: step move ${step.move} != lexicalOracle ${key}`); E.applyMove(cur, 0, mv, rule); E.stepCompanion(cur, rule); moveChecks++; } } // SET-COVER: the chosen representatives' union of exercised comparisons covers all 6, // so the seated ordering is inducible-in-principle from the lived walks. const reps = E.pickRepresentative(pool); const covered = new Set(); for (const tr of reps) tr.comparisons.forEach(c => covered.add(c)); for (const p of PAIRS) assert.ok(covered.has(p), `${rule}/${ordering.join('')}: comparison ${p} NOT covered by representatives (ordering not inducible across walks)`); // reserved = the untouched leftover pool for the deferred held-out eval. assert.strictEqual(reps.length + pool.reserved.length >= pool.trajs.length, true, `${rule}/${ordering.join('')}: reserved accounting wrong`); poolChecks++; } } console.log(` [W1.5-CONTINUOUS] ${poolChecks} (rule x ordering) pools, ${moveChecks} faithful-by-construction step checks (every move == lexicalOracle), representatives set-cover all 6 comparisons`); }); // W1.5-CONTINUOUS-WALK (DEMO-side claim, sibling to W1.5-CONTINUOUS): the app DEMO renders ONE // literal continuous oracle walk chosen by E.pickContinuousWalk(pool). This asserts the DEMO claim // SEPARATELY from the BATTERY claim above (which keeps the 6-cover guarantee on pickRepresentative). // pickContinuousWalk returns exactly ONE pool member that (a) is drawn from pool.trajs (identity // membership), (b) has max comparison-coverage (>= every other member, shortest on ties, // deterministic across a fresh pool build), (c) is one persona-faithful continuous walk (every move // == lexicalOracle on its pristine board0), (d) does NOT mutate pool.reserved (demo/battery // independence), and (e) satisfies the top1 GESTALT property (the rank-1 seated attitude wins // whenever it appears in any of the walk's conflict pairs). It deliberately does NOT assert 6-cover // on the single walk — the documented 3-4/6 single-walk ceiling is expected; the full 6-cover lives // in the pickRepresentative assertion (above) + the held-out battery (prove_*). test('W1.5-CONTINUOUS-WALK: pickContinuousWalk returns ONE max-coverage faithful walk (top1 gestalt), does not mutate reserved (demo != battery)', () => { const perms = (a) => a.length <= 1 ? [a.slice()] : a.flatMap((x, i) => perms(a.slice(0, i).concat(a.slice(i + 1))).map(p => [x].concat(p))); const ORDERINGS = perms(['G', 'C', 'D', 'N']); const sampled = ORDERINGS.filter((_, i) => i % 3 === 0); let walkChecks = 0, moveChecks = 0, top1Checks = 0, maxCov = 0; for (const rule of ['defer_to_weaker_closer', 'no_preempt', 'respect_order']) { for (const ordering of sampled) { const pool = E.playTrajectoryPool(rule, ordering, { N: 16 }); assert.ok(pool.trajs.length > 0, `${rule}/${ordering.join('')}: empty pool`); const walk = E.pickContinuousWalk(pool); assert.ok(walk, `${rule}/${ordering.join('')}: pickContinuousWalk returned nothing`); // (a) IDENTITY MEMBERSHIP: the walk is a member of pool.trajs (not a fresh/forged trajectory). assert.ok(pool.trajs.includes(walk), `${rule}/${ordering.join('')}: walk is not an identity member of pool.trajs`); // (b) MAX COVERAGE >= every other pool member; shortest on coverage ties. const cov = walk.comparisons.length; for (const tr of pool.trajs) { assert.ok(cov >= tr.comparisons.length, `${rule}/${ordering.join('')}: walk coverage ${cov} < a pool member's ${tr.comparisons.length}`); if (tr.comparisons.length === cov) assert.ok(walk.steps.length <= tr.steps.length, `${rule}/${ordering.join('')}: a same-coverage member is shorter than the chosen walk`); } if (cov > maxCov) maxCov = cov; // (b cont.) DETERMINISM: a fresh pool build picks the SAME walk (same seed + move list). const pool2 = E.playTrajectoryPool(rule, ordering, { N: 16 }); const walk2 = E.pickContinuousWalk(pool2); assert.strictEqual(walk2.seed, walk.seed, `${rule}/${ordering.join('')}: pickContinuousWalk non-deterministic (seed differs)`); assert.deepStrictEqual(walk2.steps.map(s => s.move), walk.steps.map(s => s.move), `${rule}/${ordering.join('')}: pickContinuousWalk non-deterministic (move list differs)`); // (c) ONE CONTINUOUS PERSONA-FAITHFUL WALK: every move == lexicalOracle on its pristine board0. const st = E.playTrajectory(rule, ordering, walk.seed, {}).board0; let cur = st; for (const step of walk.steps) { const from = { ...cur.pos[0] }; const mv = E.lexicalOracle(cur, 0, ordering, rule); const key = (mv.x === from.x && mv.y === from.y) ? 'stay' : E.moveKeyOf(from, mv); assert.strictEqual(step.move, key, `${rule}/${ordering.join('')} seed ${walk.seed}: step move ${step.move} != lexicalOracle ${key}`); E.applyMove(cur, 0, mv, rule); E.stepCompanion(cur, rule); moveChecks++; } // (d) NO RESERVED MUTATION: calling pickContinuousWalk leaves pool.reserved untouched (it is // never populated by the demo selector — only pickRepresentative moves leftovers to reserved). assert.deepStrictEqual(pool.reserved, [], `${rule}/${ordering.join('')}: pickContinuousWalk mutated pool.reserved (demo/battery not independent)`); // (e) TOP1 GESTALT: the rank-1 seated attitude wins in EVERY conflict pair it appears in. const rank1 = ordering[0]; for (const step of walk.steps) { const cp = step.conflictPair; if (!cp) continue; if (cp.winner === rank1 || cp.loser === rank1) { assert.strictEqual(cp.winner, rank1, `${rule}/${ordering.join('')}: rank-1 ${rank1} engaged in ${cp.comparison} but did not win`); top1Checks++; } } walkChecks++; } } // honest single-walk ceiling: max coverage on this substrate is 3-4/6, NEVER asserted to be 6. assert.ok(maxCov >= 1 && maxCov <= 6, `unexpected single-walk max coverage ${maxCov}`); console.log(` [W1.5-CONTINUOUS-WALK] ${walkChecks} (rule x ordering) single walks, ${moveChecks} faithful step checks, ${top1Checks} top1 wins (rank-1 wins whenever engaged), single-walk max coverage = ${maxCov}/6 (3-4/6 ceiling expected; 6-cover lives in the battery)`); }); /* ============================================================================ PARK PERSONA GRIDWORLD (design 2026-07-03) — engine substrate gates. Additive: no existing engine gate is touched; makeParkBoard/parkStep live off every scored path. The campaign PARK-* gates prove the overlay invariants (survival / recovery / C1 / default-off); these prove the board substrate. ========================================================================== */ const _parkCanon = (st) => { const arr = (s) => [...s].sort((a, b) => a - b); const p = st.park; return JSON.stringify({ N: st.N, wall: arr(st.wall), hazard: arr(st.hazard), walkway: arr(p.walkway), verge: arr(p.verge), deep: arr(p.deep), distDeep: p.distDeep, clusters: p.clusters, chain: p.chain, contracts: p.contracts, retire: p.retire, spawn: p.spawn, companionSpawn: p.companionSpawn, tokens: st.tokens, pos: st.pos, }); }; // PARK-GEN: the generator is a DETERMINISTIC pure function of the public seed (no rule / // persona parameter exists), emits the spec §1 stage — N=20, tree-wall border, walkway // ring+cross, two-tone danger field (verge = 1-cell band, no damage; deep = interior), gem // clusters sized 1-3 on the walkway, a 3-destination chain + 2 companion contracts with // off-lane verge stations — and the game board is a DIFFERENT park than the demo board. test('PARK-GEN: makeParkBoard = deterministic seed-pure N=20 two-tone park with chain + contracts', () => { for (const seed of [5, 7, 11]) { const st = E.makeParkBoard(seed), p = st.park; assert.strictEqual(_parkCanon(E.makeParkBoard(seed)), _parkCanon(st), `seed ${seed}: not deterministic`); assert.strictEqual(st.N, 20, 'park must be N=20'); const n = st.N; for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const kk = y * n + x, border = (x === 0 || y === 0 || x === n - 1 || y === n - 1); assert.strictEqual(st.wall.has(kk), border, `seed ${seed}: wall != border at ${x},${y}`); if (border) continue; const classes = (p.walkway.has(kk) ? 1 : 0) + (p.verge.has(kk) ? 1 : 0) + (p.deep.has(kk) ? 1 : 0); assert.strictEqual(classes, 1, `seed ${seed}: cell ${x},${y} not in exactly one class`); if (p.deep.has(kk)) assert.strictEqual(p.distDeep[kk], 0); // the 1-cell band around the deep field is ALWAYS verge (a thin all-verge meadow may // add verge cells farther out — verge is the field's walkway-adjacent band, never deep). if (p.distDeep[kk] === 1) assert.ok(p.verge.has(kk), `seed ${seed}: band-1 cell ${x},${y} not verge`); if (p.verge.has(kk)) assert.ok(p.distDeep[kk] >= 1, `seed ${seed}: verge overlaps deep`); if (p.walkway.has(kk)) assert.ok(p.distDeep[kk] >= 2, `seed ${seed}: walkway inside the caution band`); } assert.ok(p.deep.size > 20, `seed ${seed}: deep field too small to matter`); assert.strictEqual(st.hazard.size, p.deep.size, 'st.hazard mirrors the deep set'); assert.strictEqual(p.clusters.length, st.tokens.length); for (const c of p.clusters) { assert.ok(c.v >= 1 && c.v <= 3, 'cluster size 1-3'); assert.ok(p.walkway.has(c.y * n + c.x), 'clusters sit on the walkway (perimeter gem trees)'); } assert.strictEqual(p.chain.length, 3, '3-destination chain'); assert.strictEqual(p.contracts.length, 2, '2 companion contracts'); for (const ct of p.contracts) assert.ok(p.verge.has(ct.station.y * n + ct.station.x), 'stations idle on the verge'); assert.deepStrictEqual(st.pos[1], p.companionSpawn, 'companion seated at its first station'); assert.ok(p.walkway.has(p.spawn.y * n + p.spawn.x), 'player spawns on the walkway'); const gm = E.makeParkBoard(seed, { game: true }); assert.ok(gm.park.game && !p.game, 'phase flag recorded'); assert.notStrictEqual(_parkCanon(gm), _parkCanon(st), `seed ${seed}: game park must be a fresh layout`); } console.log(' [PARK-GEN] 3 seeds x {demo,game}: deterministic, border wall, 1-class partition, verge=band-1, clusters/chain/contracts placed'); }); // PARK-PHYSICS: the death channel is UNIVERSAL physics — hearts drop EXACTLY on a // non-deep -> deep ENTRY (never per deep cell, never on the verge) for the player; the // companion NEVER stands in the deep field across every persona's full playout; an // illegal input is a no-op fat-finger tally (spec P2 §A.1). test('PARK-PHYSICS: deep-entry heart on entry only; companion never in deep; illegal input = noise no-op', () => { let entries = 0, deepRuns = 0, companionTicks = 0; for (const seed of [5, 7]) { for (const persona of E.PARK_PERSONAS) { const st = E.makeParkBoard(seed); const P = E.parkStart(st); let hearts = P.hearts; while (!P.over) { const preDeep = st.park.deep.has(st.pos[0].y * st.N + st.pos[0].x); E.parkStep(P, E.parkOracleMove(P, persona)); const postDeep = st.park.deep.has(st.pos[0].y * st.N + st.pos[0].x); if (!preDeep && postDeep) { assert.strictEqual(P.hearts, hearts - 1, 'entry must cost exactly 1 heart'); entries++; } else { assert.strictEqual(P.hearts, hearts, 'no heart change off-entry'); if (preDeep && postDeep) deepRuns++; } hearts = P.hearts; companionTicks++; assert.ok(!st.park.deep.has(st.pos[1].y * st.N + st.pos[1].x), `companion entered the deep field (seed ${seed}, ${persona.join('>')})`); } } } assert.ok(entries >= 4 && deepRuns >= 4, `vacuous physics sweep: entries=${entries} deepRuns=${deepRuns}`); // illegal input (into the border tree wall) is INPUT NOISE (spec P2 §A.1): a fat-finger // tally — no move record, no turn, nothing the blind readouts ever see. const P2 = E.parkStart(E.makeParkBoard(5)); const at = { ...P2.st.pos[0] }; const wallward = P2.st.pos[0].y >= 10 ? 'D' : 'U'; // spawn sits on the ring: one step off-board/wall const rec = E.parkStep(P2, P2.st.wall.has((P2.st.pos[0].y + (wallward === 'D' ? 1 : -1)) * P2.st.N + P2.st.pos[0].x) ? wallward : 'nonsense'); assert.strictEqual(rec.move, null, 'illegal input must resolve to no move'); assert.strictEqual(rec.noise, true, 'illegal input must be flagged as noise'); assert.strictEqual(P2.inputNoise, 1, 'illegal input must tally on inputNoise'); assert.strictEqual(P2.turns, 0, 'noise must not consume a turn'); assert.strictEqual(P2.moves.length, 0, 'noise must not enter the effective move stream'); assert.deepStrictEqual(P2.st.pos[0], at, 'noise must not move the player'); console.log(` [PARK-PHYSICS] ${entries} charged entries, ${deepRuns} free in-deep steps, companion clean over ${companionTicks} ticks; noise no-op`); }); // PARK-ORACLE: every oracle move is inside the persona's own parkLexFilter set (faithful // BY CONSTRUCTION — the no-violation arm the campaign survival gate rides on), and the // blind pairwise-win recovery returns the demonstrated order on both phases (engine-level // spot check; the 6/6 x 8-seed sweep is the campaign PARK-ORDER-RECOVERABLE gate). test('PARK-ORACLE: oracle moves are lexFilter-faithful and blind-recoverable (both phases)', () => { let moveChecks = 0; for (const game of [false, true]) { for (const persona of E.PARK_PERSONAS) { const P = E.parkStart(E.makeParkBoard(7, { game })); const ordering = E.parkOrderingFor(persona); while (!P.over) { const mv = E.parkOracleMove(P, persona); assert.ok(E.parkLexFilter(P, ordering).has(mv), 'oracle move outside its own lexFilter'); E.parkStep(P, mv); moveChecks++; } const rec = E.parkRecoverOrder(E.makeParkBoard(7, { game }), P.moves); assert.deepStrictEqual(rec, persona, `${game ? 'game' : 'demo'} recovery != persona ${persona.join('>')}`); } } assert.ok(moveChecks >= 300, `vacuous oracle sweep: ${moveChecks} moves`); console.log(` [PARK-ORACLE] ${moveChecks} faithful oracle moves, 12/12 persona x phase blind recoveries exact`); }); /* ============================================================================ PARK TASK BATTERY (spec 2026-07-03 P2 §B) — engine generator gates. Additive: five minigame kinds over the same two-tone grammar + parkStep physics; the campaign TASK-* gates prove the lifecycle/report invariants. ========================================================================== */ const _TASK_HZ = { ice: { kind: 'ice', damage: 0, d: 2 }, meadow: { kind: 'meadow', damage: 1, d: 2 }, lava: { kind: 'lava', damage: 2, d: 2 } }; const _TASK_SEEDS = [11, 12, 13, 14, 15, 16]; // >= 6 public seeds per gate // per-kind FOCAL sigma denominators (the facet each kind isolates; m3 = the D chokepoint) const _TASK_FOCAL = { m1: ['gc'], m2: ['gk'], m3: ['ck'], m4: ['gc', 'gk'], m5: [] }; const _taskCell = (kind, seed) => ({ personaStream: 0, goalVariant: (kind === 'm1' || kind === 'm2') && seed % 2 === 0 ? 'deliver' : 'harvest', hazard: { ..._TASK_HZ[seed % 3 === 0 ? 'lava' : seed % 3 === 1 ? 'meadow' : 'ice'] }, seed, }); // TASK-C1: a task board is a PURE function of the PUBLIC cell fields (kind / goalVariant / // hazard / seed) — cells differing ONLY in the persona stream produce byte-identical // boards (nothing rendered can be a function of the hidden persona), and the generator is // deterministic. Also proves the shared grammar: 1-class terrain partition, verge = the // walkway-adjacent band, clusters on walkable cells, stations idle on the verge. test('TASK-C1: task boards byte-identical across persona streams (pure public cells), grammar intact', () => { let boards = 0; for (const kind of E.PARK_TASK_KINDS) { for (const seed of [11, 12]) { const cell = _taskCell(kind, seed); const st = E.makeParkTask(kind, cell), p = st.park; assert.strictEqual(_parkCanon(E.makeParkTask(kind, { ...cell, personaStream: 3 })), _parkCanon(st), `${kind} seed ${seed}: board bytes moved with the persona stream`); assert.strictEqual(_parkCanon(E.makeParkTask(kind, cell)), _parkCanon(st), `${kind} seed ${seed}: not deterministic`); assert.ok(st.N >= 10 && st.N <= 14, `${kind}: small board 10-14`); for (let y = 0; y < st.N; y++) for (let x = 0; x < st.N; x++) { const kk = y * st.N + x, border = (x === 0 || y === 0 || x === st.N - 1 || y === st.N - 1); assert.strictEqual(st.wall.has(kk), border, `${kind}: wall != border at ${x},${y}`); if (border) continue; const classes = (p.walkway.has(kk) ? 1 : 0) + (p.verge.has(kk) ? 1 : 0) + (p.deep.has(kk) ? 1 : 0); assert.strictEqual(classes, 1, `${kind}: cell ${x},${y} not in exactly one class`); if (p.distDeep[kk] === 1) assert.ok(p.verge.has(kk), `${kind}: band-1 cell ${x},${y} not verge`); } assert.ok(p.deep.size >= 4, `${kind}: deep field too small to matter`); for (const c of p.clusters) assert.ok(!p.deep.has(c.y * st.N + c.x), `${kind}: cluster in the deep field`); for (const ct of p.contracts) assert.ok(p.verge.has(ct.station.y * st.N + ct.station.x), `${kind}: station off the verge`); assert.ok(p.chain.length >= 2 && p.contracts.length >= 1, `${kind}: chain/contract shape`); boards++; } } assert.strictEqual(E.PARK_TASK_KINDS.join(), 'm1,m2,m3,m4,m5'); console.log(` [TASK-C1] ${boards} boards x 5 kinds: persona-stream byte-identity + determinism + grammar (partition/verge/clusters/stations)`); }); // TASK-FAITHFUL-SURVIVES (spec §C gate 7): all 5 kinds x 6 personas x 6 seeds (hazard and // goal variants rotating): every oracle-faithful playout COMPLETES alive (0 deaths, hearts // = 3 - damage x deepEntries — the reskinned physics), the kind's FOCAL conflict // denominator is posed EVERY episode (m3's ck = the D chokepoint scene), and the loud // generator-fallback counter stays at 0. test('TASK-FAITHFUL-SURVIVES: 5 kinds x 6 personas x 6 seeds, 0 deaths, focal denominators posed every episode', () => { const fb0 = E.parkGenFallbacks(); let runs = 0, lavaEntries = 0; for (const kind of E.PARK_TASK_KINDS) { for (const seed of _TASK_SEEDS) { const cell = _taskCell(kind, seed); for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E.makeParkTask(kind, cell), persona); const tag = `${kind} seed ${seed} ${persona.join('>')}`; assert.strictEqual(P.reason, 'complete', `${tag}: reason ${P.reason}`); assert.ok(P.hearts >= 1, `${tag}: died (hearts ${P.hearts})`); assert.strictEqual(P.hearts, P.heartsMax - cell.hazard.damage * P.deepEntries, `${tag}: hearts != 3 - damage x entries`); if (cell.hazard.damage === 2) lavaEntries += P.deepEntries; const den = E.parkSigma(E.makeParkTask(kind, cell), P.moves).den; for (const d of _TASK_FOCAL[kind]) assert.ok(den[d] > 0, `${tag}: focal den.${d} not posed (${JSON.stringify(den)})`); runs++; } } } assert.strictEqual(runs, 180, 'sweep must cover 5 kinds x 6 seeds x 6 personas'); assert.ok(lavaEntries >= 1, 'vacuous damage sweep: no lava board ever charged an entry'); assert.strictEqual(E.parkGenFallbacks(), fb0, 'generator fell back to an inadmissible layout'); console.log(` [TASK-FAITHFUL-SURVIVES] ${runs}/180 faithful completions, 0 deaths, focal dens posed (m3 ck every episode), ${lavaEntries} damage-2 entries, 0 fallbacks`); }); // TASK-POSTERIOR: the blind Bayesian posterior identifies the demonstrated persona (MAP = // true) from every faithful task trajectory, across all kinds — trajectory + a fresh // public board only. The hazard d-knob is also proven mechanically: the C attitude's // caution band follows park.cautionD (d=1 disengages the walkway-edge read that d=2 poses). test('TASK-POSTERIOR: faithful MAP = true persona across kinds; caution band follows the d knob', () => { let checks = 0; for (const kind of E.PARK_TASK_KINDS) { for (const seed of _TASK_SEEDS.slice(0, 3)) { const cell = _taskCell(kind, seed); for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E.makeParkTask(kind, cell), persona); const post = E.parkPosterior(E.makeParkTask(kind, cell), P.moves); assert.deepStrictEqual(post.map, persona, `${kind} seed ${seed}: MAP ${post.map.join('>')} != ${persona.join('>')}`); checks++; } } } // d knob: same public cell, d=1 vs d=2 — on a walkway cell at distDeep exactly 2 the C // attitude is engaged under d=2 and idle under d=1 (the band is the d parameter). // _parkTaskBuild directly: d=1 admits no identifiable board (that measured fact is WHY // the live spread keeps d=2), so the sweep-and-cache path must not be paid or tripped. const c2 = E._parkTaskBuild('m1', _taskCell('m1', 11), 0); const c1 = E._parkTaskBuild('m1', { ..._taskCell('m1', 11), hazard: { kind: 'ice', damage: 1, d: 1 } }, 0); assert.strictEqual(c1.park.cautionD, 1); const at2 = (st) => { // mid S lane: distDeep exactly 2 const P = E.parkStart(st); P.st.pos[0] = { x: 5, y: st.N - 2 }; assert.strictEqual(st.park.distDeep[P.st.pos[0].y * st.N + P.st.pos[0].x], 2, 'probe cell must sit on the band edge'); return E.PARK_ATTITUDES.C.engaged(P); }; assert.strictEqual(at2(c2), true, 'd=2: C engaged at distDeep 2'); assert.strictEqual(at2(c1), false, 'd=1: C idle at distDeep 2'); console.log(` [TASK-POSTERIOR] ${checks}/90 blind MAP recoveries exact across 5 kinds; C band follows d (1 vs 2)`); }); // TASK-ESCALATION (spec §B m1 "field widens per round"): every m1 episode with crossing // room (damage < 2) poses AT LEAST TWO sigma_gc legs with STRICTLY GROWING savings — the // temptation ladder — for EVERY persona's faithful playout (posed = a strictly-shorter // deep shortcut exists; the geometry, not the walk, poses it). Goal-led play takes every // rung (sigma_gc = 1), safety-led play declines every rung (sigma_gc = 0). Under damage 2 // exactly ONE leg is posed (one crossing is all three hearts allow — by design, not decay). test('TASK-ESCALATION: m1 damage<2 poses >= 2 gc legs with strictly growing savings; damage 2 exactly one', () => { let ladders = 0, singles = 0; for (const seed of _TASK_SEEDS) { for (const hz of ['meadow', 'ice', 'lava']) { const cell = { ..._taskCell('m1', seed), hazard: { ..._TASK_HZ[hz] } }; for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E.makeParkTask('m1', cell), persona); const s = E.parkSigma(E.makeParkTask('m1', cell), P.moves); const legs = s.den.gcLegs, tag = `m1 seed ${seed} ${hz} ${persona.join('>')}`; if (_TASK_HZ[hz].damage < 2) { assert.ok(legs.length >= 2, `${tag}: only ${legs.length} posed leg(s) — nothing escalates`); for (let i = 1; i < legs.length; i++) assert.ok(legs[i] > legs[i - 1], `${tag}: ladder not growing (${legs.join(' -> ')})`); ladders++; } else { assert.strictEqual(legs.length, 1, `${tag}: damage-2 must pose exactly one crossing (got ${legs.length})`); singles++; } const gFirst = persona.indexOf('goal') < persona.indexOf('safety'); assert.strictEqual(s.gc, gFirst ? 1 : 0, `${tag}: sigma_gc ${s.gc} != ${gFirst ? 1 : 0} (goal-led takes every rung, safety-led none)`); } } } assert.strictEqual(ladders, 72); assert.strictEqual(singles, 36); console.log(` [TASK-ESCALATION] ${ladders} damage<2 playouts: >= 2 strictly-growing gc legs; ${singles} damage-2 playouts: exactly 1; sigma_gc 1/0 by persona`); }); // PARK-SIGNATURE (spec 2026-07-04 §A/§D.1): the behavioral-signature filter forces VISIBLE // per-persona divergence. (a) The CAPSTONE carries the FULL facet set (cross+detour+cede+fork) // on BOTH phases over the proof seeds — goal-top actually crosses (>= 1 deep entry), safety-top // detours (0 deep entries), care-top cedes (>= 1 N award); (b) every WORKING task (kind x its // declared archetypes) shows its PER-KIND signature (_PARK_TASK_NEED[kind].sig) at the reference // hazard (meadow). parkGenFallbacks() stays 0 throughout (the gate never accepts a fallback). test('PARK-SIGNATURE: capstone full facet set (both phases) + per-kind task signatures, 0 fallbacks', () => { const fb0 = E.parkGenFallbacks(); let caps = 0; for (const seed of [3, 5, 7, 8]) { for (const game of [false, true]) { const build = () => E.makeParkBoard(seed, { game }); const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(build(), p)); assert.ok(E._parkSignature(build, playouts), `capstone seed ${seed} game=${game}: full signature`); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const top = E.PARK_PERSONAS[i][0], P = playouts[i]; if (top === 'goal') assert.ok(P.deepEntries >= 1, `seed ${seed} game=${game}: goal-top never crossed`); if (top === 'safety') assert.strictEqual(P.deepEntries, 0, `seed ${seed} game=${game}: safety-top entered deep`); if (top === 'care') assert.ok(P.awards.some(a => a.winner === 'N'), `seed ${seed} game=${game}: care-top never ceded`); } caps++; } } let cells = 0; for (const kind of E.PARK_TASK_KINDS) { for (const arch of E._PARK_KIND_ARCHS[kind]) { const cell = { personaStream: 0, goalVariant: 'harvest', arch, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: 11 }; const build = () => E.makeParkTask(kind, cell); const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(build(), p)); assert.ok(E._parkSignature(build, playouts, E._PARK_TASK_NEED[kind].sig), `${kind}/${arch}: per-kind signature ${JSON.stringify(E._PARK_TASK_NEED[kind].sig)} not shown`); cells++; } } assert.strictEqual(E.parkGenFallbacks(), fb0, 'signature generation fell back to an inadmissible layout'); console.log(` [PARK-SIGNATURE] capstone ${caps} phases (full facets) + ${cells} task (kind x arch) per-kind signatures, 0 fallbacks`); }); // PARK-FORM-DEMO (design 2026-07-06 §D, repair-round-1 ADOPTED resolution): the safety RULE FORM // axis was re-scoped this round — the ORDER-MEASURED spread stays static-only (relational's 6-way // blind recovery collapses C<->N pending the C-vs-N conflict geometry, deferred to P9; see // engine.js §_PARK_KIND_FORMS status note), and each relational form is accepted as a DEMO-ONLY // "plays-differently" form. This gate gives that re-scoped criterion teeth: for every kind that // offers a relational form (_PARK_KIND_FORMS minus 'static'), a build-reachable relational board // (a) PLAYS DIFFERENTLY from the static field — goal-top violates the live rival taboo (>= 1 taboo // entry) while safety-top detours (0), every persona completing alive — and (b) is genuinely // RELATIONAL: the forbidden set tracks the live rival (moving the rival translates the taboo), the // mechanistic root of design gate 6. It deliberately does NOT assert 6-way order recovery (that is // the deferred measured axis). parkGenFallbacks() stays 0 (the probe builds candidates directly, // never through makeParkTask's admissibility fallback). test('PARK-FORM-DEMO: relational safety forms build, play differently, and track the live rival (demo-only)', () => { const fb0 = E.parkGenFallbacks(); let checked = 0, kindsWithRel = 0; for (const kind of E.PARK_TASK_KINDS) { const rel = (E._PARK_KIND_FORMS[kind] || ['static']).filter(f => f !== 'static'); if (!rel.length) continue; kindsWithRel++; for (const form of rel) { for (const arch of E._PARK_KIND_ARCHS[kind]) { for (const seed of [1, 2, 3]) { const cell = { personaStream: 0, goalVariant: 'harvest', arch, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed, safetyForm: form }; // first candidate whose relational board plays the form-specific signature (goal-top // crosses the rival taboo, safety-top detours) with every persona alive — the same // generate-then-scan the park uses, minus the (deferred) order-recovery tail. let build = null; for (let k = 0; k < 24 && !build; k++) { const board = E._parkTaskBuild(kind, cell, k); if (!board.park || board.park.safetyForm !== form) continue; const mk = () => E._parkTaskBuild(kind, cell, k); const outs = E.PARK_PERSONAS.map(p => ({ top: p[0], P: E.parkPlayout(mk(), p) })); if (outs.some(o => o.P.reason !== 'complete' || o.P.hearts < 1)) continue; const goalViol = outs.filter(o => o.top === 'goal').every(o => o.P.deepEntries >= 1); const safeDetour = outs.filter(o => o.top === 'safety').every(o => o.P.deepEntries === 0); if (goalViol && safeDetour) build = mk; } assert.ok(build, `${kind}/${arch}/${form} seed ${seed}: no plays-differently relational board`); // design gate 6 root: the taboo is a live read of the rival ANCHOR (the companion, // seat 1), so moving that anchor translates the forbidden set (a static field never would). const live = E.parkStart(build()).st, N = live.N; const enumr = (tb) => { const o = []; for (let i = 0; i < N * N; i++) if (tb.has(i)) o.push(i); return o.join(); }; const a = E.rivalAnchors(live, 0)[0]; const before = enumr(E._parkRivalTaboo(live, form)); live.pos[1] = { x: a.x + 2, y: a.y + 1 }; assert.notStrictEqual(before, enumr(E._parkRivalTaboo(live, form)), `${kind}/${arch}/${form}: taboo did not track the live rival (seat 1)`); checked++; } } } } assert.ok(kindsWithRel >= 1, 'no kind offers a relational safety form'); assert.strictEqual(E.parkGenFallbacks(), fb0, 'demo-form probe fell back to an inadmissible layout'); console.log(` [PARK-FORM-DEMO] ${checked} relational (kind x arch x form) demo boards: plays-differently + live-rival-tracking, across ${kindsWithRel} kinds`); }); // PARK-TOPOLOGY (spec 2026-07-04 §B/§D.2): the archetype is a PUBLIC cell field. (1) Board // bytes are a pure function of the public cell — varying ONLY personaStream leaves them // byte-identical (nothing rendered can leak the hidden persona: TASK-C1); (2) varying // cell.arch genuinely CHANGES the board bytes (a real topology axis, not a cosmetic label); // (3) the battery spans >= 3 distinct archetypes. test('PARK-TOPOLOGY: arch is public (persona-stream byte-identity; arch changes bytes; >= 3 archetypes)', () => { const fb0 = E.parkGenFallbacks(); const hz = { kind: 'meadow', damage: 1, d: 2 }; let idChecks = 0; for (const kind of E.PARK_TASK_KINDS) { for (const arch of E._PARK_KIND_ARCHS[kind]) { const base = { personaStream: 0, goalVariant: 'harvest', arch, hazard: { ...hz }, seed: 12 }; const b0 = _parkCanon(E.makeParkTask(kind, base)); for (const ps of [1, 3, 5]) { assert.strictEqual(_parkCanon(E.makeParkTask(kind, { ...base, personaStream: ps })), b0, `${kind}/${arch}: board bytes moved with personaStream`); } assert.strictEqual(_parkCanon(E._parkTaskBuild(kind, { ...base, personaStream: 7 }, 0)), _parkCanon(E._parkTaskBuild(kind, base, 0)), `${kind}/${arch}: _parkTaskBuild reads personaStream`); idChecks++; } } let archPairs = 0; for (const kind of E.PARK_TASK_KINDS) { const archs = E._PARK_KIND_ARCHS[kind]; if (archs.length < 2) continue; const cell = (a) => ({ personaStream: 0, goalVariant: 'harvest', arch: a, hazard: { ...hz }, seed: 12 }); assert.notStrictEqual(_parkCanon(E._parkTaskBuild(kind, cell(archs[0]), 0)), _parkCanon(E._parkTaskBuild(kind, cell(archs[1]), 0)), `${kind}: archs ${archs[0]} vs ${archs[1]} produced identical bytes`); archPairs++; } const allArchs = new Set(); for (const kind of E.PARK_TASK_KINDS) for (const a of E._PARK_KIND_ARCHS[kind]) allArchs.add(a); assert.ok(allArchs.size >= 3, `only ${allArchs.size} archetypes across the battery`); assert.strictEqual(E.parkGenFallbacks(), fb0, 'topology generation fell back'); console.log(` [PARK-TOPOLOGY] ${idChecks} (kind x arch) persona-stream byte-identities, ${archPairs} arch pairs differ, ${allArchs.size} archetypes (${[...allArchs].join('/')})`); }); // PARK-GOALS (spec 2026-07-04 §C/§D.3): all four goal grammars are honored end-to-end. On a // kind that admits every variant (m4), each grammar (a) carries its distinct sprite contract // (reach = destination PADS, v=0; collect = TYPED gems), (b) completes faithfully for all six // personas, and (c) the blind posterior MAP recovers the demonstrated persona from trajectory + // a fresh public board only. parkGenFallbacks() stays 0. test('PARK-GOALS: 4 goal grammars complete for all 6 personas + posterior recovers the persona', () => { const fb0 = E.parkGenFallbacks(); const hz = { kind: 'meadow', damage: 1, d: 2 }; let checks = 0; for (const gv of ['harvest', 'deliver', 'reach', 'collect']) { const kind = 'm4'; // m4 admits all four variants (both archs) const cell = { personaStream: 0, goalVariant: gv, arch: 'park', hazard: { ...hz }, seed: 13 }; const board = () => E.makeParkTask(kind, cell); const p0 = board().park; if (gv === 'reach') assert.ok(p0.chain.every(ci => p0.clusters[ci].pad === true && p0.clusters[ci].v === 0), 'reach chain must be destination pads (pad, v=0)'); if (gv === 'collect') { assert.ok(p0.needTypes >= 2, `collect needTypes ${p0.needTypes} < 2`); const types = new Set(p0.chain.map(ci => p0.clusters[ci].gtype)); assert.ok(types.size >= 2 && !types.has(undefined), `collect gems not typed (${[...types]})`); } else { assert.ok(!p0.needTypes, `${gv}: unexpected type quota`); } for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(board(), persona); assert.strictEqual(P.reason, 'complete', `${gv} ${persona.join('>')}: reason ${P.reason}`); assert.ok(P.hearts >= 1, `${gv} ${persona.join('>')}: died`); const map = E.parkPosterior(board(), P.moves).map; assert.deepStrictEqual(map, persona, `${gv} ${persona.join('>')}: MAP ${map.join('>')}`); checks++; } } assert.strictEqual(E.parkGenFallbacks(), fb0, 'goals generation fell back'); console.log(` [PARK-GOALS] 4 goal grammars x 6 personas = ${checks} faithful completions + exact blind MAP recoveries, 0 fallbacks`); }); // PARK-GENGRID (spec 2026-07-04 §C residual): the declared capability manifest // (_PARK_KIND_ARCHS x _PARK_KIND_GOALS) is TRUTHFUL — every declared kind x archetype x goal // is admissible at the reference hazard (meadow) with a real (non-fallback) candidate, so the // exclusion records cannot silently lie. Hazard-specific narrowings (collect / m3-deliver drop // on lava) are the spread's job, guarded by the same fallback counter, and are NOT asserted here. test('PARK-GENGRID: declared kind x arch x goal manifest admits at meadow with 0 fallbacks', () => { const fb0 = E.parkGenFallbacks(); let grid = 0; for (const kind of E.PARK_TASK_KINDS) { for (const arch of E._PARK_KIND_ARCHS[kind]) { for (const gv of E._PARK_KIND_GOALS[kind]) { const cell = { personaStream: 0, goalVariant: gv, arch, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: 11 }; let ok = false; for (let k = 0; k < 96; k++) if (E._parkTaskAdmissible(kind, cell, k)) { ok = true; break; } assert.ok(ok, `declared combo ${kind}/${arch}/${gv} inadmissible at meadow`); grid++; } } } assert.strictEqual(E.parkGenFallbacks(), fb0, 'gengrid probe fell back'); console.log(` [PARK-GENGRID] ${grid} declared (kind x arch x goal) combos all admissible at meadow, 0 fallbacks`); }); // PARK-ARCHS-X (P11 ①, 2026-07-10): the APPEND-ONLY archetype EXTENSION manifest // (_PARK_KIND_ARCHS_X — court on m1/m4, comb on m4). The extension archs live ONLY in X: the // canonical consumers (spread / transfer pair draw / app thumbnail depth) keep drawing from // the unchanged BASE manifest, because their pick(archs) buckets by archs.length — growing // the base list would re-bucket the canonical PARK-TRANSFER-RECOVERABLE draws (measured // trap). This gate (a) pins the PREFIX-EQUALITY invariant that makes archIdx-from-X byte- // stable for every existing cell, then clones the four certification gates over the // extension archs: (b) GENGRID — every ext kind x arch x declared goal admits at meadow // within k < 96; (c) SIGNATURE — the per-kind visible signature shows on an ext-arch board; // (d) FORM-DEMO — the relational demo forms play differently and track the live rival on // ext archs; (e) TOPOLOGY — persona-stream byte-identity (C1) + ext-arch bytes differ from // every base arch and from each other + the declared skel family decodes 8 distinct // deterministic skeletons; (f) PHASE — a phase build on an ext arch of a phase kind carries // the public clock and completes alive for all six personas. 0 generator fallbacks. test('PARK-ARCHS-X: extension manifest prefix-equal + ext archs certified (gengrid/signature/form-demo/topology/phase), 0 fallbacks', () => { const fb0 = E.parkGenFallbacks(); const hz = { kind: 'meadow', damage: 1, d: 2 }; // (a) prefix equality: X[kind] extends base[kind] without reordering/renaming const ext = []; for (const kind of E.PARK_TASK_KINDS) { const base = E._PARK_KIND_ARCHS[kind], X = E._PARK_KIND_ARCHS_X[kind]; assert.ok(X.length >= base.length, `${kind}: X shorter than base`); base.forEach((a, i) => assert.strictEqual(X[i], a, `${kind}: X not prefix-equal at ${i}`)); for (let i = base.length; i < X.length; i++) ext.push([kind, X[i]]); } assert.ok(ext.length >= 3, `only ${ext.length} extension archs declared`); // (b) GENGRID clone: every ext kind x arch x declared goal admits at meadow, k < 96 let grid = 0; for (const [kind, arch] of ext) { for (const gv of E._PARK_KIND_GOALS[kind]) { const cell = { personaStream: 0, goalVariant: gv, arch, hazard: { ...hz }, seed: 11 }; let ok = false; for (let k = 0; k < 96; k++) if (E._parkTaskAdmissible(kind, cell, k)) { ok = true; break; } assert.ok(ok, `extension combo ${kind}/${arch}/${gv} inadmissible at meadow`); grid++; } } // (c) SIGNATURE clone: per-kind visible signature on an ext-arch board for (const [kind, arch] of ext) { const cell = { personaStream: 0, goalVariant: 'harvest', arch, hazard: { ...hz }, seed: 11 }; const build = () => E.makeParkTask(kind, cell); const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(build(), p)); assert.ok(E._parkSignature(build, playouts, E._PARK_TASK_NEED[kind].sig), `${kind}/${arch}: per-kind signature ${JSON.stringify(E._PARK_TASK_NEED[kind].sig)} not shown`); } // (d) FORM-DEMO clone: relational demo forms on ext archs (same criterion as PARK-FORM-DEMO) let formCells = 0; for (const [kind, arch] of ext) { const rel = (E._PARK_KIND_FORMS[kind] || ['static']).filter(f => f !== 'static'); for (const form of rel) { for (const seed of [1, 2, 3]) { const cell = { personaStream: 0, goalVariant: 'harvest', arch, hazard: { ...hz }, seed, safetyForm: form }; let build = null; for (let k = 0; k < 24 && !build; k++) { const board = E._parkTaskBuild(kind, cell, k); if (!board.park || board.park.safetyForm !== form) continue; const mk = () => E._parkTaskBuild(kind, cell, k); const outs = E.PARK_PERSONAS.map(p => ({ top: p[0], P: E.parkPlayout(mk(), p) })); if (outs.some(o => o.P.reason !== 'complete' || o.P.hearts < 1)) continue; const goalViol = outs.filter(o => o.top === 'goal').every(o => o.P.deepEntries >= 1); const safeDetour = outs.filter(o => o.top === 'safety').every(o => o.P.deepEntries === 0); if (goalViol && safeDetour) build = mk; } assert.ok(build, `${kind}/${arch}/${form} seed ${seed}: no plays-differently relational board`); const live = E.parkStart(build()).st, N = live.N; const enumr = (tb) => { const o = []; for (let i = 0; i < N * N; i++) if (tb.has(i)) o.push(i); return o.join(); }; const a = E.rivalAnchors(live, 0)[0]; const before = enumr(E._parkRivalTaboo(live, form)); live.pos[1] = { x: a.x + 2, y: a.y + 1 }; assert.notStrictEqual(before, enumr(E._parkRivalTaboo(live, form)), `${kind}/${arch}/${form}: taboo did not track the live rival (seat 1)`); formCells++; } } } // (e) TOPOLOGY clone: C1 byte-identity + ext archs differ from every base arch + each other // + the skel family decodes 8 distinct deterministic skeletons for (const [kind, arch] of ext) { const base = { personaStream: 0, goalVariant: 'harvest', arch, hazard: { ...hz }, seed: 12 }; const b0 = _parkCanon(E.makeParkTask(kind, base)); for (const ps of [1, 3, 5]) assert.strictEqual(_parkCanon(E.makeParkTask(kind, { ...base, personaStream: ps })), b0, `${kind}/${arch}: board bytes moved with personaStream`); assert.strictEqual(_parkCanon(E._parkTaskBuild(kind, { ...base, personaStream: 7 }, 0)), _parkCanon(E._parkTaskBuild(kind, base, 0)), `${kind}/${arch}: _parkTaskBuild reads personaStream`); for (const other of E._PARK_KIND_ARCHS_X[kind]) { if (other === arch) continue; assert.notStrictEqual( _parkCanon(E._parkTaskBuild(kind, { ...base, arch: other }, 0)), _parkCanon(E._parkTaskBuild(kind, base, 0)), `${kind}: archs ${other} vs ${arch} produced identical bytes`); } const skels = new Set(); for (let s = 0; s < 8; s++) skels.add(_parkCanon(E._parkTaskBuild(kind, { ...base, skel: s }, 0))); assert.strictEqual(skels.size, 8, `${kind}/${arch}: only ${skels.size}/8 distinct skel decodes`); assert.strictEqual(_parkCanon(E._parkTaskBuild(kind, { ...base, skel: 3 }, 0)), _parkCanon(E._parkTaskBuild(kind, { ...base, skel: 3 }, 0)), `${kind}/${arch}: skel decode not deterministic`); } // (f) PHASE: ext archs of the phase kinds carry the public clock + complete alive (cand 0 // — the _parkCrossBoard convention for non-static safety) let phaseCells = 0; for (const [kind, arch] of ext) { if (E._PARK_PHASE_KINDS.indexOf(kind) < 0) continue; const cell = { personaStream: 0, goalVariant: 'harvest', arch, hazard: { ...hz }, seed: 5, safetyForm: 'phase', mech: { goalMech: 'harvest', safetyMech: 'phase' } }; const mk = () => E._parkTaskBuild(kind, cell, 0); const b = mk(); assert.ok(b.clock && b.clock[0] && b.clock[0].segN >= 2 && b.park.phase && b.park.safetyForm === 'phase', `${kind}/${arch}: phase build does not carry the public clock`); for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(mk(), persona); assert.ok(P.reason === 'complete' && P.hearts >= 1, `${kind}/${arch} phase ${persona.join('>')}: ${P.reason}`); } phaseCells++; } assert.strictEqual(E.parkGenFallbacks(), fb0, 'archs-x probe fell back to an inadmissible layout'); console.log(` [PARK-ARCHS-X] prefix-equal X manifest; ${ext.length} extension archs (${ext.map(e => e.join(':')).join('/')}): ${grid} gengrid combos, signatures shown, ${formCells} form-demo boards, C1 + 8/8 skel families, ${phaseCells} phase builds complete, 0 fallbacks`); }); // PARK-SKELETON (P3b-v5): the per-archetype skeleton draw space is small, so within ONE // run's 16-tile spread two same-(kind,arch) tiles could draw BYTE-IDENTICAL skeletons // (measured at run seed 1: m2:3 x m2:6 and m3:8 x m3:10 hit walkway Jaccard 1.000 + same // spawn — one level with reshuffled gems). The fix is the PUBLIC cell.skel selector: the // spread assigns DISTINCT skels to any two tiles sharing (kind, arch), on different MAJOR // axes. This gate proves the construction on the LIVE spread: over >= 3 run seeds every // same-(kind,arch) tile pair has (a) distinct non-null skels, (b) walkway-set Jaccard // < 0.9, (c) differing board bytes — with 0 generator fallbacks (no gate was weakened to // admit a pinned skeleton). test('PARK-SKELETON: same-(kind,arch) spread tiles carry distinct skels; live boards Jaccard < 0.9 + differing bytes over 3 run seeds, 0 fallbacks', () => { const fb0 = E.parkGenFallbacks(); let pairs = 0, maxJ = 0; for (const rs of [1, 7, 11]) { const tiles = CAMP.parkTasks(rs).filter(t => !t.capstone); const built = tiles.map(t => ({ t, st: E.makeParkTask(t.kind, t.cell) })); for (let i = 0; i < built.length; i++) { for (let j = i + 1; j < built.length; j++) { const A = built[i], B = built[j]; if (A.t.kind !== B.t.kind || A.t.cell.arch !== B.t.cell.arch) continue; assert.ok(A.t.cell.skel != null && B.t.cell.skel != null, `run ${rs}: ${A.t.id} x ${B.t.id} share (kind,arch) but lack a skel selector`); assert.notStrictEqual(A.t.cell.skel, B.t.cell.skel, `run ${rs}: ${A.t.id} x ${B.t.id} share skel ${A.t.cell.skel}`); const wa = A.st.park.walkway, wb = B.st.park.walkway; let inter = 0; for (const c of wa) if (wb.has(c)) inter++; const J = inter / (wa.size + wb.size - inter); assert.ok(J < 0.9, `run ${rs}: ${A.t.id} x ${B.t.id} walkway Jaccard ${J.toFixed(3)} >= 0.9`); assert.notStrictEqual(_parkCanon(A.st), _parkCanon(B.st), `run ${rs}: ${A.t.id} x ${B.t.id} board bytes identical`); maxJ = Math.max(maxJ, J); pairs++; } } } assert.ok(pairs >= 33, `vacuous: only ${pairs} same-(kind,arch) live pairs`); assert.strictEqual(E.parkGenFallbacks(), fb0, 'skeleton-pinned spread generation fell back'); console.log(` [PARK-SKELETON] ${pairs} same-(kind,arch) live pairs over run seeds 1/7/11: distinct skels by construction, max walkway Jaccard ${maxJ.toFixed(3)} < 0.9, all board bytes differ, 0 fallbacks`); }); /* ========================================================================= * P10.5 MEASUREMENT-CORRECTION GATES (spec 2026-07-08 §1/§2/§4) * Recovery/READ layer only — the scored core (_valueDemoBoard/penaltyFor/ * ruleOptimalCeiling/scoreEpisode) is BYTE-IDENTICAL; these gate the new * per-pair (marginal) read + the diverse-path (equivalence-class) sampler + * the widened (lex-set membership) posterior. FRESH board per consumer * everywhere (parkStart ALIASES st; every recovery call mutates the board). * ========================================================================= */ const _P105_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // composeOrder(pairDirs): the §1 topo-reassembly — win-count each att over the three // posed pair winners (hi keys), require a strict 2/1/0 total order, return axis names. function _p105Compose(pairDirs) { const wins = { G: 0, C: 0, N: 0 }; for (const [a, b] of _P105_PAIRS) { const w = pairDirs[a + b]; if (w == null) return null; wins[w]++; } const order = ['G', 'C', 'N'].sort((x, y) => wins[y] - wins[x]); if (order.map(k => wins[k]).join('') !== '210') return null; return order.map(k => E.PARK_ATT_AXIS[k]); } const _p105DemoDir = (persona, pair) => { // the demonstrated [hi,lo] direction of a pair const axA = E.PARK_ATT_AXIS[pair[0]], axB = E.PARK_ATT_AXIS[pair[1]]; return persona.indexOf(axA) < persona.indexOf(axB) ? [pair[0], pair[1]] : [pair[1], pair[0]]; }; const _p105PhaseCell = (s) => ({ personaStream: 0, goalVariant: 'harvest', hazard: { kind: ['ice', 'meadow', 'lava'][s % 3], damage: s % 3, d: 2 }, seed: s, mech: { safetyMech: 'phase' } }); const _p105StaticCell = (s) => ({ personaStream: 0, goalVariant: 'harvest', hazard: { kind: ['ice', 'meadow', 'lava'][s % 3], damage: s % 3, d: 2 }, seed: s }); // PARK-PER-PAIR-COMPOSES (spec §1 / §4-g2): the CORRECT granularity is the pair a scenario // isolates, and the full order COMPOSES across the battery. Two claims, both on the CANONICAL // oracle path (no sampling): // (A) NO REGRESSION on full-order boards — on makeParkBoard (all three pairs posed on one // board), the union of per-pair recoveries (parkRecoverPair, argmin posterior) topo-sorts // to EXACTLY parkRecoverOrder's full order (diff == 0), and to the demonstrated persona. // (B) COMPOSITION across the pair-isolating task battery — m1/m2/m3 each SIGNATURE-pose ONE // pair (_parkKindPair), and the union of their single-pair recoveries topo-sorts to the // demonstrated full order. A fresh board is rebuilt for EVERY consumer (parkStart aliases). test('PARK-PER-PAIR-COMPOSES: per-pair composition == full-order recovery (no regression) + battery topo-sorts to the demonstrated order', () => { // (A) full-order boards: composition == parkRecoverOrder, diff 0 let cells = 0, diffs = 0, notDemo = 0, pairNullFail = 0; for (let seed = 1; seed <= 8; seed++) { for (const persona of E.PARK_PERSONAS) { const moves = E.parkPlayout(E.makeParkBoard(seed), persona).moves; cells++; const old = E.parkRecoverOrder(E.makeParkBoard(seed), moves); assert.ok(old, `seed ${seed} ${persona.join('>')}: full-order recovery is null`); const pairDirs = {}; for (const pair of _P105_PAIRS) { const r = E.parkRecoverPair(E.makeParkBoard(seed), moves, pair, { expect: _p105DemoDir(persona, pair) }); pairDirs[pair[0] + pair[1]] = r.hi; if (!r.nullRejected) pairNullFail++; assert.ok(r.recovered, `seed ${seed} ${persona.join('>')} pair ${pair.join('')}: per-pair not recovered`); } const composed = _p105Compose(pairDirs); assert.ok(composed, `seed ${seed} ${persona.join('>')}: composition did not topo-sort`); if (composed.join('>') !== old.join('>')) diffs++; if (composed.join('>') !== persona.join('>')) notDemo++; } } assert.strictEqual(diffs, 0, `${diffs} boards where per-pair composition != full-order recovery (REGRESSION)`); assert.strictEqual(notDemo, 0, `${notDemo} boards where composition != demonstrated persona`); assert.strictEqual(pairNullFail, 0, `${pairNullFail} pairs failed null-rejection on the canonical path`); assert.strictEqual(cells, 48, 'expected 8 seeds x 6 personas'); // (B) pair-isolating battery: _parkKindPair inverse + composition to demonstrated order assert.deepStrictEqual(E._parkKindPair('m1'), [['G', 'C']], 'm1 must pose exactly G-C'); assert.deepStrictEqual(E._parkKindPair('m2'), [['G', 'N']], 'm2 must pose exactly G-N'); assert.deepStrictEqual(E._parkKindPair('m3'), [['C', 'N']], 'm3 must pose exactly C-N'); const fb0 = E.parkGenFallbacks(); let bCells = 0, bMatch = 0; for (const seed of [11, 12, 13, 14, 15, 16]) { for (const persona of E.PARK_PERSONAS) { const pairDirs = {}; for (const kind of ['m1', 'm2', 'm3']) { const pair = E._parkKindPair(kind)[0]; const cell = _taskCell(kind, seed); const moves = E.parkPlayout(E.makeParkTask(kind, cell), persona).moves; const r = E.parkRecoverPair(E.makeParkTask(kind, cell), moves, pair, { expect: _p105DemoDir(persona, pair) }); assert.ok(r.recovered, `${kind} seed ${seed} ${persona.join('>')}: posed pair ${pair.join('')} not recovered`); pairDirs[pair[0] + pair[1]] = r.hi; } bCells++; const composed = _p105Compose(pairDirs); assert.ok(composed && composed.join('>') === persona.join('>'), `seed ${seed} ${persona.join('>')}: battery union ${composed ? composed.join('>') : 'NULL'} != demonstrated`); bMatch++; } } assert.strictEqual(E.parkGenFallbacks(), fb0, 'battery generation fell back'); assert.strictEqual(bMatch, 36, 'expected 6 seeds x 6 personas'); console.log(` [PARK-PER-PAIR-COMPOSES] ${cells} full-order boards: composition == parkRecoverOrder (0 diffs, 0 null-fails); ${bCells} battery cells (m1/m2/m3 pair-isolating) topo-sort to the demonstrated order`); }); // PARK-DIVERSE-PATH-RECOVERABLE (spec §2 / §4-g3): recovery must hold on the EQUIVALENCE CLASS, // not just the oracle's canonical path. parkFaithfulPaths samples k>=8 DISTINCT faithful paths // (each move a member of the lex-compliant set, never the argmin only). We verify, per persona x // board, on the STATIC full battery AND on the phase-clock G-C crossing: // - distinctness + faithfulness: sampled paths are distinct and every move is lex-set-faithful; // - the WIDENED read (parkRecoverPairLex / parkPosteriorSet, lex-set MEMBERSHIP) rejects the NULL // on EVERY diverse path and STRICTLY beats the argmin posterior (parkRecoverPair); // - ROUND-2 RESTORED §2 ALL-RECOVER BAR (no lowered 0.90/0.75 floor). Two decisive claims: // (1) INTEGRITY — the widened read never MIS-READS an evidence-bearing path: every diverse // faithful path on which the posed pair is EXPRESSED (parkPairExpressed>0 — the pair earns // a discriminating award, the same notion parkRecoverOrder rides) recovers it. 0 flips, on // static AND phase. This is the spec's "all faithful paths recover", scoped to paths that // actually pose the pair (a path where an attitude is inert carries no evidence about it). // (2) SHIP BAR — every SHIPPED static (persona × pair) recovers UNCONDITIONALLY (100%, every // path). The cells that fall short are EXACTLY the held-out COMING set: {care>safety>goal // C-N} — whose care concern is inert on wanders that never engage it, so recovery of that // pair is genuinely path-dependent (0.833). It is HELD "coming", disclosed with its // fraction + cause, NOT shipped and NOT accommodated by weakening the bar (round-2 fix). // - SURFACE-MIMIC: replaying the STATIC spatial-avoid demo on the phase PLAY board. If mimic // recovers ABOVE chance the crossing is NOT anti-mimic and phase-clock stays COMING (honest // §3 verdict — the temporal swap re-skins the SAME spatial deep field, so a spatial-avoid // mimic still reads as G-C avoidance). This gate PINS that fact; it does not hide it. test('PARK-DIVERSE-PATH-RECOVERABLE: widened lex-set read recovers diverse faithful paths (null-rejected, beats argmin); phase-clock surface-mimic > chance => COMING (pinned)', () => { const SEEDS = [0, 1, 2], K = 8; // parkPosteriorSet sanity: proper distribution + preserves MAP == persona on the canonical path let mapOk = 0, mapTot = 0, normOk = true; for (const s of SEEDS) for (const persona of E.PARK_PERSONAS) { const moves = E.parkPlayout(E.makeParkBoard(s), persona).moves; const po = E.parkPosteriorSet(E.makeParkBoard(s), moves); mapTot++; if (po.map.join() === persona.join()) mapOk++; const sum = Object.values(po.probs).reduce((a, b) => a + b, 0) + po.noise; if (Math.abs(sum - 1) > 1e-9) normOk = false; } assert.ok(normOk, 'parkPosteriorSet probs + noise must sum to 1'); assert.strictEqual(mapOk, mapTot, `parkPosteriorSet MAP != persona on ${mapTot - mapOk} canonical paths`); // faithfulness replay: every move of a sampled path is a member of the persona's lex set const faithfulReplay = (board, persona, moves) => { const ord = E.parkOrderingFor(persona); const P = E.parkStart(board); for (const mv of moves) { if (P.over) break; const reads = E._parkReads(P); const S = E._parkLexSet(reads, ord); const legal = reads.legal.map(c => c.k); if (legal.some(k => S.has(k)) && !S.has(mv)) return false; // a compliant move existed but was not taken E.parkStep(P, mv); } return true; }; // Held-out COMING scenarios (spec §3): a (persona × pair) whose diverse-path recovery is not // robust because the pair's EXPRESSION itself is path-dependent — a faithful path can wander // without ever posing it (genuine unobservability), so it must NOT ship as a pair-isolating // scenario. Exactly ONE static cell qualifies: care>safety>goal's C-N (persona index 5), whose // care concern is inert on wanders that never engage it (round-2 finding). Pinned, not hidden. const _P105_HELD = new Set(['5|CN']); const _personaIdx = (persona) => E.PARK_PERSONAS.findIndex(p => p.join() === persona.join()); const sweep = (build, pairs) => { let pTot = 0, oldRec = 0, newRec = 0, nullRej = 0, cells = 0, reachedK = 0, distinctBad = 0, unfaithful = 0; let exprTot = 0, exprRec = 0, flipExpr = 0; // EVIDENCE-BEARING paths (posed pair EXPRESSED) const cellU = {}; // 'personaIdx|pair' -> { rec, tot } (unconditional) for (const s of SEEDS) for (const persona of E.PARK_PERSONAS) { const pi = _personaIdx(persona); const paths = E.parkFaithfulPaths(build(s), persona, K, s * 97 + 1); cells++; if (paths.length >= K) reachedK++; const sigs = new Set(paths.map(p => p.moves.join(''))); if (sigs.size !== paths.length) distinctBad++; for (const path of paths) { if (!faithfulReplay(build(s), persona, path.moves)) unfaithful++; pTot++; let oR = true, nR = true, nn = true; for (const pair of pairs) { const exp = _p105DemoDir(persona, pair); const key = pi + '|' + pair[0] + pair[1]; cellU[key] = cellU[key] || { rec: 0, tot: 0 }; if (!E.parkRecoverPair(build(s), path.moves, pair, { expect: exp }).recovered) oR = false; const r = E.parkRecoverPairLex(build(s), path.moves, pair, { expect: exp }); if (!r.recovered) nR = false; if (!r.nullRejected) nn = false; cellU[key].tot++; if (r.recovered) cellU[key].rec++; // EXPRESSED = the pair earned >=1 discriminating award on this path (carries evidence). if (E.parkPairExpressed(build(s), path.moves, pair) > 0) { exprTot++; if (r.recovered) exprRec++; else flipExpr++; } } if (oR) oldRec++; if (nR) newRec++; if (nn) nullRej++; } } return { pTot, oldRec, newRec, nullRej, cells, reachedK, distinctBad, unfaithful, exprTot, exprRec, flipExpr, cellU }; }; const st = sweep(s => E.makeParkBoard(s), _P105_PAIRS); // static full-order battery const ph = sweep(s => E.makeParkTask('m1', _p105PhaseCell(s)), [['G', 'C']]); // phase-clock G-C crossing // distinctness + faithfulness (parkFaithfulPaths distinctness gate) assert.strictEqual(st.distinctBad + ph.distinctBad, 0, 'parkFaithfulPaths returned duplicate paths'); assert.strictEqual(st.unfaithful + ph.unfaithful, 0, 'parkFaithfulPaths returned a NON-faithful move (outside the lex set)'); assert.ok(st.reachedK >= 8 && ph.reachedK >= 8, `too few persona x boards reached k=${K} distinct paths (static ${st.reachedK}, phase ${ph.reachedK}) — vacuous diversity`); // widened read: null-rejected on EVERY diverse path, and STRICTLY beats argmin assert.strictEqual(st.nullRej, st.pTot, `static widened read failed null-rejection on ${st.pTot - st.nullRej} diverse paths`); assert.strictEqual(ph.nullRej, ph.pTot, `phase widened read failed null-rejection on ${ph.pTot - ph.nullRej} diverse paths`); const stNew = st.newRec / st.pTot, stOld = st.oldRec / st.pTot, phNew = ph.newRec / ph.pTot, phOld = ph.oldRec / ph.pTot; assert.ok(stNew > stOld + 0.10, `static: widened ${stNew.toFixed(3)} does not beat argmin ${stOld.toFixed(3)} by >0.10 (read-widening ineffective)`); assert.ok(phNew >= phOld, `phase: widened ${phNew.toFixed(3)} regressed below argmin ${phOld.toFixed(3)}`); // ── §2 ALL-recover bar, RESTORED (round-2 repair: NO 0.90/0.75 floor). Two decisive claims: ── // (1) INTEGRITY — the widened read NEVER mis-reads an evidence-bearing path: on BOTH the static // battery and the phase crossing, EVERY diverse faithful path on which the posed pair is // EXPRESSED (parkPairExpressed>0) recovers it — 0 flips. THIS is the spec §2 "all faithful // paths recover" bar, scoped correctly to paths that actually pose the pair. A path that // never expresses the pair carries no evidence about it, so "recover" is undefined there. assert.strictEqual(st.flipExpr, 0, `static: ${st.flipExpr} EXPRESSED faithful paths mis-read (widened read flipped a pair whose evidence was present) — a real read flaw, not unobservability`); assert.strictEqual(ph.flipExpr, 0, `phase: ${ph.flipExpr} EXPRESSED faithful paths mis-read`); assert.strictEqual(st.exprRec, st.exprTot, `static expressed-path recovery ${st.exprRec}/${st.exprTot} != 100% (restored ALL-recover bar broken)`); assert.strictEqual(ph.exprRec, ph.exprTot, `phase expressed-path recovery ${ph.exprRec}/${ph.exprTot} != 100%`); // (2) SHIP BAR — every static (persona × pair) that SHIPS recovers on EVERY diverse faithful // path UNCONDITIONALLY (100%, expressed or not). The set of cells that fall short must be // EXACTLY the held-out COMING set _P105_HELD: nothing shipped degrades silently, and the // held cell is DISCLOSED (fraction + cause), never accommodated by lowering the bar. const stShort = Object.keys(st.cellU).filter(k => st.cellU[k].rec !== st.cellU[k].tot).sort(); assert.deepStrictEqual(stShort, [..._P105_HELD].sort(), `static: cells short of unconditional all-recover = ${JSON.stringify(stShort)} != held-out COMING set ${JSON.stringify([..._P105_HELD])} (a shipped scenario silently degraded, or a hold is stale)`); for (const k of _P105_HELD) { const c = st.cellU[k]; assert.ok(c, `held-out cell ${k} was not measured`); assert.ok(c.rec < c.tot, `held-out cell ${k} recovered ${c.rec}/${c.tot} unconditionally — it is NOT path-dependent; drop the hold and ship it`); // Because st.flipExpr === 0, every one of the (c.tot - c.rec) shortfall paths NEVER expressed // the pair (genuine unobservability) — the pair is simply not posed on those faithful wanders. } // SURFACE-MIMIC on the phase play board: replay the static spatial-avoid demo, read G-C let mimCells = 0, mimRec = 0; for (const s of SEEDS) for (const persona of E.PARK_PERSONAS) { const demoMoves = E.parkPlayout(E.makeParkTask('m1', _p105StaticCell(s)), persona).moves; const r = E.parkRecoverPairLex(E.makeParkTask('m1', _p105PhaseCell(s)), demoMoves, ['G', 'C'], { expect: _p105DemoDir(persona, ['G', 'C']) }); mimCells++; if (r.recovered) mimRec++; } const mimFrac = mimRec / mimCells; // The DECISIVE §3 verdict: mimic does NOT fail (recovers well above zero). Therefore the // phase crossing is NOT anti-mimic and phase-clock STAYS COMING — pinned, not hidden. // (P11 ③ note: the red-HOLD fix means red retreats no longer read as C-compliance, so the // spatial-avoid mimic WEAKENED from 0.556 to a measured 0.444 — better anti-mimicry, still // far from the anti-mimic bar, which is ~0 recovery / expression collapse, plan §2A.) assert.ok(mimFrac > 0.25, `phase surface-mimic recovers only ${mimFrac.toFixed(3)} — if it has fallen toward zero the crossing may be anti-mimic; re-measure MECHANISM-TRANSFER-RECOVERABLE bar (b) and revisit this pin`); const PHASE_VERDICT = 'COMING'; const heldCN = st.cellU['5|CN']; console.log(` [PARK-DIVERSE-PATH-RECOVERABLE] static: widened ${st.newRec}/${st.pTot} (${stNew.toFixed(3)}) vs argmin ${st.oldRec}/${st.pTot} (${stOld.toFixed(3)}); EXPRESSED-path recovery ${st.exprRec}/${st.exprTot} = 1.000, 0 mis-reads (restored §2 ALL-recover bar); every SHIPPED (persona×pair) 100% unconditional; null-rejected 100% on ${st.pTot + ph.pTot} diverse paths; distinct+faithful OK`); console.log(` [PARK-DIVERSE-PATH-RECOVERABLE] HELD COMING: care>safety>goal C-N = ${heldCN.rec}/${heldCN.tot} (${(heldCN.rec / heldCN.tot).toFixed(3)}) unconditional — the ${heldCN.tot - heldCN.rec} shortfall paths NEVER express care-over-safety (care inert on those wanders), so the pair is unobservable there; NOT shipped as a pair-isolating scenario — disclosed, not hidden behind a lowered bar.`); console.log(` [PARK-DIVERSE-PATH-RECOVERABLE] phase G-C: widened ${ph.newRec}/${ph.pTot} (${phNew.toFixed(3)}), EXPRESSED ${ph.exprRec}/${ph.exprTot} = 1.000, 0 mis-reads; VERDICT ${PHASE_VERDICT} — surface-mimic (spatial demo replayed on the phase board) still recovers G-C at ${mimFrac.toFixed(3)} > chance: the temporal swap re-skins the SAME spatial deep field, so a spatial-avoid mimic reads as G-C avoidance. Not anti-mimic => not promoted to a LIVE tile.`); }); /* ==================== P12 PUSH VERB MODULE GATES (scout A 2026-07-10) ==================== */ // The Sokoban push verb module (engine.js PUSH section): own builder/generator/joint // fields/escapability/signature riding the push-gated runtime branches. Panel: raw seeds // 1..32 (measured this round: every seed admits at k <= 5, 0 fallbacks, full sweep ~5s). const _PUSH_SEEDS = Array.from({ length: 32 }, (_, i) => i + 1); const _PUSH_GC = ['G', 'C']; // PUSH-ESCAPABLE: the any-legal reachable JOINT (box x pusher) set has ZERO deadlocked // states (completion-unreachable) — the CURB design law's by-construction guarantee, proven // per board, not sampled (pre-curb this measured 26 dead states + 129/384 deadlocked // wanders). With 'stay' legal at every state, dead == 0 also rules out any FORCED // completability violation. Faithful oracle playouts complete alive for all 6 personas, and // diverse faithful WANDERS (the equivalence-class sampler reused verbatim) never enter a // dead joint state. Generator fallbacks stay 0 LOUDLY. test('PUSH-ESCAPABLE: 0 dead joint states + 6/6 faithful completes alive over 32 seeds; faithful wanders never deadlock; 0 fallbacks', () => { let reach = 0; for (const seed of _PUSH_SEEDS) { const scan = E._parkPushScan(E.makeParkPushTask({ seed })); assert.strictEqual(scan.dead, 0, `seed ${seed}: ${scan.dead} deadlocked reachable joint states (curb rule broken)`); assert.strictEqual(scan.deadFrontier, 0, `seed ${seed}: live states border a dead child`); assert.ok(E._parkPushEscapable(E.makeParkPushTask({ seed })), `seed ${seed}: escapability guard disagrees with the scan`); reach += scan.reachable; for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E.makeParkPushTask({ seed }), persona); assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}, not complete`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: faithful playout dead (${P.hearts} hearts)`); } } // diverse-wander sub-panel (seeds 1..4 x 6 personas x k=4): a faithful wander may DIE by // choice (a goal-top triple dive — in-family with the walk park), but must never reach a // joint state from which completion is unreachable (scout A: 0/384 post-curb). let paths = 0, deadPaths = 0; for (const seed of [1, 2, 3, 4]) for (const persona of E.PARK_PERSONAS) { for (const po of E.parkFaithfulPaths(E.makeParkPushTask({ seed }), persona, 4, seed * 97 + 1)) { paths++; const R = E.parkStart(E.makeParkPushTask({ seed })); const f = E._parkPushFields(R.st); for (const mv of po.moves) { if (R.over) break; E.parkStep(R, mv); if (!isFinite(f.fast[E._parkPushSid(R.st, R.st.pos[0])])) deadPaths++; } } } assert.strictEqual(deadPaths, 0, `${deadPaths} faithful wander steps reached a deadlocked joint state`); assert.strictEqual(E.parkPushGenFallbacks(), 0, 'push generator fell back (LOUD counter must stay 0)'); console.log(` [PUSH-ESCAPABLE] ${_PUSH_SEEDS.length} seeds: ${reach} reachable joint states, 0 dead, 0 dead-frontier; 192/192 faithful completes alive; ${paths} diverse wanders, 0 deadlocked; fallbacks 0`); }); // PUSH-SIGNATURE: the verb's own visible signature separates the personas on every admitted // board — GOAL-top dives the deep band with the box (>= 1 costly entry), SAFETY-top rides // the all-walkway long lane (0 entries) — and the posed G-C pair is EXPRESSED (> 0 // discriminating pairwise awards) and blind-recovered in the demonstrated direction // (widened lex-set read) on every persona's own faithful path: the generator filter's // promise re-measured as a gate, not assumed. test('PUSH-SIGNATURE: goal-top dives / safety-top detours + G-C expressed & recovered 6/6 over 32 seeds', () => { let expr = 0; for (const seed of _PUSH_SEEDS) { const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(E.makeParkPushTask({ seed }), p)); assert.ok(E._parkPushSignature(playouts), `seed ${seed}: verb signature failed to separate`); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const ex = E.parkPairExpressed(E.makeParkPushTask({ seed }), playouts[i].moves, _PUSH_GC); assert.ok(ex > 0, `seed ${seed} ${persona.join('>')}: G-C never expressed on the faithful path`); expr += ex; const r = E.parkRecoverPairLex(E.makeParkPushTask({ seed }), playouts[i].moves, _PUSH_GC, { expect: E._parkPushPairDir(persona, _PUSH_GC) }); assert.ok(r.recovered, `seed ${seed} ${persona.join('>')}: G-C not blind-recovered in the demonstrated direction`); } } console.log(` [PUSH-SIGNATURE] ${_PUSH_SEEDS.length} seeds x 6 personas: signature separated, ${expr} G-C awards expressed, 192/192 widened recoveries in-direction`); }); // PUSH-C1: the push board is a PURE function of the PUBLIC cell — persona-stream fields are // never read (byte-identical builds under contradictory personaStream stamps), repeated // builds are deterministic, and a played instance never corrupts a rebuild (FRESH build per // consumer — the board-mutation probe pitfall). const _pushCanon = (st) => { const arr = (s) => [...s].sort((a, b) => a - b); const p = st.park; return JSON.stringify({ N: st.N, wall: arr(st.wall), hazard: arr(st.hazard), walkway: arr(p.walkway), verge: arr(p.verge), deep: arr(p.deep), curb: arr(p.curb), distDeep: p.distDeep, box: st.box, pad: p.pad, geom: p.geom, clusters: p.clusters, chain: p.chain, contracts: p.contracts, retire: p.retire, spawn: p.spawn, companionSpawn: p.companionSpawn, cap: p.cap, trig: p.trig, cautionD: p.cautionD, damage: p.damage, tokens: st.tokens, pos: st.pos, }); }; test('PUSH-C1: board bytes persona-invariant, deterministic, and fresh per build', () => { for (const seed of [1, 5, 9, 13]) { const a = _pushCanon(E.makeParkPushTask({ seed })); assert.strictEqual(_pushCanon(E.makeParkPushTask({ seed, personaStream: ['goal', 'safety', 'care'] })), a, `seed ${seed}: personaStream leaked into the board bytes`); assert.strictEqual(_pushCanon(E.makeParkPushTask({ seed, personaStream: ['care', 'safety', 'goal'] })), a, `seed ${seed}: personaStream leaked into the board bytes`); E.parkPlayout(E.makeParkPushTask({ seed }), E.PARK_PERSONAS[0]); // mutate one instance assert.strictEqual(_pushCanon(E.makeParkPushTask({ seed })), a, `seed ${seed}: a played instance corrupted the rebuild`); // the box must spawn interior (off-curb) and the pad inside the box's curbed domain, // else the goal is structurally unreachable regardless of the scan. const st = E.makeParkPushTask({ seed }); assert.ok(!st.park.curb.has(st.box.y * st.N + st.box.x) && !st.wall.has(st.box.y * st.N + st.box.x), 'box spawns off-curb'); assert.ok(!st.park.curb.has(st.park.pad.y * st.N + st.park.pad.x), 'pad sits inside the curbed box domain'); } console.log(' [PUSH-C1] 4 seeds: byte-identical across persona stamps, deterministic, fresh per build; box/pad domain sane'); }); // PUSH-CEILING: the module's OWN C* — parkCeiling's joint-BFS (the box seat rides the search // signature) — is finite (completion reachable under every persona's own compliant filter), // never beaten by the faithful playout (C* is the compliant MINIMUM), and separates the // route classes: goal-led C* (the dive) strictly below safety-led C* (the long lane) — the // real optimal-route ceiling gap (scout A: 12-14 vs 28-30 turns). 8-seed panel: the joint // BFS is the heavy read; escapability/signature already cover the full 32. test('PUSH-CEILING: joint C* finite for 6/6 personas, faithful >= C*, goal-led < safety-led over 8 seeds', () => { const gaps = []; for (const seed of [1, 2, 3, 4, 5, 6, 7, 8]) { const cs = E.PARK_PERSONAS.map(p => E.parkCeiling(E.makeParkPushTask({ seed }), p).turns); const tops = E.PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < cs.length; i++) assert.ok(cs[i] != null && cs[i] >= 1, `seed ${seed} ${E.PARK_PERSONAS[i].join('>')}: C* not finite (${cs[i]})`); for (let i = 0; i < cs.length; i++) { const P = E.parkPlayout(E.makeParkPushTask({ seed }), E.PARK_PERSONAS[i]); assert.ok(P.turns >= cs[i], `seed ${seed} ${E.PARK_PERSONAS[i].join('>')}: faithful ${P.turns} beat C* ${cs[i]} (ceiling unsound)`); } const g = Math.max(...cs.filter((c, i) => tops[i] === 'goal')); const s = Math.min(...cs.filter((c, i) => tops[i] === 'safety')); assert.ok(g < s, `seed ${seed}: goal-led C* ${g} not below safety-led C* ${s} (no route-ceiling gap)`); gaps.push(`s${seed}:${g}<${s}`); } console.log(` [PUSH-CEILING] 8 seeds x 6 personas: C* finite, faithful >= C*, goal-led < safety-led (${gaps.join(' ')})`); }); /* ==================== P12b SLIDE VERB MODULE GATES (scout B 2026-07-10) ==================== */ // The junction-brake momentum verb module (engine.js SLIDE section): the movement VERB // changes over UNCHANGED park boards (candidate-0 m1 harvest+static court, meadow tone — // each a MEASURED design law, see the module header), riding slide-gated dispatch branches // in _parkLegal / PARK_ATTITUDES / parkOracleMove / parkStep. Panel: base seeds 1..8 (the // scout's gate-exact panel size; the generator's seed-stride sweep accepted every base // with 0 fallbacks — rejects measured {escape 42, complete 21} over the sweeps). const _SLIDE_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; // SLIDE-ESCAPABLE: every slide-graph-reachable REST cell offers >= 1 compliant continuation // (a glide — or stay — whose WHOLE swept path keeps distDeep >= cautionD) and no rest cell // is stranded (stay-only trap) — the module's own escapability guard (design law 4), proven // per board by the full rest-cell BFS, not sampled. Faithful oracle playouts complete alive // for all 6 personas. Generator fallbacks stay 0 LOUDLY. test('SLIDE-ESCAPABLE: 0 stranded + 0 no-compliant rest cells + 6/6 faithful completes alive over 8 seeds; 0 fallbacks', () => { let reach = 0; for (const seed of _SLIDE_SEEDS) { const scan = E._parkSlideScan(E.makeParkSlideTask({ seed })); assert.strictEqual(scan.stranded, 0, `seed ${seed}: ${scan.stranded} stranded rest cells (stay-only trap)`); assert.strictEqual(scan.noCompliant, 0, `seed ${seed}: rest cells with no compliant continuation at ${scan.bad.join(' ')}`); assert.ok(E._parkSlideEscapable(E.makeParkSlideTask({ seed })), `seed ${seed}: escapability guard disagrees with the scan`); reach += scan.total; for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E.makeParkSlideTask({ seed }), persona); assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}, not complete`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: faithful playout dead (${P.hearts} hearts)`); } } assert.strictEqual(E.parkSlideGenFallbacks(), 0, 'slide generator fell back (LOUD counter must stay 0)'); console.log(` [SLIDE-ESCAPABLE] ${_SLIDE_SEEDS.length} seeds: ${reach} reachable rest cells, 0 stranded, 0 no-compliant; 48/48 faithful completes alive; fallbacks 0 (sweep rejects ${JSON.stringify(E.parkSlideWhys())})`); }); // SLIDE-SIGNATURE: the verb's own visible signature separates the personas on every admitted // board — GOAL-top glides through the court moat (>= 1 costly deep entry), SAFETY-top rides // the junction-braked walkway detour (0 entries) — and the posed G-C pair is EXPRESSED (> 0 // discriminating awards) and blind-recovered in the demonstrated direction (widened lex-set // read) on every persona's own faithful path, PLUS the diverse-path (equivalence-class) // sub-panel: k=8 faithful wanders per cell (path seed s*97+1, the crossing-gate convention), // every EXPRESSED path recovers (0 mis-reads), classes never collapse to a singleton // (measured minClass 2 — goal-top glide beelines have small faithful classes; the exhausted- // class inventory prints below), and at most 1 path of the 336 is evidence-free (genuine // unobservability by the parkPairExpressed contract, not a mis-read — measured exactly 1, // pinned so a regression that grows blind paths trips the gate). test('SLIDE-SIGNATURE: goal-top dives / safety-top detours + G-C expressed & recovered 6/6 + diverse-path widened 1.000 over 8 seeds', () => { let awards = 0; for (const seed of _SLIDE_SEEDS) { const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(E.makeParkSlideTask({ seed }), p)); assert.ok(E._parkSlideSignature(playouts), `seed ${seed}: verb signature failed to separate`); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const ex = E.parkPairExpressed(E.makeParkSlideTask({ seed }), playouts[i].moves, E.PARK_SLIDE_PAIR); assert.ok(ex > 0, `seed ${seed} ${persona.join('>')}: G-C never expressed on the faithful path`); awards += ex; const r = E.parkRecoverPairLex(E.makeParkSlideTask({ seed }), playouts[i].moves, E.PARK_SLIDE_PAIR, { expect: E._parkPushPairDir(persona, E.PARK_SLIDE_PAIR) }); assert.ok(r.recovered, `seed ${seed} ${persona.join('>')}: G-C not blind-recovered in the demonstrated direction`); } } let nPaths = 0, minClass = Infinity, pExpr = 0, pRec = 0, pMis = 0, pUnobs = 0; const exhausted = {}; for (const seed of _SLIDE_SEEDS) for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const paths = E.parkFaithfulPaths(E.makeParkSlideTask({ seed }), persona, 8, seed * 97 + 1); if (paths.length < 8) { const key = persona.map(a => a[0]).join('>'); (exhausted[key] = exhausted[key] || []).push(`s${seed}:${paths.length}`); } if (paths.length < minClass) minClass = paths.length; for (const po of paths) { nPaths++; if (E.parkPairExpressed(E.makeParkSlideTask({ seed }), po.moves, E.PARK_SLIDE_PAIR) > 0) { pExpr++; const r = E.parkRecoverPairLex(E.makeParkSlideTask({ seed }), po.moves, E.PARK_SLIDE_PAIR, { expect: E._parkPushPairDir(persona, E.PARK_SLIDE_PAIR) }); if (r.recovered) pRec++; else pMis++; } else pUnobs++; } } assert.ok(minClass >= 2, `diverse-path viability: a faithful class collapsed to ${minClass}`); assert.strictEqual(pMis, 0, `${pMis} expressed diverse paths MIS-read (real read flaw)`); assert.strictEqual(pRec, pExpr, `widened recovery ${pRec}/${pExpr} on expressed diverse paths (must be 1.000)`); assert.ok(pUnobs <= 1, `${pUnobs} evidence-free diverse paths (measured exactly 1/336 — growth = a posing regression)`); console.log(` [SLIDE-SIGNATURE] ${_SLIDE_SEEDS.length} seeds x 6 personas: signature separated, ${awards} G-C awards, 48/48 widened recoveries in-direction; diverse ${nPaths} paths minClass ${minClass}, widened ${pRec}/${pExpr} = 1.000 (mis 0, evidence-free ${pUnobs}); exhausted-class inventory ${JSON.stringify(exhausted)}`); }); // SLIDE-C1: the slide board is a PURE function of the PUBLIC cell — persona-stream stamps // never change a byte, repeated builds are deterministic, a played instance never corrupts // a rebuild (FRESH build per consumer) — AND the module's overlay law holds BYTE-FOR-BYTE: // the slide board is the IDENTICAL walk board (same public cell minus moveMech) plus ONLY // the park.slide flag (movement verb as pure runtime overlay — the phase-module discipline). // Junction braking is real geometry: the board has lamp-post junctions and >1-cell glides. const _slideCanon = (st, dropFlag) => { const park = { ...st.park }; if (dropFlag) delete park.slide; return JSON.stringify({ ...st, park }, (k, v) => v instanceof Set ? [...v].sort((a, b) => a - b) : v); }; test('SLIDE-C1: board bytes persona-invariant, deterministic, fresh per build; slide = walk board + flag ONLY; junctions real', () => { for (const seed of [1, 3, 5, 7]) { const a = _slideCanon(E.makeParkSlideTask({ seed }), false); assert.strictEqual(_slideCanon(E.makeParkSlideTask({ seed, personaStream: ['goal', 'safety', 'care'] }), false), a, `seed ${seed}: personaStream leaked into the board bytes`); assert.strictEqual(_slideCanon(E.makeParkSlideTask({ seed, personaStream: ['care', 'safety', 'goal'] }), false), a, `seed ${seed}: personaStream leaked into the board bytes`); E.parkPlayout(E.makeParkSlideTask({ seed }), E.PARK_PERSONAS[0]); // mutate one instance assert.strictEqual(_slideCanon(E.makeParkSlideTask({ seed }), false), a, `seed ${seed}: a played instance corrupted the rebuild`); const st = E.makeParkSlideTask({ seed }); assert.strictEqual(st.park.slide, true, `seed ${seed}: park.slide flag missing`); // overlay law: the walk twin of the SAME public cell (mech minus moveMech) differs by the flag alone const cell = E._parkSlideCell(st.park.seed); const twinCell = { ...cell, mech: { goalMech: cell.mech.goalMech, safetyMech: cell.mech.safetyMech } }; const twin = E._parkTaskBuild(E.PARK_SLIDE_KIND, twinCell, 0); assert.strictEqual(twin.park.slide, undefined, 'walk twin must carry no slide flag'); assert.strictEqual(_slideCanon(twin, false), _slideCanon(st, true), `seed ${seed}: slide board diverges from its walk twin beyond the park.slide flag`); // junction braking is real: >= 1 lamp-post junction on the walkway and >= 1 multi-cell glide const n = st.N; let junctions = 0, longGlide = false; for (const kk of st.park.walkway) if (E._parkSlideJunction(st, kk, { x: 1, y: 0 }) || E._parkSlideJunction(st, kk, { x: 0, y: 1 })) junctions++; for (let kk = 0; kk < n * n && !longGlide; kk++) { if (st.wall.has(kk)) continue; for (const d of [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }]) if (E._parkSlidePath(st, { x: kk % n, y: (kk / n) | 0 }, d, -1).length > 1) { longGlide = true; break; } } assert.ok(junctions > 0, `seed ${seed}: no lamp-post junctions on the walkway`); assert.ok(longGlide, `seed ${seed}: no multi-cell glide anywhere — momentum verb vacuous`); } console.log(' [SLIDE-C1] 4 seeds: byte-identical across persona stamps, deterministic, fresh per build; slide board == walk twin + park.slide flag ONLY; junctions + multi-cell glides real'); }); // SLIDE-CEILING: the module's OWN C* — parkCeiling's BFS measured in GLIDES (slide adds no // plan state: the walk search signature covers it; the dynamics are parkStep's slide branch // verbatim) — is finite (completion reachable under every persona's own compliant filter), // never beaten by the faithful playout (C* is the compliant MINIMUM), and separates the // route classes: goal-led C* (the moat glide) strictly below safety-led C* (the braked // walkway detour) on every seed — measured gaps 7<9 / 7<8 / 8<11 across the panel. test('SLIDE-CEILING: glide C* finite for 6/6 personas, faithful >= C*, goal-led < safety-led over 8 seeds', () => { const gaps = []; for (const seed of _SLIDE_SEEDS) { const cs = E.PARK_PERSONAS.map(p => E.parkCeiling(E.makeParkSlideTask({ seed }), p).turns); const tops = E.PARK_PERSONAS.map(p => p[0]); for (let i = 0; i < cs.length; i++) assert.ok(cs[i] != null && cs[i] >= 1, `seed ${seed} ${E.PARK_PERSONAS[i].join('>')}: C* not finite (${cs[i]})`); for (let i = 0; i < cs.length; i++) { const P = E.parkPlayout(E.makeParkSlideTask({ seed }), E.PARK_PERSONAS[i]); assert.ok(P.turns >= cs[i], `seed ${seed} ${E.PARK_PERSONAS[i].join('>')}: faithful ${P.turns} beat C* ${cs[i]} (ceiling unsound)`); } const g = Math.max(...cs.filter((c, i) => tops[i] === 'goal')); const s = Math.min(...cs.filter((c, i) => tops[i] === 'safety')); assert.ok(g < s, `seed ${seed}: goal-led C* ${g} not below safety-led C* ${s} (no route-ceiling gap)`); gaps.push(`s${seed}:${g}<${s}`); } console.log(` [SLIDE-CEILING] 8 seeds x 6 personas: C* finite, faithful >= C*, goal-led < safety-led (${gaps.join(' ')})`); }); /* ---------------- PARK-DYN (Task 0): dyn container + beat/schedule substrate ---------- */ // dyn is OPT-IN: a legacy board that never assigns st.park.dyn must carry no such field, and // parkStep on it must behave exactly as before dyn existed (C1/byte-stability). test('PARK-DYN dyn absent -> parkStep byte-identical to pre-dyn behavior', () => { const st = E.makeParkTask('m1', { seed: 7, hazard: { kind:'meadow', damage:1, d:2 } }); const P1 = E.parkStart(st); E.parkStep(P1, 'R'); assert(!P1.st.park.dyn, 'no dyn container on legacy cells'); }); test('PARK-DYN _parkDynInit shape + beat advances only with dyn', () => { const st = E.makeParkTask('m1', { seed: 7, hazard: { kind:'meadow', damage:1, d:2 } }); st.park.dyn = E._parkDynInit(st); const P = E.parkStart(st); E.parkStep(P, 'R'); assert(P.st.park.dyn.beat === 1, 'beat ticks per step'); }); // _parkSchedule: not directly exercised by the two tests above, but it is a Task 0 deliverable // every later mechanic's timing (drying stones / tilting logs / gate-open windows) will be // built on — pin its contract now: pure, deterministic, right shape, in-range. test('PARK-DYN _parkSchedule deterministic, length n, values in [0,period)', () => { const a = E._parkSchedule(7, 6, 4); const b = E._parkSchedule(7, 6, 4); assert.deepStrictEqual(a, b, 'same seed/n/period must reproduce the same schedule'); assert.strictEqual(a.length, 6, 'schedule length must equal n'); for (const v of a) assert.ok(v >= 0 && v < 4, `schedule value ${v} out of [0,period)`); const c = E._parkSchedule(8, 6, 4); assert.notDeepStrictEqual(c, a, 'a different seed should (almost certainly) diverge'); }); /* ============ Y12 STONES FIELD MODULE (Task 1): consumable stepping stones ============ */ // The y12 crossing: a stream cuts the board; three stone crossings span it — CRACKED (the // beeline, park.deep, so entry costs the body), the companion's FRESH crossing (its BFS plan // rides it), and a free FRESH crossing far away. Every stone the PLAYER leaves SINKS // (park.dyn.gone) and becomes water. Panel = the module's own seed sweep. const _Y12_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; // _parkY12Path(P, what): a TEST-ONLY walk helper — the move-key sequence that walks the player // from its current cell to the nearest cell of a target class ('cracked' | 'fresh' | 'bank'), // over the LIVE legal terrain (walls, water, sunk stones, the companion all excluded). BFS on // the same passability rule _parkLegal enforces, so a returned path is always playable. function _parkY12Path(P, what) { const st = P.st, n = st.N, park = st.park, dyn = park.dyn; const stone = (kk) => park.stones.fresh.has(kk) || park.stones.cracked.has(kk); const want = (kk) => what === 'cracked' ? park.stones.cracked.has(kk) : what === 'fresh' ? park.stones.fresh.has(kk) : /* bank */ !stone(kk); const co = st.pos[1] ? st.pos[1].y * n + st.pos[1].x : -1; const blocked = (kk) => st.wall.has(kk) || dyn.gone.has(kk) || kk === co; const src = st.pos[0].y * n + st.pos[0].x; const prev = new Map([[src, null]]); const q = [src]; const DD = [{ k: 'U', x: 0, y: -1 }, { k: 'D', x: 0, y: 1 }, { k: 'L', x: -1, y: 0 }, { k: 'R', x: 1, y: 0 }]; for (let h = 0; h < q.length; h++) { const kk = q[h]; if (kk !== src && want(kk)) { // unwind the move keys const out = []; for (let c = kk; prev.get(c); c = prev.get(c).from) out.unshift(prev.get(c).k); return out; } const x = kk % n, y = (kk / n) | 0; for (const d of DD) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (blocked(nk) || prev.has(nk)) continue; prev.set(nk, { from: kk, k: d.k }); q.push(nk); } } return null; } const _y12walk = (P, moves) => { for (const m of moves) E.parkStep(P, m); }; // Y12-STONES-PHYSICS: the consumable-terrain contract. (a) entering a CRACKED stone costs // exactly one heart — CERTAIN, not probabilistic (determinism principle); (b) LEAVING any stone // sinks it into park.dyn.gone; (c) a sunk stone is water — never legal again, and the companion // never plans through it; (d) the seed-pure park.stones sets are NEVER mutated by play. test('Y12-STONES-PHYSICS: cracked stone costs a heart; a left stone sinks and turns to water', () => { const cell = E._parkStonesCell(3); const P = E.parkStart(E._parkStonesBuild(cell)); const park = P.st.park; const freshBefore = [...park.stones.fresh].sort((a, b) => a - b).join(); const toCrack = _parkY12Path(P, 'cracked'); assert.ok(toCrack && toCrack.length, 'no walkable route to a cracked stone'); _y12walk(P, toCrack.slice(0, -1)); // up to the cell BEFORE the stone const h0 = P.hearts, d0 = P.deepEntries; E.parkStep(P, toCrack[toCrack.length - 1]); // step ON const k = E._parkKey(P.st, P.st.pos[0]); assert.ok(park.stones.cracked.has(k), 'the walk helper did not land on a cracked stone'); assert.strictEqual(P.hearts, h0 - 1, `cracked-stone entry must cost exactly 1 heart (${h0} -> ${P.hearts})`); assert.strictEqual(P.deepEntries, d0 + 1, 'cracked entry must tally on the deep-entry meter'); assert.ok(!park.dyn.gone.has(k), 'a stone under the walker has NOT sunk yet'); const off = _parkY12Path(P, 'bank'); assert.ok(off && off.length, 'stranded on a stone with no bank exit'); E.parkStep(P, off[0]); // step OFF assert.ok(park.dyn.gone.has(k), 'a stone the player LEFT must sink into dyn.gone'); // sunk == water: never a legal candidate, never a companion plan cell const back = E._parkLegal(P).map(c => c.key); assert.ok(!back.includes(k), 'a sunk stone is still offered as a legal move (it must be water)'); const plan = E._parkCompanionPlan(P); if (plan) assert.ok(!plan.path.includes(k), 'the companion planned a route through a sunk stone'); assert.strictEqual([...park.stones.fresh].sort((a, b) => a - b).join(), freshBefore, 'play mutated the SEED-PURE park.stones sets (only dyn.gone may change)'); }); // Y12-STONES-READS: the three-mind read of the new field. N (care) PRESERVES the companion's // crossing — the fresh stones on its live BFS plan are the ones it cannot do without — so the // N-compliant set never contains a step ONTO a guarded stone, while G (goal) and C (safety) are // free to take it. This is the pref-level wiring the blind reduction rides; it must be a real // discrimination, not a vacuous set. test('Y12-STONES-READS: N-pref preserves the companion-plan stones; G/C are free to take them', () => { let scenes = 0, diverged = 0; for (const seed of _Y12_SEEDS) { for (const persona of E.PARK_PERSONAS) { const P = E.parkStart(E._parkStonesBuild(E._parkStonesCell(seed))); while (!P.over) { const reads = E._parkReads(P); const guard = E._parkStonesGuard(P); const onGuard = reads.legal.filter(c => c.k !== 'stay' && guard.has(c.key)); if (onGuard.length && reads.atts.N.engaged) { scenes++; for (const c of onGuard) { assert.ok(!reads.atts.N.pref.has(c.k), `seed ${seed}: N prescribes a step onto a guarded companion stone (${c.k})`); // the OTHER half of the title, and the reason this cell exists: care alone protects the // crossing. If G or C ever ALSO forbade every guarded stone, the minds would agree here // and the scene would carry no conflict to read. (The old assertion here was a // tautology — `_parkKey(...) >= 0` is always true — so it tested nothing.) const gFree = reads.atts.G.engaged && reads.atts.G.pref.has(c.k); const cFree = reads.atts.C.engaged && reads.atts.C.pref.has(c.k); if (gFree || cFree) diverged++; } } E.parkStep(P, E.parkOracleMove(P, persona)); } } } assert.ok(scenes > 0, 'the guarded-stone scene never arose on any seed — the N read is vacuous'); assert.ok(diverged > 0, 'no guarded stone was EVER goal- or safety-compliant: care forbids nothing the other minds wanted, so the scene poses no conflict'); console.log(` [Y12-STONES-READS] ${scenes} guarded-stone decision states over 8 seeds x 6 personas; ${diverged} where G or C would have taken the stone N protects`); }); // Y12-STONES-C1: the board is a PURE function of the PUBLIC cell — persona stamps never change // a byte, repeated builds are deterministic, a played instance never corrupts a rebuild (dyn is // RUNTIME state, so a fresh build starts with an empty gone set), and the legacy walk boards are // untouched (no park.stones / no park.dyn on them — opt-in by construction). const _y12canon = (st) => JSON.stringify(st, (k, v) => v instanceof Set ? [...v].sort((a, b) => a - b) : v); test('Y12-STONES-C1: board bytes persona-invariant, deterministic, fresh per build; legacy walk boards untouched', () => { for (const seed of [1, 3, 5, 7]) { const a = _y12canon(E._parkStonesBuild(E._parkStonesCell(seed))); const withStream = { ...E._parkStonesCell(seed), personaStream: ['goal', 'safety', 'care'] }; assert.strictEqual(_y12canon(E._parkStonesBuild(withStream)), a, `seed ${seed}: personaStream leaked into the board bytes`); E.parkPlayout(E._parkStonesBuild(E._parkStonesCell(seed)), E.PARK_PERSONAS[0]); // mutate one instance assert.strictEqual(_y12canon(E._parkStonesBuild(E._parkStonesCell(seed))), a, `seed ${seed}: a played instance corrupted the rebuild`); const st = E._parkStonesBuild(E._parkStonesCell(seed)); assert.ok(st.park.stones && st.park.stones.cracked.size === 2, 'two cracked stones'); assert.ok(st.park.stones.fresh.size === 4, 'four fresh stones (two crossings)'); assert.strictEqual(st.park.dyn.gone.size, 0, 'a fresh build starts with no sunk stones'); assert.ok(st.park.water.size > 0, 'the stream must exist'); for (const kk of st.park.water) assert.ok(st.wall.has(kk), 'water must be impassable terrain (wall)'); for (const kk of st.park.stones.cracked) assert.ok(st.park.deep.has(kk), 'a cracked stone IS the deep field (its entry cost is the park deep-entry heart)'); } const legacy = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); assert.ok(!legacy.park.stones && !legacy.park.dyn && !legacy.park.water, 'a legacy walk board grew a stones/dyn field (byte stability broken)'); console.log(' [Y12-STONES-C1] 4 seeds: byte-identical across persona stamps, deterministic, fresh per build; legacy walk boards carry no stones/dyn/water'); }); // Y12-STONES-SHIP-GATE (P9/P10 ship rule): a y12 cell may ship as a LIVE picker tile ONLY if it // clears BLIND 6/6-persona order recovery — every persona's own faithful play completes and // parkRecoverOrder (trajectory + a fresh board, no persona symbol anywhere) reassembles exactly // the demonstrated order. Built out of the shipped recovery stack (parkRecoverOrder / // parkPairExpressed / parkRecoverPairLex), never a parallel channel. The measured sweep numbers // live on the campaign slot header; this gate PINS the two bars the module actually clears // (faithful completion + the per-pair widened recovery of every EXPRESSED pair) and REPORTS the // order-recovery tally, so a regression that loses a pair trips it. test('Y12-STONES-SHIP-GATE: 6/6 faithful completes alive; posed pairs blind-recovered; order recovery measured', () => { let rec = 0, tot = 0, expr = 0, recPair = 0, misPair = 0; const posed = { GC: 0, GN: 0, CN: 0 }; // per-pair, so the C-N verdict is pinned, not inferred const pairs = [['G', 'C'], ['G', 'N'], ['C', 'N']]; for (const seed of _Y12_SEEDS) { const cell = E._parkStonesCell(seed); const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(E._parkStonesBuild(cell), p)); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i], P = playouts[i]; assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}, not complete`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: faithful playout died`); tot++; const r = E.parkRecoverOrder(E._parkStonesBuild(cell), P.moves); if (r && r.join() === persona.join()) rec++; for (const pair of pairs) { if (!(E.parkPairExpressed(E._parkStonesBuild(cell), P.moves, pair) > 0)) continue; expr++; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); if (E.parkRecoverPairLex(E._parkStonesBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } } assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw, not a posing gap)`); // the SHIP bar itself: pinned to the MEASURED state of the module (see the campaign slot header) assert.strictEqual(E._parkStonesRecovers(E._parkStonesCell(E.PARK_STONES_SHIP_SEED)), E.PARK_STONES_SHIPPABLE, 'the module\'s declared shippability disagrees with the blind 6/6 order-recovery read on the shipped seed'); // THE DEMO-ONLY VERDICT, PINNED — the same negative tripwire the five sibling cells carry. y12's // C and N never contradict: the stones cost the BODY, and the companion's fresh row is preserved // by a move care would make anyway, so the caring option is a subset of the cautious one and the // trajectory carries no C-N evidence. Re-measured after all six cells landed: still 0/144. If // this ever fires it is GOOD news and must not land silently — re-measure the sweep and re-gate. assert.strictEqual(posed.CN, 0, `C-N is now EXPRESSED (${posed.CN}) on stones — the pair this cell never posed is being posed; re-measure and RE-GATE the slot (ship:true is earned by 6/6)`); assert.strictEqual(rec, 0, `blind ORDER recovery is now ${rec}/${tot} (was 0) — re-measure and re-gate`); // THE SHIPPED PATH, ACTUALLY DRIVEN. A field module has NO generator (see the tombstone at // _parkStonesAdmissible): the campaign reaches it through the registry, so drive exactly that — // `admits` for the sweep and `parkFieldBuild` -> `build` for the board — and read the reject // tallies that `admits` itself produced. (This block used to assert parkStonesGenFallbacks() === 0 // while nothing ever called the generator: the counter could not move, so the assertion was // VACUOUS and green forever. Every sibling inherited that hole; every sibling closed it.) const m = E.PARK_FIELD_MECHS.stones; for (const seed of _Y12_SEEDS) { const cell = m.cell(seed); assert.ok(m.admits(cell), `seed ${seed}: the campaign's own admission predicate REJECTS this cell (rejects: ${JSON.stringify(E.parkStonesWhys())})`); const st = E.parkFieldBuild(cell); assert.strictEqual(st.park.fieldMech, 'stones', `seed ${seed}: parkFieldBuild did not build a stones board`); assert.strictEqual(st.park.dyn.gone.size, 0, `seed ${seed}: parkFieldBuild handed back a PLAYED board (stones already sunk) — every blind read rebuilds, and a played board poisons all of them`); } assert.deepStrictEqual(E.parkStonesWhys(), { complete: 0, dead: 0, sig: 0, unexpressed: 0, norec: 0 }, 'the stones admission predicate REJECTED a swept cell (the LOUD reject tallies must stay 0)'); console.log(` [Y12-STONES-SHIP-GATE] 8 seeds x 6 personas: ${tot}/${tot} faithful completes alive; blind ORDER recovery ${rec}/${tot}; per-pair widened ${recPair}/${expr} expressed (mis-read ${misPair}); posed G-C ${posed.GC} G-N ${posed.GN} C-N ${posed.CN}; registry admits ${_Y12_SEEDS.length}/${_Y12_SEEDS.length} swept cells, every build fresh; ship seed ${E.PARK_STONES_SHIP_SEED} shippable=${E.PARK_STONES_SHIPPABLE}`); }); /* ============ PARK_FIELD_MECHS — THE FIELD-MECHANIC PLUG-IN SEAM (Task 1) ============ */ // The registry is the SEAM five more park cells (a toll gate, a downed companion, a rolling log, // rising water, a duckling) are built through, in parallel, without touching the engine body. So // the seam must be proven GENERIC — not merely "whatever y12 happened to need". These tests // therefore drive it with a SYNTHETIC mech (`__probe`) that uses every hook, including the three // y12 does NOT use (onEnter, tick, oracleCost): if the seam only fits stones, they fail. // // THE HOOK BUNDLE (the contract T2-T6 are handed): // build(cell) -> st the board (stamps park.fieldMech + park.dyn itself) // legalMask(P, key, who) -> bool 'me' | 'mate' | 'route' — is this cell impassable? // onEnter(P, ev) after the walker's position commits (ev = {mvKey,from,to,fromKey,toKey}) // onLeave(P, ev) for the cell the walker vacated // tick(P, ev) the world advances (after dyn.beat++) — may end the run // reads: { ctx(P), G|C|N: { engaged(P,ctx), prefer(P,legal,ctx) -> Set } } // oracleCost(P, cand, ctx) -> num an additive term on the persona's route-metric argmin const _PROBE_ID = '__probe'; // _probeMech(cfg): a throwaway mech over a LEGACY walk board — the board is the shipped m1 walk // task, so any behavior change the probe observes is the SEAM's doing and nothing else. function _probeMech(cfg) { cfg = cfg || {}; return { build: (cell) => { const st = E.makeParkTask('m1', { seed: (cell && cell.seed) || 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); st.park.fieldMech = _PROBE_ID; st.park.dyn = E._parkDynInit(st); st.park.probe = { log: [], ticks: [], purse: 3, mask: new Set(), maskWho: null, bite: 0 }; return st; }, legalMask: (P, key, who) => { const pr = P.st.park.probe; return pr.mask.has(key) && (pr.maskWho == null || pr.maskWho === who); }, onEnter: (P, ev) => { const pr = P.st.park.probe; pr.log.push('enter:' + ev.mvKey + ':' + ev.toKey); pr.purse--; }, onLeave: (P, ev) => { P.st.park.probe.log.push('leave:' + ev.mvKey + ':' + ev.fromKey); }, tick: (P, ev) => { const pr = P.st.park.probe; pr.ticks.push(E._parkBeatOf(P)); if (pr.bite) { P.hearts -= pr.bite; pr.bite = 0; } // the T4 body-block / T5 terminal shape if (cfg.drown) { P.over = true; P.reason = 'drown'; } // a tick may END the run on its own terms }, reads: { ctx: (P) => ({ ban: P.st.park.probe.ban || new Set() }), N: { engaged: (P, ctx) => ctx.ban.size > 0, // engage an attitude the walk board leaves cold prefer: (P, legal, ctx) => new Set(legal.filter(c => !ctx.ban.has(c.key)).map(c => c.k)), }, C: { prefer: (P, legal, ctx) => new Set(legal.filter(c => !ctx.ban.has(c.key)).map(c => c.k)) }, G: { prefer: (P, legal) => new Set(legal.map(c => c.k)) }, // inert: must not perturb G }, oracleCost: (P, cand) => (P.st.park.probe.pricey === cand.k ? 100 : 0), }; } // REGISTRY-SEAM-HOOKS: the lifecycle half of the contract — a mech is reached through // cell.mech.fieldMech alone, every hook fires where the contract says, and a board with NO // fieldMech is a no-op on every dispatch point (byte stability: the whole park predates this). test('REGISTRY-SEAM-HOOKS: build/legalMask/onEnter/onLeave/tick fire per contract; no-fieldMech board untouched', () => { E.PARK_FIELD_MECHS[_PROBE_ID] = _probeMech(); try { // (1) build + dispatch by cell.mech.fieldMech (the campaign's only entry point) const st = E.parkFieldBuild({ seed: 7, mech: { fieldMech: _PROBE_ID } }); assert.strictEqual(st.park.fieldMech, _PROBE_ID, 'the board does not carry its mech id'); const P = E.parkStart(st); const pr = P.st.park.probe; // (2) legalMask('me'): a masked cell leaves the player's legal set... const legal0 = E._parkLegal(P).map(c => c.k).sort().join(''); const victim = E._parkLegal(P).find(c => c.k !== 'stay'); pr.mask.add(victim.key); pr.maskWho = 'me'; const legal1 = E._parkLegal(P).map(c => c.k).sort().join(''); assert.ok(!legal1.includes(victim.k) && legal0.includes(victim.k), 'legalMask(who="me") did not remove the masked cell from _parkLegal'); // ...and 'mate' is a SEPARATE domain (the companion planner must be maskable on its own — // T2's closed gate and T4's rolling log are walls for the companion but not for the walker) const mateOk = E._parkCompanionPlan(P); pr.maskWho = 'mate'; pr.mask = new Set(mateOk && mateOk.path ? mateOk.path.slice(0, 1) : []); const mateBlocked = E._parkCompanionPlan(P); if (mateOk && mateOk.path && mateOk.path.length) { assert.ok(!mateBlocked || !mateBlocked.path.length || mateBlocked.path[0] !== mateOk.path[0], 'legalMask(who="mate") did not re-route the companion plan'); } pr.mask = new Set(); pr.maskWho = null; // (3) onLeave BEFORE onEnter, both carrying the vacated/entered keys; tick after the beat const fromKey = E._parkKey(P.st, P.st.pos[0]); const mv = E._parkLegal(P).find(c => c.k !== 'stay'); E.parkStep(P, mv.k); assert.deepStrictEqual(pr.log, ['leave:' + mv.k + ':' + fromKey, 'enter:' + mv.k + ':' + mv.key], 'onLeave/onEnter did not fire in order with the right cells'); assert.strictEqual(pr.purse, 2, 'onEnter could not charge an entry cost (the toll shape)'); assert.deepStrictEqual(pr.ticks, [1], 'tick did not fire once, after dyn.beat++'); // (4) tick fires on 'stay' too — T4's log rolls and T5's water rises while the walker waits E.parkStep(P, 'stay'); assert.deepStrictEqual(pr.ticks, [1, 2], 'tick did not fire on a stay (a beat-driven entity would freeze)'); // (5) a tick that costs the body ENDS the run on that same step (T4 body-block): the death // check must be re-read AFTER the tick, or a lethal hit is silently deferred a whole turn. pr.bite = 99; E.parkStep(P, 'stay'); assert.ok(P.over && P.reason === 'death', 'a tick that emptied the body did not end the run on that step'); // (6) a board with NO fieldMech never touches the seam const legacy = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); assert.ok(!legacy.park.fieldMech && !legacy.park.dyn, 'a legacy walk board grew a fieldMech/dyn field'); const L = E.parkStart(legacy); E.parkStep(L, E.parkOracleMove(L, E.PARK_PERSONAS[0])); assert.ok(!L.st.park.probe, 'the seam ran on a board that never opted in'); } finally { delete E.PARK_FIELD_MECHS[_PROBE_ID]; } }); // REGISTRY-SEAM-READS: the SELECTION half — a mech may (a) engage an attitude the walk board // leaves disengaged, (b) narrow any of G/C/N by INTERSECTION with the shipped compliant set (the // lexical discipline: never a new scoring channel — parkReduce stays the one scorer), and // (c) price a candidate in the persona's argmin. Also: a tick may end the run on its own reason. test('REGISTRY-SEAM-READS: reads.ctx/engaged/prefer narrow G/C/N by intersection; oracleCost prices the argmin; tick may end the run', () => { E.PARK_FIELD_MECHS[_PROBE_ID] = _probeMech(); try { const P = E.parkStart(E.parkFieldBuild({ seed: 7, mech: { fieldMech: _PROBE_ID } })); const pr = P.st.park.probe; // (a) a mech may ENGAGE a mind the walk board leaves cold (y12's care facet does exactly this; // T3's downed companion and T4's block formation will too). Walk to a state where the // SHIPPED care attitude is disengaged — otherwise the OR is vacuous and proves nothing. let cold = false; for (let i = 0; i < 60 && !P.over; i++) { if (!E.PARK_ATTITUDES.N.engaged(P)) { cold = true; break; } E.parkStep(P, E.parkOracleMove(P, E.PARK_PERSONAS[0])); } assert.ok(cold && !P.over, 'no state found where the shipped care attitude is disengaged'); assert.ok(!E._parkReads(P).atts.N.engaged, 'precondition: the merged N must be cold before the mech speaks'); const ban = E._parkLegal(P).filter(c => c.k !== 'stay').map(c => c.key); pr.ban = new Set(ban.slice(0, 1)); const rd = E._parkReads(P); assert.ok(rd.atts.N.engaged, 'reads.N.engaged could not engage an attitude the walk board leaves disengaged'); // (b) INTERSECTION, not replacement: the banned move is gone from N, and every OTHER move the // shipped attitude allowed survives (a mech that could REPLACE a compliant set would be a // second scoring channel wearing the attitude's name). const banned = E._parkLegal(P).find(c => c.key === ban[0]).k; assert.ok(!rd.atts.N.pref.has(banned), 'reads.N.prefer did not narrow the compliant set'); assert.ok(rd.atts.N.pref.has('stay'), 'the mech-narrowed N lost a move it never banned'); // C is narrowed the same way, and its shipped band still binds (the intersection is the // SHIPPED C set minus the mech ban — never a superset of it). const baseC = E.PARK_ATTITUDES.C.engaged(P) ? E.PARK_ATTITUDES.C.preference(P, E._parkLegal(P)) : null; if (baseC) for (const k of rd.atts.C.pref) assert.ok(baseC.has(k), 'a mech prefer() WIDENED the shipped C set'); // (c) oracleCost prices a candidate out of the argmin without touching any attitude. It can // only bite where the persona actually HAS a choice — the lexical filter routinely narrows // to a singleton, and there the tie-break is inert by construction. So walk to a state with // >= 2 compliant moves and price out the one the persona wanted. pr.ban = new Set(); const persona = E.PARK_PERSONAS[0], ord = E.parkOrderingFor(persona); let forked = false; for (let i = 0; i < 60 && !P.over; i++) { if (E.parkLexFilter(P, ord).size >= 2) { forked = true; break; } E.parkStep(P, E.parkOracleMove(P, persona)); } assert.ok(forked && !P.over, 'no state found where the persona has >= 2 lexically compliant moves'); const want = E.parkOracleMove(P, persona); pr.pricey = want; assert.notStrictEqual(E.parkOracleMove(P, persona), want, 'oracleCost did not re-price the persona argmin'); pr.pricey = null; assert.strictEqual(E.parkOracleMove(P, persona), want, 'the argmin did not return to the shipped pick once the price was lifted'); } finally { delete E.PARK_FIELD_MECHS[_PROBE_ID]; } // (d) a tick may terminate the run with its OWN reason (T5's rising water drowns the walker) E.PARK_FIELD_MECHS[_PROBE_ID] = _probeMech({ drown: true }); try { const P = E.parkStart(E.parkFieldBuild({ seed: 7, mech: { fieldMech: _PROBE_ID } })); E.parkStep(P, 'stay'); assert.ok(P.over && P.reason === 'drown', 'a tick could not end the run on its own terms'); } finally { delete E.PARK_FIELD_MECHS[_PROBE_ID]; } }); // REGISTRY-SEAM-BODY: THE GUARANTEE THE FIVE PARALLEL CELLS RIDE ON. The engine BODY // (parkStep / _parkLegal / _parkReads / _parkFields / _parkCompanionPlan / parkOracleMove) must // name NO mechanic — every mechanic-specific fact lives behind the registry, inside the // mechanic's own block. If this fails, the next cell cannot be added without editing the body, // which is exactly the coupling the registry exists to remove: source-level, so it cannot rot. test('REGISTRY-SEAM-BODY: stones registers through PARK_FIELD_MECHS; the engine body names no mechanic', () => { // the first client is reached ONLY through the registry const m = E.PARK_FIELD_MECHS.stones; assert.ok(m && typeof m.build === 'function', 'stones is not registered in PARK_FIELD_MECHS'); const st = E.parkFieldBuild({ seed: 1, mech: { fieldMech: 'stones' } }); assert.strictEqual(st.park.fieldMech, 'stones', 'parkFieldBuild did not stamp the mech id'); assert.ok(st.park.stones && st.park.stones.cracked.size === 2, 'parkFieldBuild did not build the y12 board'); // the SEAM GATE. Slice engine.js at the SENTINEL banner: everything above is the shared body, // everything below is mechanic territory. (The sentinel is a fixed marker, not "wherever stones // happens to start" — an agent whose module lands above stones must still land below the line.) // Comments are stripped first: the registry's contract doc necessarily cites y12 as its worked // example, and a gate that policed prose would just teach the next owner to rename variables. // // This is a STRUCTURAL check, not a blacklist of names I guessed. It derives the forbidden ids // from the registry itself, so it covers mechanics that do not exist yet: const src = fs.readFileSync(path.join(__dirname, 'engine.js'), 'utf8'); const cut = src.indexOf(E.PARK_FIELD_SENTINEL); assert.ok(cut > 0, `the field-module sentinel (${E.PARK_FIELD_SENTINEL}) is missing from engine.js`); const body = src.slice(0, cut) .replace(/\/\*[\s\S]*?\*\//g, ' ') // block comments .replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // line comments (': //' would be a URL, not a comment) // (1) no registered mech id may appear as a STRING LITERAL in the body — this is what catches the // most natural leak of all, `if (st.park.fieldMech === 'toll')`, and it grows automatically // with every mechanic the five agents register. for (const id of Object.keys(E.PARK_FIELD_MECHS)) { for (const lit of [`'${id}'`, `"${id}"`, '`' + id + '`']) { assert.ok(!body.includes(lit), `the engine body branches on a mech id (${lit}) — dispatch belongs in PARK_FIELD_MECHS, not in an if`); } assert.ok(!body.includes('park.' + id) && !new RegExp('_park' + id[0].toUpperCase() + id.slice(1)).test(body), `the engine body names mechanic '${id}' (park.${id} / _park${id}... ) — it must be reachable ONLY through the registry`); } // (2) the body's ONLY dyn member access is `.beat`. dyn is the shared Task-0 substrate; every // other field on it (gone / flood / ents / gateOpen / downed, and whatever a mechanic adds) // is MECHANIC-PRIVATE, and the body reading any of them would be reaching past the seam. // _parkClone is exempt: it must copy dyn without knowing what is in it, which is why it now // clones structurally rather than by naming fields. const dynHits = [...body.matchAll(/\bdyn\s*\.\s*([A-Za-z_$][\w$]*)/g)].map(m => m[1]); const illegal = [...new Set(dynHits)].filter(f => f !== 'beat'); assert.deepStrictEqual(illegal, [], `the engine body reads mechanic-private dyn fields (${illegal.join(', ')}) — only dyn.beat is the body's business`); // (3) belt and braces: the private state of the five cells that do not exist yet for (const bad of ['park.toll', 'park.flood', 'park.duck', 'park.downed', 'park.log']) { assert.ok(!body.includes(bad), `the engine body names a mechanic (${bad})`); } console.log(' [REGISTRY-SEAM-BODY] engine body is mechanic-free; stones reaches the engine only through PARK_FIELD_MECHS'); }); /* ---- the seam's THREE STRUCTURAL GUARANTEES (review fixes, 2026-07-13) ---------------------- Three defects the first cut of the seam had. Each is in the ENGINE BODY — the part T2-T6 may not touch — so each gets a test that pins the guarantee for them. All three are driven by fixture mechs, because the point is precisely that they must hold for mechs y12 is not. */ // _blockMech(cfg): a fixture that MASKS THE COMPANION'S DOMAIN — the shape shared by y14's closed // toll gate, y3's frozen companion and y10's sinking rings. This is the fixture that found C1. function _blockMech(cfg) { cfg = cfg || {}; return { build: (cell) => { const st = E.makeParkTask('m1', { seed: (cell && cell.seed) || 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); st.park.fieldMech = '__block'; st.park.dyn = E._parkDynInit(st); st.park.probe = { wall: cfg.wall !== false }; return st; }, // the companion cannot move at all: every cell but its own is a wall FOR IT (who === 'mate'). // The walker's own domain is untouched — exactly y3's "the companion is down" and y14's // "the gate is shut against him". legalMask: (P, key, who) => who === 'mate' && P.st.park.probe.wall, }; } // SEAM-COMPANION-STUCK (C1): "unreachable" and "arrived" are NOT the same thing. The companion // planner used to return the same shape for both ({next: null}), and _parkCompanionStep read that // as ARRIVED — so it advanced the contract, relocated, and permanently RETIRED (P.mode 'done'). // The care read then goes cold forever. That breaks y14 (a shut gate makes the gem unreachable, so // the companion abandons the contract before the walker can pay the second toll), y3 (freezing the // companion IS masking it), and y10 (sinking rings orphan him) — and it is latent in y12 itself: // once the last fresh stone sinks, the stranded companion the whole care read defends against // would just quietly retire instead of registering as harm. // THE GUARANTEE: a companion can be STUCK without being FINISHED. test('SEAM-COMPANION-STUCK: a masked-in companion WAITS and keeps its contract (never retires)', () => { E.PARK_FIELD_MECHS.__block = _blockMech(); try { const P = E.parkStart(E.parkFieldBuild({ seed: 7, mech: { fieldMech: '__block' } })); // Drive the COMPANION directly. (Through a playout the walker would eat the contracted gem and // advance the contract for a perfectly good reason — which is not the bug under test.) Put him // on his errand, with his gem alive and out of his reach, and tick him: he must simply WAIT. P.mode = 'toGem'; const at0 = { ...P.st.pos[1] }, contract0 = P.contract; assert.ok(P.st.tokens[P.st.park.contracts[contract0].gem].alive, 'precondition: his gem is still there'); for (let i = 0; i < 12; i++) E._parkCompanionStep(P); assert.strictEqual(P.st.pos[1].x, at0.x, 'the frozen companion moved'); assert.strictEqual(P.st.pos[1].y, at0.y, 'the frozen companion moved'); assert.strictEqual(P.contract, contract0, 'the stuck companion ADVANCED its contract (it retired instead of waiting)'); assert.strictEqual(P.mode, 'toGem', 'the stuck companion abandoned his errand — "unreachable" was read as "arrived"'); // and the plan says WHY it cannot move, so a mechanic can tell the two apart const plan = E._parkCompanionPlan(P); assert.ok(plan, 'a stuck companion must still HAVE a plan object (null = no contract at all)'); assert.strictEqual(plan.next, null, 'a stuck companion has no next step'); assert.strictEqual(plan.stuck, true, 'the plan does not report WHY there is no next step'); assert.ok(!plan.arrived, 'a stuck companion is not an arrived one'); } finally { delete E.PARK_FIELD_MECHS.__block; } // the same guarantee on the REAL cell: sink every stone and the companion must be stranded — // which is the harm y12's care read exists to defend against. If he retires instead, the stake // the whole cell is built on is fake. const P2 = E.parkStart(E._parkStonesBuild(E._parkStonesCell(1))); const park = P2.st.park; P2.mode = 'toGem'; // he is on his errand (trig = the whole board) const plan0 = E._parkCompanionPlan(P2); assert.ok(plan0 && plan0.next && !plan0.stuck, 'precondition: with the stones intact he has a way over'); for (const kk of park.stones.fresh) park.dyn.gone.add(kk); // every sound crossing spent for (const kk of park.stones.cracked) park.dyn.gone.add(kk); const plan2 = E._parkCompanionPlan(P2); assert.ok(plan2 && plan2.stuck === true && !plan2.arrived, 'with every stone sunk the companion is NOT reported stranded — the care stake is unenforced'); const before = { ...P2.st.pos[1], c: P2.contract, m: P2.mode }; E._parkCompanionStep(P2); assert.ok(P2.st.pos[1].x === before.x && P2.st.pos[1].y === before.y && P2.contract === before.c && P2.mode === before.m, 'the stranded companion walked on water or quietly retired instead of being stranded'); }); // SEAM-LEGAL-ADD (C2): legalMask can only SUBTRACT. y3's assist is "walk into the adjacent downed // companion" — a cell the engine's own hardcoded rule ("the companion's cell is never a // destination") forbids, and no mask can re-open. Without an additive path the assist — the whole // of y3 — is inexpressible through the seam. test('SEAM-LEGAL-ADD: legalAdd re-opens a cell the engine itself forbids (the y3 assist shape)', () => { E.PARK_FIELD_MECHS.__add = { build: (cell) => { const st = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); st.park.fieldMech = '__add'; st.park.dyn = E._parkDynInit(st); st.park.probe = { open: false }; return st; }, legalAdd: (P, key, who) => who === 'me' && P.st.park.probe.open && key === E._parkKey(P.st, P.st.pos[1]), // the COMPANION's own cell }; try { const P = E.parkStart(E.parkFieldBuild({ seed: 7, mech: { fieldMech: '__add' } })); // walk the player adjacent to the companion let adj = false; for (let i = 0; i < 80 && !P.over; i++) { if (Math.abs(P.st.pos[0].x - P.st.pos[1].x) + Math.abs(P.st.pos[0].y - P.st.pos[1].y) === 1) { adj = true; break; } E.parkStep(P, E.parkOracleMove(P, E.PARK_PERSONAS[0])); } assert.ok(adj && !P.over, 'never got adjacent to the companion'); const coKey = E._parkKey(P.st, P.st.pos[1]); const shut = E._parkLegal(P).some(c => c.key === coKey); assert.ok(!shut, 'precondition: the companion cell is normally not a destination'); P.st.park.probe.open = true; const opened = E._parkLegal(P).find(c => c.key === coKey); assert.ok(opened, 'legalAdd could not re-open the companion cell — the y3 assist is inexpressible'); assert.strictEqual(opened.add, true, 'a force-opened candidate is not flagged (a mech cannot tell it apart)'); } finally { delete E.PARK_FIELD_MECHS.__add; } }); // SEAM-CLONE-DEEP (C3): parkCeiling forks runtimes and searches. dyn is MUTABLE, so a fork that // shares any part of it lets sibling branches corrupt each other and the C* plan gets read off a // board no branch ever stood on. The first cut enumerated gone/flood/ents/gateOpen by hand — it // missed `downed` (shipped by _parkDynInit!) and cloned `ents` only one level deep, so y3's // dyn.downed and y6's ents[].trail would have leaked. Clone STRUCTURALLY instead. test('SEAM-CLONE-DEEP: _parkClone deep-copies every dyn field, including nested ones', () => { const st = E._parkStonesBuild(E._parkStonesCell(1)); const P = E.parkStart(st); const d = P.st.park.dyn; // the fields _parkDynInit ships, plus the nesting the five cells will actually put there d.downed = { rescued: false, crawl: 0 }; // y3 d.ents.push({ kind: 'duck', at: 5, hearts: 1, trail: [1, 2] }); // y6 (nested array) d.gone.add(99); d.flood.add(7); d.gateOpen.me = true; // y12 / y10 / y14 const F = E._parkClone(P); assert.notStrictEqual(F.st.park.dyn, d, 'the fork shares the dyn container'); for (const k of ['gone', 'flood', 'ents', 'gateOpen', 'downed']) { assert.notStrictEqual(F.st.park.dyn[k], d[k], `dyn.${k} is shared by reference with the fork`); } assert.notStrictEqual(F.st.park.dyn.ents[0], d.ents[0], 'dyn.ents[0] is shared with the fork'); assert.notStrictEqual(F.st.park.dyn.ents[0].trail, d.ents[0].trail, 'dyn.ents[0].trail is shared (one-level-deep clone)'); // mutate the fork every way the five cells will; the parent must not feel any of it F.st.park.dyn.downed.rescued = true; F.st.park.dyn.ents[0].trail.push(3); F.st.park.dyn.ents[0].hearts = 0; F.st.park.dyn.gone.add(1234); F.st.park.dyn.gateOpen.mate = true; assert.strictEqual(d.downed.rescued, false, 'a fork mutated the parent dyn.downed (y3 would break)'); assert.strictEqual(d.ents[0].trail.length, 2, 'a fork mutated the parent dyn.ents[].trail (y6 would break)'); assert.strictEqual(d.ents[0].hearts, 1, 'a fork mutated the parent entity'); assert.ok(!d.gone.has(1234) && !d.gateOpen.mate, 'a fork mutated the parent dyn terrain'); // and the values really were copied, not just the containers assert.ok(F.st.park.dyn.gone.has(99) && F.st.park.dyn.flood.has(7) && F.st.park.dyn.gateOpen.me === true, 'the fork did not inherit the parent dyn state'); }); // SEAM-INVARIANTS (I2/I3): the two traps the five agents would otherwise fall into, PINNED as // behavior so the contract text cannot drift from the code. // I3 the legal set is NEVER empty. A mask that took every cell (y10's water taking the walker's // own square) would make parkOracleMove return 'stay', which parkStep then rejects as INPUT // NOISE — turns and the beat stop advancing and the run limps to reason:'noise' instead of // the mechanic's own terminal. 'stay' is therefore always re-admitted as a last resort; a // mechanic that wants the run to END must end it in tick(). // I2 a reads.prefer returning the EMPTY set does NOT veto: _parkLexSet narrows only on a // non-empty set, so an empty narrowing makes that mind INERT — the opposite of what an // author writing `prefer` would assume. Pinned here so the contract's warning stays true. test('SEAM-INVARIANTS: the legal set is never empty; an empty prefer() makes a mind inert, it does not veto', () => { // I3 — a mech that masks EVERYTHING, the walker's own cell included E.PARK_FIELD_MECHS.__void = { build: (cell) => { const st = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); st.park.fieldMech = '__void'; st.park.dyn = E._parkDynInit(st); return st; }, legalMask: () => true, // every cell, every traveller, impassable }; try { const P = E.parkStart(E.parkFieldBuild({ seed: 7, mech: { fieldMech: '__void' } })); const legal = E._parkLegal(P); assert.strictEqual(legal.length, 1, 'a total mask did not leave exactly the last-resort move'); assert.strictEqual(legal[0].k, 'stay', "the last-resort move is not 'stay'"); const noise0 = P.inputNoise; E.parkStep(P, E.parkOracleMove(P, E.PARK_PERSONAS[0])); assert.strictEqual(P.inputNoise, noise0, 'the run limped into input-noise instead of stepping'); assert.strictEqual(P.turns, 1, 'the turn did not advance under a total mask'); assert.strictEqual(E._parkBeatOf(P), 1, 'the beat did not advance under a total mask (schedules would freeze)'); } finally { delete E.PARK_FIELD_MECHS.__void; } // I2 — an empty prefer() is INERT, not a veto E.PARK_FIELD_MECHS.__mute = { build: (cell) => { const st = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); st.park.fieldMech = '__mute'; st.park.dyn = E._parkDynInit(st); return st; }, reads: { G: { prefer: () => new Set() } }, // "veto everything" — an author's natural mistake }; try { const P = E.parkStart(E.parkFieldBuild({ seed: 7, mech: { fieldMech: '__mute' } })); const rd = E._parkReads(P); assert.ok(rd.atts.G.engaged, 'precondition: G is engaged here'); assert.strictEqual(rd.atts.G.pref.size, 0, 'the empty narrowing did not reach the read'); const ord = E.parkOrderingFor(E.PARK_PERSONAS[0]); // goal-top const lex = E.parkLexFilter(P, ord); assert.ok(lex.size > 1, 'an empty prefer() VETOED the move set — if this ever becomes true, the contract note on prefer must change'); assert.ok(E.parkOracleMove(P, E.PARK_PERSONAS[0]) !== null, 'the oracle lost its move'); } finally { delete E.PARK_FIELD_MECHS.__mute; } }); // SEAM-BUILD-LOUD (minor): a typo in the ONE line a T2-T6 agent adds to campaign.js used to yield // a null board that failed somewhere far away. Fail AT the typo instead. test('SEAM-BUILD-LOUD: parkFieldBuild throws on an unregistered fieldMech', () => { assert.throws(() => E.parkFieldBuild({ seed: 1, mech: { fieldMech: 'nope' } }), /nope/, 'an unknown fieldMech built a null board instead of throwing'); }); // SEAM-COLD-PREFER: the trap every field cell walked into, hoisted out of the six modules and // enforced ONCE, over the registry, for every cell that will ever be added. // // _parkReads (engine.js) ORs engagement but INTERSECTS preference: // engaged = shipped.engaged(P) || mech.engaged(P, ctx) // if (engaged) pref = shipped.preference(P, legal) INTERSECT mech.prefer(P, legal, ctx) // so a module's prefer() RUNS IN STATES ITS OWN engaged() REJECTED — wherever the shipped // attitude is warm and the mechanic's facet is cold. Measured while the cells were being built: // y8's C.prefer ran cold in 1490 states, y6's in 1407 of 3718, and in y8's case the module header // documented the exact opposite of what shipped. Left unguarded it is not merely wrong, it is // silent: a cold prefer that narrows to {'stay'} makes a CARE-LED WALKER STOP WALKING, and a cold // prefer that narrows to {} makes that mind INERT (see SEAM-INVARIANTS I2) — the opposite of the // veto its author intended. Both failure modes are invisible in a green sweep. // // The discipline: gate prefer() on the same predicate as engaged(), and return the FULL legal set // when cold. Every one of the six cells now does. This test is what keeps the seventh honest. // // NOT fixed in the engine body, deliberately. Making _parkReads skip a cold mech.prefer would fuse // two dials that are independent by design — engaged is an OR-in ("turn a mind ON that the board // leaves cold"), prefer is a narrowing ("apply wherever it is on"). A mechanic that wants to narrow // a mind the SHIPPED attitude already engages, without ever forcing it on, would then have to // return engaged:true and force it on everywhere. That trades one silent surprise for another, and // it makes the MIRRORED bug — an engaged() narrower than its prefer() domain — unobservable rather // than impossible. A body change hides the mistake; this test makes it fail loudly. test('SEAM-COLD-PREFER: every registered mechanic\'s prefer() is INERT wherever its own engaged() is false', () => { let mechs = 0, coldStates = 0, checks = 0; const offenders = []; for (const id of Object.keys(E.PARK_FIELD_MECHS)) { const m = E.PARK_FIELD_MECHS[id]; if (!m.reads || !m.cell) continue; // no reads to police / no seeds to sweep const facets = ['G', 'C', 'N'].filter(k => m.reads[k] && m.reads[k].engaged && m.reads[k].prefer); if (!facets.length) continue; mechs++; for (const seed of [1, 5, 9]) { for (const persona of E.PARK_PERSONAS) { const P = E.parkStart(E.parkFieldBuild(m.cell(seed))); while (!P.over) { const legal = E._parkLegal(P); const ctx = m.reads.ctx ? m.reads.ctx(P) : null; for (const k of facets) { if (m.reads[k].engaged(P, ctx)) continue; // warm: prefer() is entitled to narrow coldStates++; const pref = m.reads[k].prefer(P, legal, ctx); // cold: it must not remove a thing for (const c of legal) { checks++; if (!pref.has(c.k)) { offenders.push(`${id}.${k} seed ${seed} ${persona.join('>')}: cold prefer() dropped '${c.k}' (legal ${legal.map(l => l.k).join(',')} -> pref ${[...pref].join(',') || '(empty)'})`); } } } E.parkStep(P, E.parkOracleMove(P, persona)); } } } } assert.strictEqual(offenders.length, 0, `a cold facet NARROWED the compliant set — the mind it belongs to is being silently steered (or silenced) in states the mechanic itself says it has no opinion about:\n ${offenders.slice(0, 6).join('\n ')}${offenders.length > 6 ? `\n ... and ${offenders.length - 6} more` : ''}`); // anti-vacuity: if no cold state is ever reached, this test proves nothing and must be re-aimed. assert.ok(coldStates > 0, 'no facet was ever cold across the whole registry — this gate is vacuous as written and would pass against any prefer()'); assert.ok(mechs > 0, 'no registered mechanic exposes a gated reads facet — the gate has nothing to police'); console.log(` [SEAM-COLD-PREFER] ${mechs} mechanics x 3 seeds x 6 personas: ${coldStates} cold-facet states, ${checks} legal moves checked, 0 narrowed`); }); /* ================ y14 TOLL GATE (Task 2) — the gem-priced crossing ================ The cell: a hedge cuts the board in two and there are exactly TWO ways through. The FIELD gap is the beeline and it is the park's deep meadow — crossing costs a heart. The GATE is safe, one step off the line, and it takes a GEM. Entering it deducts the toll and opens it FOR THE WALKER ONLY (dyn.gateOpen.me). Paying a SECOND time — standing on the gate and moving into it again, which is the in-place 'stay' — buys the COMPANION's lane (dyn.gateOpen.mate), and only then does his planner route through it. Until then his gem is unreachable and his plan is {stuck:true}: he HOLDS (seam guarantee 1), which is the only reason the second toll can still land. The junction the cell poses: G = don't pay, cut the field, eat the heart. C = pay, take the gate, keep the body. N = pay the SECOND toll — it buys you nothing and opens his road. The toll spends the GEM GAUGE (st.score[0]); the goal threshold (the chain) is UNCHANGED, so paying is a G-priced move, not a new scoring channel (parkReduce stays the one scorer). Test-only walk helpers (they belong to the tests, not to the engine surface): _y14To drives the walker along a BFS route over his OWN legal set, _y14Gate names the move key that pays. */ const _Y14_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; // _y14Route(P, key): the move-key list that walks the walker to cell `key` over his CURRENT legal // domain (mask-aware: a gate he cannot afford is a wall, exactly as _parkLegal sees it). Oracle- // independent — no persona symbol anywhere near it. function _y14Route(P, target) { const st = P.st, n = st.N, src = E._parkKey(st, st.pos[0]); if (src === target) return []; const prev = new Map([[src, null]]), q = [src]; const pass = (kk) => { if (st.wall.has(kk)) return false; const m = E.PARK_FIELD_MECHS[st.park.fieldMech]; return !(m.legalMask && m.legalMask(P, kk, 'me')); }; for (let h = 0; h < q.length; h++) { const kk = q[h]; if (kk === target) break; const x = kk % n, y = (kk / n) | 0; for (const m of [{ k: 'U', x: 0, y: -1 }, { k: 'D', x: 0, y: 1 }, { k: 'L', x: -1, y: 0 }, { k: 'R', x: 1, y: 0 }]) { const nx = x + m.x, ny = y + m.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (prev.has(nk) || !pass(nk)) continue; prev.set(nk, { from: kk, k: m.k }); q.push(nk); } } if (!prev.has(target)) return null; const out = []; for (let cur = target; prev.get(cur); cur = prev.get(cur).from) out.unshift(prev.get(cur).k); return out; } // _y14To(P, target): walk there (one parkStep per key; the companion advances as it always does). function _y14To(P, target) { const route = _y14Route(P, target); assert.ok(route, 'no legal route to the target cell'); for (const k of route) E.parkStep(P, k); return P; } // _y14NeighbourOf(P, key): the cell the walker must stand on to pay — the SOUTH approach cell of // the gate (the walker spawns south of the hedge; the gate's other neighbour is the north bank). function _y14NeighbourOf(P, key) { const st = P.st, n = st.N, x = key % n, y = (key / n) | 0; const cands = [[x, y + 1], [x, y - 1], [x - 1, y], [x + 1, y]] .filter(([cx, cy]) => cx > 0 && cy > 0 && cx < n - 1 && cy < n - 1 && !st.wall.has(cy * n + cx)) .map(([cx, cy]) => cy * n + cx) .filter(kk => _y14Route(P, kk)); assert.ok(cands.length, 'the gate has no reachable neighbour'); return cands[0]; } // _y14Gate(P): THE PAYING MOVE. Off the gate it is the step INTO it; ON the gate it is the // in-place payment — the walker moves into the cell he already stands on ('stay'). There is no // pay button and no new input key: paying is walking into the gate. function _y14Gate(P) { const st = P.st, gate = st.park.toll.gate, n = st.N, cur = E._parkKey(st, st.pos[0]); if (cur === gate) return 'stay'; const gx = gate % n, gy = (gate / n) | 0, p = st.pos[0]; const d = [['U', 0, -1], ['D', 0, 1], ['L', -1, 0], ['R', 1, 0]].find(([, dx, dy]) => p.x + dx === gx && p.y + dy === gy); assert.ok(d, 'the walker is not adjacent to the gate'); return d[0]; } // Y14-TOLL-PAY (the brief's step 1, adapted to the seam): entering the gate deducts exactly one // gem from the GAUGE and opens the gate FOR THE WALKER ONLY. The companion's lane stays shut, so // his contracted gem is unreachable and his plan is STUCK (not arrived) — he holds his contract. test('Y14-TOLL-PAY: entering the gate deducts one gem and opens it for the walker alone', () => { const st = E._parkTollBuild(E._parkTollCell(5)); const P = E.parkStart(st); const park = P.st.park, gate = park.toll.gate; assert.strictEqual(park.toll.price, 1, 'the toll is one gem'); assert.strictEqual(park.dyn.gateOpen.me, false, 'a fresh board has a shut gate'); assert.strictEqual(park.dyn.gateOpen.mate, false, 'a fresh board has a shut mate lane'); _y14To(P, _y14NeighbourOf(P, gate)); // up to the cell BEFORE the gate // trig = the whole board, so his intent is public from his first beat: he is CONTRACTED to a gem // on the far side of the hedge, he refuses the deep gap, and the gate is shut to him — so his // plan is STUCK. Not arrived (seam guarantee 1): he holds his contract and waits for the toll. const plan0 = E._parkCompanionPlan(P); assert.ok(plan0 && plan0.stuck === true, 'with the gate shut the companion must be STUCK (not arrived, not walking)'); assert.strictEqual(P.contract, 0, 'the stuck companion must keep his contract (never retire)'); const g0 = P.st.score[0]; assert.ok(g0 >= park.toll.price, 'the walker must reach the gate with a gem to spend (the purse gems are on his route)'); const h0 = P.hearts; E.parkStep(P, _y14Gate(P)); // walk INTO the gate = pay assert.strictEqual(E._parkKey(P.st, P.st.pos[0]), gate, 'the paying move did not land the walker on the gate'); assert.strictEqual(P.st.score[0], g0 - 1, `the gate entry must deduct exactly one gem (${g0} -> ${P.st.score[0]})`); assert.strictEqual(P.hearts, h0, 'the gate costs a GEM, never a heart — the two gauges are separate channels'); assert.strictEqual(park.dyn.gateOpen.me, true, 'the toll did not open the gate for the walker'); assert.strictEqual(park.dyn.gateOpen.mate, false, 'mate lane still closed'); const plan1 = E._parkCompanionPlan(P); assert.ok(plan1 && plan1.stuck === true, 'the walker\'s own toll must not open the companion\'s lane'); }); // Y14-TOLL-MATE (the brief's step 1, second body): the SECOND, IN-PLACE payment (standing on the // gate, moving into it again) buys the COMPANION's lane — and his planner immediately re-routes // THROUGH the gate. This is the move that buys the walker nothing at all. test('Y14-TOLL-MATE: the second in-place toll opens the mate lane and re-plans the companion through the gate', () => { const st = E._parkTollBuild(E._parkTollCell(5)); const P = E.parkStart(st); const park = P.st.park, gate = park.toll.gate; _y14To(P, _y14NeighbourOf(P, gate)); E.parkStep(P, _y14Gate(P)); // first toll (walk in) const g1 = P.st.score[0]; assert.ok(g1 >= park.toll.price, 'the walker must still hold a gem for the companion\'s toll'); E.parkStep(P, _y14Gate(P)); // second toll (in-place: 'stay') assert.strictEqual(P.st.score[0], g1 - 1, 'the second toll must deduct a second gem'); assert.strictEqual(park.dyn.gateOpen.mate, true, 'the in-place payment did not open the mate lane'); const plan = E._parkCompanionPlan(P); assert.ok(plan && !plan.stuck, 'the companion is still stuck after his toll was paid'); assert.ok(plan.path.some(k => k === park.toll.gate), 'companion routes via gate'); assert.strictEqual(P.st.score[0], 0, 'both tolls spent the purse'); // and the purse is spent, but the GOAL LINE never moved: the chain is what completes the run assert.ok(P.dest < park.chain.length && !P.over, 'paying must not complete (or fail) the run — the goal threshold is unchanged'); }); // Y14-TOLL-WALL: an empty purse makes the gate a WALL — for the walker (it leaves his legal set // and his route metric) and, until the second toll, for the companion. Board geometry is never // touched: the gate is a walkway cell, so the wall is a MASK the mechanic owns. test('Y14-TOLL-WALL: a short purse makes the gate a wall; a paid gate never charges the walker twice', () => { const P = E.parkStart(E._parkTollBuild(E._parkTollCell(5))); const st = P.st, park = st.park, gate = park.toll.gate; st.score[0] = 0; // empty the gauge const m = E.PARK_FIELD_MECHS.toll; assert.ok(m.legalMask(P, gate, 'me'), 'a gate the walker cannot pay for must be impassable to him'); assert.ok(m.legalMask(P, gate, 'route'), 'an unaffordable gate must carry no route'); assert.ok(m.legalMask(P, gate, 'mate'), 'the gate is shut to the companion until HIS toll is paid'); assert.ok(!st.wall.has(gate), 'the gate must be a WALKWAY cell — the wall is a mask, never board geometry'); st.score[0] = 2; assert.ok(!m.legalMask(P, gate, 'me'), 'an affordable gate is passable'); // pay once, then leave and come back: the walker is never charged twice for his own lane _y14To(P, _y14NeighbourOf(P, gate)); E.parkStep(P, _y14Gate(P)); const g1 = P.st.score[0]; const back = E._parkLegal(P).find(c => c.k !== 'stay' && c.key !== gate); E.parkStep(P, back.k); // step OFF the gate E.parkStep(P, _y14Gate(P)); // and back ON assert.strictEqual(P.st.score[0], g1, 'the walker was charged twice for a gate he had already opened'); assert.strictEqual(park.dyn.gateOpen.mate, false, 'walking back in must NOT buy the mate lane (only the IN-PLACE toll does)'); }); // Y14-TOLL-READS: the three-way junction, read through the SHIPPED three minds. On the walker's // own faithful paths: G (goal) takes the field and eats the heart; C (safety) takes the gate and // keeps the body; N (care) prescribes the SECOND toll — the move that buys the walker nothing. // A real discrimination, not a vacuous set: on the gate with the mate lane shut, N's compliant set // must not equal the legal set. test('Y14-TOLL-READS: N prescribes the companion\'s toll; G/C are free to spend nothing', () => { let payScenes = 0, narrowed = 0, goalWades = 0, safeGates = 0; for (const seed of _Y14_SEEDS) { for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const P = E.parkStart(E._parkTollBuild(E._parkTollCell(seed))); const park = P.st.park; while (!P.over) { const onGate = E._parkKey(P.st, P.st.pos[0]) === park.toll.gate; if (onGate && park.dyn.gateOpen.me && !park.dyn.gateOpen.mate && P.st.score[0] >= park.toll.price) { const reads = E._parkReads(P); payScenes++; assert.ok(reads.atts.N.engaged, 'care is COLD while the companion\'s road is shut and you are standing on the toll'); assert.ok(reads.atts.N.pref.has('stay'), 'care does not prescribe the in-place toll that buys his lane'); if (reads.atts.N.pref.size < reads.legal.length) narrowed++; } E.parkStep(P, E.parkOracleMove(P, persona)); } if (persona[0] === 'goal' && P.deepEntries >= 1) goalWades++; if (persona[0] === 'safety' && P.deepEntries === 0) safeGates++; } } assert.ok(payScenes > 0, 'the toll decision never arose on any seed — the care read is vacuous'); assert.ok(narrowed === payScenes, 'the care read never NARROWED anything at the toll (it prescribes nothing)'); assert.strictEqual(goalWades, _Y14_SEEDS.length * 2, 'a goal-led persona must cut the field and pay the heart'); assert.strictEqual(safeGates, _Y14_SEEDS.length * 2, 'a safety-led persona must take the gate and keep the body'); console.log(` [Y14-TOLL-READS] ${payScenes} toll-decision states over 8 seeds x 6 personas (${narrowed} where care narrowed the set); goal-led wade ${goalWades}/16, safety-led gate ${safeGates}/16`); }); // Y14-TOLL-C1: the board is a pure function of the PUBLIC cell (no persona symbol reaches the // generator), deterministic, fresh per build (dyn is RUNTIME state), and the legacy boards are // untouched — the whole cell is opt-in behind cell.mech.fieldMech = 'toll'. test('Y14-TOLL-C1: board bytes persona-invariant, deterministic, fresh per build; legacy walk boards untouched', () => { const canon = (st) => JSON.stringify(st, (k, v) => v instanceof Set ? [...v].sort((a, b) => a - b) : v); for (const seed of [1, 3, 5, 7]) { const a = canon(E._parkTollBuild(E._parkTollCell(seed))); const stamped = { ...E._parkTollCell(seed), personaStream: ['goal', 'safety', 'care'] }; assert.strictEqual(canon(E._parkTollBuild(stamped)), a, `seed ${seed}: personaStream leaked into the board bytes`); E.parkPlayout(E._parkTollBuild(E._parkTollCell(seed)), E.PARK_PERSONAS[0]); assert.strictEqual(canon(E._parkTollBuild(E._parkTollCell(seed))), a, `seed ${seed}: a played instance corrupted the rebuild`); const st = E._parkTollBuild(E._parkTollCell(seed)); assert.ok(st.park.toll && st.park.toll.gate >= 0 && st.park.toll.price === 1, 'the board must carry park.toll = { gate, price }'); assert.strictEqual(st.park.dyn.gateOpen.me, false, 'a fresh build has a shut gate'); assert.strictEqual(st.park.dyn.gateOpen.mate, false, 'a fresh build has a shut mate lane'); assert.strictEqual(st.score[0], 0, 'a fresh build starts with an empty purse'); assert.ok(!st.park.deep.has(st.park.toll.gate), 'the gate is SAFE ground — its price is a gem, not a heart'); } const legacy = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); assert.ok(!legacy.park.toll && !legacy.park.dyn, 'a legacy walk board grew a toll/dyn field (byte stability broken)'); }); // Y14-TOLL-SHIP-GATE (P9/P10): the y14 cell ships as a LIVE picker tile ONLY on blind 6/6-persona // ORDER recovery. Same shape as the y12 gate: it PINS what the module clears (faithful completion // alive + the widened per-pair recovery of every EXPRESSED pair, in the demonstrated direction) and // REPORTS the order tally, and it asserts the module's DECLARED shippability against the measured // read — so the claim on the campaign slot header is a pin a regression must break. test('Y14-TOLL-SHIP-GATE: 6/6 faithful completes alive; posed pairs blind-recovered; order recovery measured', () => { let rec = 0, tot = 0, expr = 0, recPair = 0, misPair = 0; const posed = { GC: 0, GN: 0, CN: 0 }; for (const seed of _Y14_SEEDS) { const cell = E._parkTollCell(seed); const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(E._parkTollBuild(cell), p)); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i], P = playouts[i]; assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}, not complete`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: faithful playout died`); tot++; const r = E.parkRecoverOrder(E._parkTollBuild(cell), P.moves); if (r && r.join() === persona.join()) rec++; for (const pair of E.PARK_TOLL_PAIRS) { if (!(E.parkPairExpressed(E._parkTollBuild(cell), P.moves, pair) > 0)) continue; expr++; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); if (E.parkRecoverPairLex(E._parkTollBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } } assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a read flaw, not a posing gap)`); // THE DEMO-ONLY CLAIM, PINNED (review, minor 3). Reporting rec and asserting nothing left the // module's whole ship verdict resting on ONE seed. This is the tripwire: it fires the day someone // actually poses C-N on this geometry (which would be GOOD news — and it must not land silently). assert.strictEqual(rec, 0, `blind ORDER recovery is now ${rec}/${tot} (was 0): a pair that was never posed is being posed — re-measure the sweep and RE-GATE the slot (ship:true is earned by 6/6, and this may now earn it)`); assert.strictEqual(posed.CN, 0, `C-N is now EXPRESSED (${posed.CN}) — the standing gap may be closed on this cell; re-measure`); assert.strictEqual(E._parkTollRecovers(E._parkTollCell(E.PARK_TOLL_SHIP_SEED)), E.PARK_TOLL_SHIPPABLE, 'the module\'s declared shippability disagrees with the blind 6/6 order-recovery read on the shipped seed'); // THE SHIPPED PATH, ACTUALLY DRIVEN (review rounds 1+2). A FIELD module has NO generator: the // campaign reaches it through the REGISTRY — `admits` for the seed sweep and `parkFieldBuild` -> // `build` for the board. So drive exactly those two, and nothing that only a test would call. // (History, so it is not re-introduced: this block first asserted parkTollGenFallbacks() === 0 // without ever calling the module generator — VACUOUS, since the counter only moved inside it. // The repair was not to call the generator from here: that would have MANUFACTURED THE ONLY // CALLER and produced a green assertion about nobody's code. The generator was a fossil copied // from the VERB modules, which the campaign really does call directly. It is gone; the reject // telemetry moved nowhere, because it always hung off `admits`.) const m = E.PARK_FIELD_MECHS.toll; for (const seed of _Y14_SEEDS) { const cell = m.cell(seed); assert.ok(m.admits(cell), `seed ${seed}: the campaign's own admission predicate REJECTS this cell (rejects: ${JSON.stringify(E.parkTollWhys())})`); // FRESH BOARD PER BUILD — the replay-safety property the whole recovery stack rides on: every // read above rebuilds the board, and a board that came back PLAYED (gate open, purse spent) // would silently poison every one of them. const st = E.parkFieldBuild(cell); assert.strictEqual(st.park.fieldMech, 'toll', `seed ${seed}: parkFieldBuild did not build a toll board`); assert.strictEqual(st.park.dyn.gateOpen.me, false, `seed ${seed}: parkFieldBuild handed back a PLAYED board (it must be fresh each call)`); assert.strictEqual(st.score[0], 0, `seed ${seed}: parkFieldBuild handed back a board with a spent purse`); } // THE LOUD COUNTER (and it hangs off `admits`, the hook the campaign genuinely calls). All five // reject reasons must be 0: every swept seed is admissible at candidate 0. assert.deepStrictEqual(E.parkTollWhys(), { complete: 0, dead: 0, sig: 0, unexpressed: 0, norec: 0 }, 'the toll admission predicate REJECTED a swept cell (the LOUD reject tallies must stay 0)'); console.log(` [Y14-TOLL-SHIP-GATE] 8 seeds x 6 personas: ${tot}/${tot} faithful completes alive; blind ORDER recovery ${rec}/${tot}; per-pair widened ${recPair}/${expr} expressed (mis-read ${misPair}); posed G-C ${posed.GC} G-N ${posed.GN} C-N ${posed.CN}; registry admits 8/8 swept cells (rejects ${JSON.stringify(E.parkTollWhys())}), every build fresh; ship seed ${E.PARK_TOLL_SHIP_SEED} shippable=${E.PARK_TOLL_SHIPPABLE}`); }); /* ================= y3 DOWNED-COMPANION FIELD CELL (Task 3) ================= The companion starts COLLAPSED inside the meadow (a park.deep cell) and does not walk. Walking INTO him from an adjacent cell ASSISTS him — one turn — after which he stands and resumes his ordinary contract. Left alone he CRAWLS one cell toward the bank every `crawlEvery` beats and picks himself up when he reaches it: the self-recovery CEILING, so no persona can be wedged by a companion who never rises. The junction the cell poses, read as a lexicographic combination of the SHIPPED three minds: N care dive in — close the distance to the downed man (and the meadow charges your body) C safety keep to the edge — the caution band forbids the meadow, so C can never assist G goal ignore him — the beeline to the gems runs straight across the meadow The whole cell rides the registry (PARK_FIELD_MECHS.downed): the assist is legalAdd('me') — the engine's own rule is that the companion's cell is NEVER a destination, and only an ADDITIVE legality can re-open it. The freeze is legalMask('mate'); the crawl is tick(); the care read is reads.N. Not one line of the engine body knows y3 exists. */ // _parkY3AssistMove(P): the move key that steps INTO the downed companion, or null. Test-only // driver over the module's own PUBLIC read (E._parkDownedAssist) — never a second implementation // of the rule under test. const _parkY3AssistMove = (P) => E._parkDownedAssist(P); // _parkY3WalkAdjacent(P): drive the WALKER (not the oracle — this is a physics fixture, and a // persona would be a C1 leak into a test that must hold for all six) to a cell adjacent to the // downed companion, greedily over the legal set, never stepping onto him. Returns true on arrival. function _parkY3WalkAdjacent(P, cap) { const md = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); for (let i = 0; i < (cap || 40) && !P.over; i++) { if (md(P.st.pos[0], P.st.pos[1]) === 1) return true; let best = null, bd = Infinity; for (const c of E._parkLegal(P)) { if (c.add) continue; // never take the assist itself: that is the move under test const d = md(c, P.st.pos[1]); if (d < bd) { bd = d; best = c.k; } } if (!best) return false; E.parkStep(P, best); } return md(P.st.pos[0], P.st.pos[1]) === 1; } // Y3-DOWNED-ASSIST: the cell's physics. The companion is FROZEN where he lies until an ADJACENT // assist; the assist is ONE turn; it is an ACTION, not a step (the walker reaches the fallen man, // he does not stand on him); and it DECLINES the deep-entry charge parkStep prices against the // cell the move pointed at (the N1 decision — see the module header). test('DOWNED-ASSIST: companion frozen until an adjacent assist; the assist is one turn, and an ACTION not a step', () => { const st = E._parkDownedBuild({ seed: 11 }); const P = E.parkStart(st); const at0 = { x: P.st.pos[1].x, y: P.st.pos[1].y }; assert.ok(P.st.park.deep.has(E._parkKey(P.st, at0)), 'the downed companion must lie IN the meadow (a park.deep cell)'); assert.strictEqual(P.st.park.dyn.downed.rescued, false, 'a fresh board starts with the companion down'); // (1) he does not walk. (Two beats: below crawlEvery, so not even the crawl has fired.) E.parkStep(P, 'R'); E.parkStep(P, 'R'); assert.ok(P.st.pos[1].x === at0.x && P.st.pos[1].y === at0.y, 'the downed companion walked'); assert.strictEqual(P.contract, 0, 'the frozen companion advanced his contract (STUCK was read as ARRIVED)'); assert.strictEqual(E._parkCompanionPlan(P).stuck, true, 'a frozen companion must report plan.stuck (not arrived)'); // (2) the assist. Walk adjacent; the companion's cell is normally NOT a destination, and only // legalAdd re-opens it — flagged `add`, and only while he is down. assert.ok(_parkY3WalkAdjacent(P), 'never reached a cell adjacent to the downed companion'); assert.strictEqual(P.st.park.dyn.downed.rescued, false, 'he self-recovered before the walker arrived (the crawl ceiling is too fast)'); const mv = _parkY3AssistMove(P); assert.ok(mv, 'no assist move offered from an adjacent cell'); const cand = E._parkLegal(P).find(c => c.k === mv); assert.strictEqual(cand.add, true, 'the assist is not a force-opened (legalAdd) candidate'); assert.strictEqual(cand.key, E._parkKey(P.st, P.st.pos[1]), 'the assist does not point at the companion cell'); // CARE MUST NOT GO SILENT AT THE TARGET. reads.N is a "close the distance to him" preference, and // that shape is structurally EMPTY once you are already adjacent — no ordinary move reduces a // distance of 1. An empty prefer() is INERT, not a veto (contract trap 1), so care would drop out // of the decision at the exact moment it is supposed to speak, and the walker would stroll past a // man he is standing next to. y3 survives this ONLY because legalAdd force-opens the companion's // own cell: the assist is itself a candidate, at distance 0, so it IS the closing move and care's // set is non-empty precisely when it matters. That is a load-bearing consequence of the hook this // cell forced into the seam, and it is exactly the kind of thing that rots silently — so pin it. const rdAdj = E._parkReads(P); assert.ok(rdAdj.atts.N.engaged, 'care went cold standing next to the fallen man'); assert.ok(rdAdj.atts.N.pref.size > 0, 'care\'s compliant set is EMPTY at adjacency — a "close the distance" prefer() goes silent at the target, and an empty prefer is INERT, not a veto (trap 1)'); assert.ok(rdAdj.atts.N.pref.has(mv), 'the ASSIST is not in care\'s compliant set — the one move that helps him is not a move care prefers'); const stand = { x: P.st.pos[0].x, y: P.st.pos[0].y }; const h0 = P.hearts, de0 = P.deepEntries, t0 = P.turns, fx0 = P.st.fx.length; E.parkStep(P, mv); assert.strictEqual(P.st.park.dyn.downed.rescued, true, 'the assist did not revive him'); assert.strictEqual(P.st.park.dyn.downed.by, 'assist', 'the rescue was not credited to the walker'); assert.strictEqual(P.turns, t0 + 1, 'the assist did not take exactly one turn'); // AN ACTION, NOT A STEP: he reaches the fallen man; he does not stand on him. assert.ok(P.st.pos[0].x === stand.x && P.st.pos[0].y === stand.y, 'the walker STOOD ON the companion (onEnter must restore st.pos[0])'); assert.deepStrictEqual(P.path[P.path.length - 1], stand, 'the trace claims the walker walked onto the companion'); // the assist itself costs no body. (Here the walker is standing IN the meadow — he waded in to // reach him — so parkStep's deep charge never even fires: it prices a nonDeep->deep TRANSITION and // he is already deep. The heart this rescue cost was charged at the BANK, on the way in. The other // arm — assisting FROM the bank, where the charge DOES fire and the module declines it — is N1, and // it is pinned in its own test below.) assert.strictEqual(P.hearts, h0, 'the assist charged a heart'); assert.strictEqual(P.deepEntries, de0, 'the assist bumped the deep-entry meter'); assert.ok(!P.st.fx.slice(fx0).some(f => f.k === 'deep'), 'the assist flashed a deep-entry fx'); // (2b) and the wade REALLY DID cost him: care is paid for by the path, not by the assist. assert.strictEqual(P.deepEntries, 1, 'the walker reached a man lying in the meadow without ever entering it'); assert.strictEqual(P.hearts, 3 - P.st.park.damage, 'the wade into the meadow charged no heart'); // (3) he stands: the freeze lifts and he resumes his ORDINARY contract (out of the meadow first — // legalAdd('mate') opens his limp home; he never re-enters the field once he is on the bank). const co0 = { x: P.st.pos[1].x, y: P.st.pos[1].y }; for (let i = 0; i < 8 && !P.over; i++) E.parkStep(P, 'stay'); assert.ok(P.st.pos[1].x !== co0.x || P.st.pos[1].y !== co0.y, 'the rescued companion never got up'); assert.ok(!P.st.park.deep.has(E._parkKey(P.st, P.st.pos[1])) || E._parkCompanionPlan(P).next, 'the rescued companion is stranded in the meadow (his walk-out lane never opened)'); // and the assist is not a repeatable button: once he is up, his cell is shut again assert.strictEqual(_parkY3AssistMove(P), null, 'the assist is still on offer after the rescue'); }); // Y3-DOWNED-N1: THE SHARP EDGE, AND THE DECISION TAKEN ON IT. parkStep prices `ev.to` against // park.deep BEFORE the hooks run, and it prices where the move POINTED — not where the walker ends // up. So an assist aimed at a man lying on a meadow cell is charged ♥-1 and deepEntries++ even // though onEnter puts the walker straight back and he never stands there. (Seam contract N1, and // this cell is why the note exists.) // // THIS MODULE DECLINES THE CHARGE. The test drives the arm where it actually fires — the walker on // the BANK, the man on the meadow cell beside it (which is exactly the board after he has crawled to // the lip) — because from inside the field the transition never happens and the branch would go // untested. Both arms must come out at the same price: the assist costs the body NOTHING, and what // the rescue costs is the WADE, charged at the bank by the ordinary universal physics like anyone // else's shortcut. The two reasons this is not cosmetic: // - deepEntries is a meter the SIGNATURE and ADMISSIBILITY gates reason from, and it means // "costly entries into the hazard field". The walker does not enter that cell. // - absorbed, the charge would fire INCONSISTENTLY: 0 from inside the meadow (already deep), 1 // from the bank. The same act of kindness priced differently depending on how long the man had // been lying there. That is an artefact, not a design. test('DOWNED-N1: the assist from the BANK declines the deep-entry charge parkStep already fired', () => { const st = E._parkDownedBuild({ seed: 11 }); const P = E.parkStart(st); const park = st.park, n = st.N; // the LIP: a meadow cell one step from the bank (out === 1), with a passable non-deep neighbour. // This is the board after THREE crawls (a fourth would put him ON the bank, where he stands up by // himself and there is nothing left to assist). Reached here by CONSTRUCTION rather than by playing // the schedule, so the test pins the PHYSICS and not one seed's crawl timing — but the state is a // real one: on the shipped boards he sits at out === 1 for beats 12..15, which is exactly when a // human player walking the promenade can reach down from the verge and fire this branch. let lip = -1, bank = -1; for (let kk = 0; kk < n * n && lip < 0; kk++) { if (!park.deep.has(kk) || park.downed.out[kk] !== 1) continue; const x = kk % n, y = (kk / n) | 0; for (const d of E.DIRS) { const nk = (y + d.y) * n + (x + d.x); if (st.wall.has(nk) || park.deep.has(nk) || park.downed.out[nk] !== 0) continue; lip = kk; bank = nk; break; } } assert.ok(lip >= 0, 'no meadow cell at the lip of the bank — the N1 arm is unreachable on this board'); st.pos[1] = { x: lip % n, y: (lip / n) | 0 }; // the man, at the edge of the field st.pos[0] = { x: bank % n, y: (bank / n) | 0 }; // the walker, on firm ground beside him P.path = [{ x: st.pos[0].x, y: st.pos[0].y }]; assert.ok(!park.deep.has(bank) && park.deep.has(lip), 'fixture: the walker must stand OFF the field and the man ON it'); const mv = E._parkDownedAssist(P); assert.ok(mv, 'no assist offered from the bank beside him'); const stand = { x: P.st.pos[0].x, y: P.st.pos[0].y }; const h0 = P.hearts, de0 = P.deepEntries, fx0 = st.fx.length; E.parkStep(P, mv); assert.strictEqual(P.st.park.dyn.downed.rescued, true, 'the assist from the bank did not revive him'); assert.ok(P.st.pos[0].x === stand.x && P.st.pos[0].y === stand.y, 'the walker stood on the companion'); // THE DECISION, pinned: the charge fired inside parkStep, and the module took it back off. assert.strictEqual(P.hearts, h0, 'N1: reaching down from firm ground cost a heart for a cell the walker never enters (the charge was absorbed, not declined)'); assert.strictEqual(P.deepEntries, de0, 'N1: the deep-entry METER counted an entry that never happened — the signature and admissibility gates read this'); assert.ok(!st.fx.slice(fx0).some(f => f.k === 'deep'), 'N1: the declined charge still flashed the deep-entry fx'); // and the walker is still standing on the bank, so the trace agrees with the meters assert.ok(!park.deep.has(E._parkKey(st, P.st.pos[0])), 'the walker ended the assist inside the field'); }); // Y3-DOWNED-CRAWL: THE CEILING. A companion nobody helps must not be able to wedge the board: he // drags himself one cell toward the bank every `crawlEvery` beats and picks himself up when he // reaches it. Deterministic (the beat, never a clock or an rng draw at read time). test('DOWNED-CRAWL: ignored, he crawls to the bank on the beat and self-recovers (the no-deadlock ceiling)', () => { const st = E._parkDownedBuild({ seed: 11 }); const P = E.parkStart(st); const every = st.park.downed.crawlEvery; assert.ok(every >= 2, 'crawlEvery must be a real period'); const seat = { x: P.st.pos[1].x, y: P.st.pos[1].y }; const out0 = st.park.downed.out[E._parkKey(st, seat)]; assert.ok(out0 >= 1, 'the seat must be a real distance from the bank, or there is no crawl to schedule'); // he does not move OFF the beat... for (let i = 0; i < every - 1 && !P.over; i++) E.parkStep(P, 'stay'); assert.ok(P.st.pos[1].x === seat.x && P.st.pos[1].y === seat.y, 'he crawled early (the schedule is not on the beat)'); // ...and he moves ON it, strictly toward the bank E.parkStep(P, 'stay'); const out1 = st.park.downed.out[E._parkKey(st, P.st.pos[1])]; assert.strictEqual(out1, out0 - 1, 'the crawl did not take him one cell closer to the bank'); assert.strictEqual(P.st.park.dyn.downed.crawl, 1, 'the crawl counter did not advance'); assert.strictEqual(P.st.park.dyn.downed.rescued, false, 'a crawl is not a recovery'); // left alone he reaches the bank and STANDS — the ceiling. He is then an ordinary companion. for (let i = 0; i < every * (out0 + 2) && !P.over && !P.st.park.dyn.downed.rescued; i++) E.parkStep(P, 'stay'); assert.strictEqual(P.st.park.dyn.downed.rescued, true, 'an ignored companion never recovered — the board can deadlock'); assert.strictEqual(P.st.park.dyn.downed.by, 'self', 'a self-recovery was credited to the walker'); assert.ok(!P.st.park.deep.has(E._parkKey(P.st, P.st.pos[1])), 'he "recovered" without ever reaching the bank'); assert.ok(!E._parkCompanionPlan(P).stuck, 'the recovered companion is still frozen'); }); // Y3-DOWNED-READS: the THREE-WAY JUNCTION, read through the shipped G/C/N and nothing else. The // care facet ENGAGES on the downed man (a mind the walk board leaves cold out here) and NARROWS // care to the moves that close the distance to him — including the assist itself. The point of the // cell is that at the meadow's lip C and N are DISJOINT: safety forbids the very step care demands. // (That C-N disjunction is the pair y12 could never pose — see its module header.) test('DOWNED-READS: care engages on the downed man and narrows to the closing moves; C and N go disjoint at the lip', () => { let lips = 0, cnDisjoint = 0, gnDisjoint = 0, seen = 0, gChoice = 0; for (const seed of [11, 12, 13, 14, 15, 16]) { const P = E.parkStart(E._parkDownedBuild({ seed })); // the shipped care attitude is COLD at spawn (no contested gem, nobody blocked) — so the // engagement below is the mechanic's, and the OR is not vacuous. assert.ok(!E.PARK_ATTITUDES.N.engaged(P), `seed ${seed}: precondition — the walk board's own care read must be cold at spawn`); const rd = E._parkReads(P); assert.ok(rd.atts.N.engaged, `seed ${seed}: care did not engage on a downed companion`); const md = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); const d0 = md(P.st.pos[0], P.st.pos[1]); for (const c of E._parkLegal(P)) { if (!rd.atts.N.pref.has(c.k)) continue; assert.ok(md(c, P.st.pos[1]) < d0, `seed ${seed}: care admitted a move that does not close on the companion`); } // walk the care-led path and read the two junctions it must cross. They live in DIFFERENT places // and that is the design, not an accident: // C-N on the PROMENADE, at the square where he is straight in — care's only closing move is // the step into the field, which is the one step the caution band strikes out. (Inside the // meadow C has nothing to say at all: every neighbour is deep, so its compliant set is // empty and the mind goes inert. The disjunction can only be posed at the lip.) // G-N in the MEADOW, while he drags himself WEST and the gems are east — following him costs // goal progress, move by move. This is what makes care>goal readable, and it is why the // crawl runs away from the errand rather than across it. const live = (a) => a.engaged && a.pref && a.pref.size > 0; for (let i = 0; i < 40 && !P.over; i++) { const r = E._parkReads(P); if (live(r.atts.N)) { seen++; if (live(r.atts.C)) { lips++; if (![...r.atts.N.pref].some(k => r.atts.C.pref.has(k))) { cnDisjoint++; // AND THE GOAL MIND MUST STILL HAVE A REAL CHOICE HERE. The reads are lexicographic: a // subordinate mind narrows by INTERSECTION, so under a dominant set of size 1 it has // nothing left to choose between and expresses nothing. If G collapses to a singleton at // the very square where C and N are disjoint, then for every G-led persona the C-vs-N // question is decided before care or safety is ever consulted — the junction is crushed // before it is read, and the sweep reports it as a failure of the MECHANIC when it is a // failure of the CORRIDOR. (This is not hypothetical: an earlier draw put the gems behind // the man, the no-greedy-backtrack rule struck out G's promenade move, G collapsed onto // care's own move, and C-N silently vanished for care>safety>goal on every seed — 5/6.) if (live(r.atts.G) && r.atts.G.pref.size >= 2) gChoice++; } } if (live(r.atts.G) && ![...r.atts.N.pref].some(k => r.atts.G.pref.has(k))) gnDisjoint++; } // drive the CARE-led persona: this is the cell's showpiece path (a care>* walker dives in) E.parkStep(P, E.parkOracleMove(P, ['care', 'goal', 'safety'])); if (P.st.park.dyn.downed.rescued) break; } } assert.ok(cnDisjoint > 0, 'C and N are never disjoint — the dive-vs-keep-your-distance junction is not posed at all'); assert.ok(gnDisjoint > 0, 'G and N are never disjoint — going to him never costs the errand anything, so care>goal is unreadable'); assert.ok(gChoice > 0, 'at every C-N junction the GOAL mind is a SINGLETON — a subordinate mind cannot express anything under a dominant set with no choices in it, so the C-vs-N distinction is crushed before it is ever read. The gems must lie BEYOND him and across, leaving goal INDIFFERENT between the step into the field and the step along the promenade.'); console.log(` [DOWNED-READS] 6 seeds on the care-led path: ${seen} states with care live (${lips} with safety live too); C-N disjoint ${cnDisjoint}, G-N disjoint ${gnDisjoint}, C-N junctions where G still has a real choice ${gChoice}`); }); // Y3-DOWNED-ESCAPABLE (the brief's escapability gate): EVERY persona's faithful playout finishes, // alive. The board must never deadlock — a downed companion is a scene, not a wall — and the crawl // ceiling is what guarantees it even for the personas whose safety mind forbids them to help. test('DOWNED-ESCAPABLE: every persona playout survives and finishes', () => { for (const seed of [11, 12, 13]) { for (const persona of E.PARK_PERSONAS) { const out = E.parkPlayout(E._parkDownedBuild({ seed }), persona); assert.ok(out.over, `seed ${seed} ${persona.join('>')}: never finished`); assert.strictEqual(out.reason, 'complete', `seed ${seed} ${persona.join('>')}: ended '${out.reason}', not complete`); assert.ok(out.hearts > 0, `seed ${seed} ${persona.join('>')}: died`); } } }); // Y3-DOWNED-C1: the board is a PURE function of the PUBLIC cell — a persona stamp never changes a // byte, rebuilds are deterministic, a PLAYED instance never corrupts a rebuild (the rescue and the // crawl live on the RUNTIME dyn, never on the seed-pure park.downed), and the legacy walk boards // are untouched. The crawl schedule is the BEAT, so there is no clock and no draw at read time. const _y3canon = (st) => JSON.stringify(st, (k, v) => v instanceof Set ? [...v].sort((a, b) => a - b) : v); test('DOWNED-C1: board bytes persona-invariant, deterministic, fresh per build; the seed-pure seat never moves', () => { for (const seed of [11, 12, 13, 14]) { const cell = E._parkDownedCell(seed); const a = _y3canon(E._parkDownedBuild(cell)); const withStream = { ...cell, personaStream: ['care', 'goal', 'safety'] }; assert.strictEqual(_y3canon(E._parkDownedBuild(withStream)), a, `seed ${seed}: personaStream leaked into the board bytes`); E.parkPlayout(E._parkDownedBuild(cell), E.PARK_PERSONAS[4]); // play one instance to the rescue assert.strictEqual(_y3canon(E._parkDownedBuild(cell)), a, `seed ${seed}: a played instance corrupted the rebuild`); const st = E._parkDownedBuild(cell); assert.strictEqual(st.park.fieldMech, 'downed', 'the board does not carry its registry id'); assert.ok(st.park.downed && st.park.downed.crawlEvery >= 2, 'no seed-pure downed spec on the board'); assert.strictEqual(st.park.downed.at, E._parkKey(st, st.pos[1]), 'the companion does not start on his seed-pure seat'); assert.ok(st.park.deep.has(st.park.downed.at), 'the seat is not in the meadow'); assert.strictEqual(st.park.dyn.downed.rescued, false, 'a fresh build starts rescued'); assert.strictEqual(st.park.dyn.downed.crawl, 0, 'a fresh build starts mid-crawl'); for (const kk of st.park.walkway) assert.ok(st.park.distDeep[kk] >= 2, 'THE PARK FRAME LAW: a walkway cell inside the caution band'); } const legacy = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); assert.ok(!legacy.park.downed && !legacy.park.dyn, 'a legacy walk board grew a downed/dyn field (byte stability broken)'); console.log(' [DOWNED-C1] 4 seeds: byte-identical across persona stamps, deterministic, fresh per build; frame law holds'); }); // Y3-DOWNED-SHIP-GATE (P9/P10): a y3 cell may ship as a LIVE picker tile ONLY if it clears BLIND // 6/6-persona ORDER recovery — every persona's own faithful play completes and parkRecoverOrder // (trajectory + a FRESH board; no persona symbol reaches it) reassembles exactly the demonstrated // order. Built out of the shipped recovery stack, never a parallel channel. // TASK 10 — MEASURED UNDER CALIBRATION, and it survives. The bar now reads at PARK_CAL_TURNS, the // same window the readout discards (P3a §4), because a bar that reads evidence the product throws // away is measuring a game nobody plays. That is what took y8's ship claim (6/6 -> 0/6). y3 does not // flinch: 144/144 uncalibrated, 144/144 CALIBRATED, and this gate now sweeps ALL 24 measured seeds // so the 144/144 on the module header is the number actually ASSERTED rather than a 48/48 sample of // it. y3 survives because its evidence is spread down the whole walk — its earliest award of any // kind is turn 3, so the calibration window costs it literally nothing — where y8 committed on move // one. y3 is now the ONLY park cell that has earned the bar. const _Y3_SEEDS = Array.from({ length: 24 }, (_, i) => 11 + i); // the full measured sweep, 11..34 test('DOWNED-SHIP-GATE: 144/144 blind order recovery UNDER CALIBRATION — the only cell that ships', () => { let rec = 0, recCal = 0, tot = 0, expr = 0, recPair = 0, misPair = 0, cn = 0; const pairs = [['G', 'C'], ['G', 'N'], ['C', 'N']]; for (const seed of _Y3_SEEDS) { const cell = E._parkDownedCell(seed); const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(E._parkDownedBuild(cell), p)); assert.ok(E._parkDownedSignature(playouts), `seed ${seed}: the field signature does not separate the personas`); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i], P = playouts[i]; assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}, not complete`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: faithful playout died`); tot++; const r = E.parkRecoverOrder(E._parkDownedBuild(cell), P.moves); if (r && r.join() === persona.join()) rec++; const rc = E.parkRecoverOrder(E._parkDownedBuild(cell), P.moves, E.PARK_CAL_TURNS); if (rc && rc.join() === persona.join()) recCal++; for (const pair of pairs) { if (!(E.parkPairExpressed(E._parkDownedBuild(cell), P.moves, pair) > 0)) continue; expr++; if (pair[0] === 'C') cn++; const expect = E._parkPushPairDir(persona, pair); if (E.parkRecoverPairLex(E._parkDownedBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } } assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw, not a posing gap)`); assert.ok(cn > 0, 'the C-N pair is never EXPRESSED — the dive-vs-distance junction the whole cell exists to pose is not being read'); // THE SHIP BAR, ASSERTED — not merely tallied and printed. y3 is the FIRST ship:true field cell, so // the 6/6 blind order-recovery claim IS the licence for the live picker tile and has to be a HARD // pin on every seed swept. (The y12 template this file inherited only logs `rec` and pins the // single-seed `_parkStonesRecovers === SHIPPABLE`. That is harmless there because y12 is ship:FALSE // — its pin is a NEGATIVE one. Inherited here unchanged it would have been a hole you could drive a // regression through: lose 6/6 on seeds 12..18 and this test would print a smaller number and still // pass. It is the ship bar; assert it.) assert.strictEqual(rec, tot, `blind ORDER recovery ${rec}/${tot} — a ship:true cell must reassemble the demonstrated order on EVERY persona of EVERY seed`); // THE BAR THAT ACTUALLY LICENSES THE TILE (Task 10): the CALIBRATED read — the one the product // makes. It must be 144/144 too. If this ever drops below `tot` while `rec` stays at `tot`, y3 has // become y8: a cell whose discrimination lives inside the calibration window, claiming a read the // readout cannot perform. In that case the slot comes DOWN to a preview; it is not the gate that // gets relaxed. assert.strictEqual(recCal, tot, `CALIBRATED blind ORDER recovery ${recCal}/${tot} (uncalibrated ${rec}/${tot}) — y3's ship:true is ` + `licensed by 6/6 at skip=${E.PARK_CAL_TURNS}, the window the readout discards, and by nothing weaker. ` + 'A gap between these two numbers is the y8 defect appearing on y3: DEMOTE THE SLOT, do not lower the bar.'); assert.strictEqual(E._parkDownedRecovers(E._parkDownedCell(E.PARK_DOWNED_SHIP_SEED)), E.PARK_DOWNED_SHIPPABLE, 'the module\'s declared shippability disagrees with the CALIBRATED blind 6/6 order-recovery read on the shipped seed'); assert.strictEqual(E.PARK_DOWNED_SHIPPABLE, true, 'y3 ships LIVE — if this is being flipped, the slot must be flipped with it'); // and the predicate that actually SELECTS the shipped board must be at least as strong as the claim // (see _parkDownedAdmissible: the ship bar is folded into admission — AT THE SAME CALIBRATION — so // the campaign cannot seat a board that plays but does not read. y3 folds because y3 SHIPS; y8 had // to unfold when it stopped shipping, or its every board would have gone inadmissible.) assert.ok(E._parkDownedAdmissible(E._parkDownedCell(E.PARK_DOWNED_SHIP_SEED)), 'the shipped seed is not admissible by the module\'s own campaign-facing predicate'); console.log(` [DOWNED-SHIP-GATE] ${_Y3_SEEDS.length} seeds x 6 personas: ${tot}/${tot} faithful completes alive; blind ORDER recovery ${rec}/${tot} uncalibrated / ${recCal}/${tot} CALIBRATED (skip=${E.PARK_CAL_TURNS} — the readout's own window; y3 is unmoved by it, y8 loses everything); per-pair widened ${recPair}/${expr} expressed (C-N ${cn}, mis-read ${misPair}); ship seed ${E.PARK_DOWNED_SHIP_SEED} shippable=${E.PARK_DOWNED_SHIPPABLE}`); }); /* ================== y8 ROLLING LOG (Task 4) — the BEAT-DRIVEN field cell ================== A log rolls down a lane on a fixed beat. Standing on the cell it rolls INTO is a BODY BLOCK: the walker takes the heart and the log is stunned. The companion's planner treats the log's current cell as a temporary wall, and the lane's last cell is the ONE gap in the hedge — so a log that reaches it wedges his only corridor for good (plan.stuck: he holds, he never retires). Registered entirely through PARK_FIELD_MECHS.log (build/legalMask/tick/reads/cell/admits); the engine body is not touched (REGISTRY-SEAM-BODY proves it). */ const _Y8_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; // _parkY8StandOnLane(P): drive the walker (through the LEGAL move set, never by teleporting him // — the whole cell is about a body being somewhere) onto the cell the log will roll into next, // and leave him there with the log LIVE (un-stunned), so the caller's next `period` beats contain // exactly one roll and it lands on him. Test-only helper: it walks a BFS route over the walker's // own legal domain, re-aims every time the log rolls out from under the target, and — if it // happens to arrive on the very beat the roll lands (which body-blocks then and there) — waits out // the stun rather than handing the caller a log that cannot roll. Returns false if the log reached // the gap before the walker got into position. function _parkY8StandOnLane(P) { const st = P.st, park = st.park, n = st.N; for (let guard = 0; guard < 200 && !P.over; guard++) { const L = park.dyn.ents[0]; if (L.i + 1 >= park.log.lane.length) return false; // the log has parked: no block cell left const target = park.log.lane[L.i + 1]; const here = E._parkKey(st, st.pos[0]); if (here === target && L.stunned === 0) return true; // in position, body in the way, log live if (here === target) { E.parkStep(P, 'stay'); continue; } // hold while it shakes the last hit off // BFS over the walker's LEGAL domain (the log's own cell is a wall for him) back to `here` const prev = new Map([[target, null]]); const q = [target]; let step = null; for (let h = 0; h < q.length && step === null; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const d of [{ x: 0, y: -1, k: 'D' }, { x: 0, y: 1, k: 'U' }, { x: -1, y: 0, k: 'R' }, { x: 1, y: 0, k: 'L' }]) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (prev.has(nk) || st.wall.has(nk) || nk === park.log.lane[L.i]) continue; prev.set(nk, d.k); // d.k = the move that walks nk -> kk if (nk === here) { step = d.k; break; } q.push(nk); } } if (step === null) return false; // no route to the block cell E.parkStep(P, step); } return false; } // Y8-LOG-PHYSICS (the brief's Step-1 test, adapted to the engine's real runtime surface: hearts // live on P, not st): (0) a fresh build FREEZES the log for PARK_LOG_CORRIDOR beats — the corridor // delay (the walker is in his neutral entry tube; the log holds the same k so the C-N scene re-poses // past the calibration span); (a) once thawed, the log advances exactly one lane cell per `period` // beats, and it does so while the walker STAYS — a beat-driven entity that froze whenever the walker // waited would not be a clock (this is why the seam has `tick`, and why tick fires on 'stay'); (b) // standing on the cell it rolls into costs ONE heart and STUNS it — the log does not advance that // roll. The heart is the PRICE of the caring move; the care READ is a separate channel (Y8-LOG-READS). test('Y8-LOG-PHYSICS: log freezes for the corridor, then rolls every period; body-block stuns it and costs one heart', () => { // (0) THE CORRIDOR FREEZE: a fresh build starts stunned for PARK_LOG_CORRIDOR beats and does NOT // advance during them (in faithful play the walker spends them descending his tube). const Pf = E.parkStart(E._parkLogBuild({ seed: 4 })); const if0 = Pf.st.park.dyn.ents[0].i; for (let t = 0; t < E.PARK_LOG_CORRIDOR; t++) E.parkStep(Pf, 'stay'); assert.strictEqual(Pf.st.park.dyn.ents[0].i, if0, 'the log advanced DURING the corridor freeze'); // (1) THE ROLL: once thawed, it advances exactly one lane cell per `period` beats, on 'stay' moves. const P = E.parkStart(E._parkLogBuild({ seed: 4 })); const log = () => P.st.park.dyn.ents[0]; for (let t = 0; t < E.PARK_LOG_CORRIDOR; t++) E.parkStep(P, 'stay'); // thaw the corridor freeze first const i0 = log().i; for (let t = 0; t < P.st.park.log.period; t++) E.parkStep(P, 'stay'); assert.strictEqual(log().i, i0 + 1, 'log advanced one cell after period beats (on stay moves)'); // (2) THE BODY-BLOCK: on a FRESH board — the walker descends his tube while the log is still frozen, // exactly as faithful play does, so the driver reaches the block cell in time — stand him on the // log's next cell and let the roll land on him. const Pb = E.parkStart(E._parkLogBuild({ seed: 4 })); const logb = () => Pb.st.park.dyn.ents[0]; assert.ok(_parkY8StandOnLane(Pb), 'could not put the walker on the log\'s next cell'); const h0 = Pb.hearts, blocks0 = Pb.st.park.dyn.blocks; for (let t = 0; t < Pb.st.park.log.period; t++) E.parkStep(Pb, 'stay'); assert.ok(Pb.hearts === h0 - 1 && logb().stunned > 0, 'block = hurt + stun'); assert.strictEqual(Pb.st.park.dyn.blocks, blocks0 + 1, 'the body-block was not tallied'); assert.strictEqual(Pb.st.park.log.lane[logb().i + 1], E._parkKey(Pb.st, Pb.st.pos[0]), 'the log rolled THROUGH the body it was supposed to be blocked by (it must NOT advance on a block)'); }); // Y8-LOG-READS: THE CRUX OF THE CELL. The care read must come from the PATH CHOICE (moving // upstream into the blocking formation), NEVER from counting hearts — ♥ and personality are // separate channels. So this test asserts on the compliant SETS, at states where they diverge: // N (care) prescribes the moves that close on the log's next cell (the interception), and // 'stay' when the walker is already standing in its way (hold the line). // C (safety) forbids exactly the cell the roll is about to land on. // On the roll step, with the walker on or beside the block cell, those two sets are DISJOINT — // which is the C-vs-N conflict scene the park has never had (the standing C-N gap y12 and x2 were // both retired for). G is left alone: the goal mind never sees the log at all. test('Y8-LOG-READS: N prescribes the blocking formation, C forbids the roll cell; the two are disjoint', () => { let scenes = 0, disjoint = 0, gFree = 0, cold = 0, nCold = 0; for (const seed of _Y8_SEEDS) { for (const persona of E.PARK_PERSONAS) { const P = E.parkStart(E._parkLogBuild({ seed })); while (!P.over) { const reads = E._parkReads(P); const ctx = reads.mctx; if (ctx && ctx.blockKey != null && reads.atts.N.engaged && reads.atts.C.engaged) { scenes++; const nSet = reads.atts.N.pref, cSet = reads.atts.C.pref; let inter = 0; for (const m of nSet) if (cSet.has(m)) inter++; if (nSet.size && cSet.size && inter === 0) disjoint++; // THE CARE SET IS THE APPROACH ITSELF: every move care prescribes STRICTLY closes on the // log's next cell (the assertion used to be `<=`, which passes against any implementation // that merely does not recede — it would have accepted a care read that stood still and // called it caring. `prefer` narrows with `<`; the test now says `<`). const here = E._parkKey(P.st, P.st.pos[0]); const holding = here === ctx.blockKey; for (const c of reads.legal) { if (!nSet.has(c.k)) continue; if (holding) { assert.strictEqual(c.k, 'stay', 'care let go of the block cell it was standing on'); continue; } assert.ok(ctx.dist[c.key] < ctx.dist[here], `seed ${seed}: care prescribed a move that does not close on the log's next cell (${c.k})`); } if (reads.atts.G.engaged && [...nSet].some(m => !reads.atts.G.pref.has(m))) gFree++; } // BOTH FACETS GATE THEIR OWN prefer() (review Important 1+2). _parkReads intersects a // mechanic's prefer whenever the COMBINED engagement is true — and the SHIPPED safety // attitude is engaged nearly everywhere on this board — so a prefer() that does not re-check // its own predicate runs cold and silently becomes a standing rule. Pinned, in the exact // states where it bites: where the mechanic's own facet is COLD, its prefer must be inert, // i.e. it must not remove a single legal move. if (ctx) { const legalKeys = reads.legal.map(c => c.k); if (!E._parkLogCLive(P, ctx)) { const cP = E.PARK_FIELD_MECHS.log.reads.C.prefer(P, reads.legal, ctx); for (const k of legalKeys) assert.ok(cP.has(k), `seed ${seed}: SAFETY's facet is COLD here (no roll landing) yet its prefer() still removed '${k}' — the single-state read has become a standing rule`); cold++; } if (!E._parkLogNLive(P, ctx)) { const nP = E.PARK_FIELD_MECHS.log.reads.N.prefer(P, reads.legal, ctx); for (const k of legalKeys) assert.ok(nP.has(k), `seed ${seed}: CARE's facet is COLD here yet its prefer() still removed '${k}' — cold care must say nothing, never collapse the mind onto 'stay'`); nCold++; } } E.parkStep(P, E.parkOracleMove(P, persona)); } } } assert.ok(scenes > 0, 'the blocking-formation scene never arose — the N read is vacuous'); assert.ok(disjoint > 0, 'C and N were never DISJOINT: the body-block scene poses no C-vs-N conflict, so the cell reads no better than y12'); assert.ok(gFree > 0, 'the goal mind never disagreed with the interception — the care detour is not a detour'); assert.ok(cold > 0, 'safety\'s facet was never COLD — the gate-your-own-prefer assertion above never ran, so it proves nothing'); // and the same non-vacuity guard on the CARE side, which is the half that was actually dangerous // (cold care used to collapse onto {'stay'}). If a future geometry made NLive always true, the // N-cold check would silently stop running and the landmine could be re-armed unnoticed. assert.ok(nCold > 0, 'care\'s facet was never COLD — the cold-care assertion above never ran, so it proves nothing'); console.log(` [Y8-LOG-READS] ${scenes} blocking-formation states over 8 seeds x 6 personas; ${disjoint} with C and N DISJOINT (the C-N poser); ${gFree} where G refuses the care move; ${cold}/${nCold} states where safety's/care's facet is cold and its prefer() correctly says nothing`); }); // Y8-LOG-C1 + ESCAPABILITY: the board is a pure function of the PUBLIC cell (persona stamps never // change a byte; a played instance never corrupts a rebuild — the roll state is RUNTIME dyn); the // legacy walk boards are untouched; and "legal" is not "survivable" — so every lane cell must have // a lateral step off it (a log that could corner the walker with no survivable move is a generator // bug), and every persona must finish ALIVE. const _y8canon = (st) => JSON.stringify(st, (k, v) => v instanceof Set ? [...v].sort((a, b) => a - b) : v); test('Y8-LOG-C1: board bytes persona-invariant, deterministic, fresh per build; every lane cell has a step OFF it', () => { for (const seed of _Y8_SEEDS) { const a = _y8canon(E._parkLogBuild(E._parkLogCell(seed))); const withStream = { ...E._parkLogCell(seed), personaStream: ['goal', 'safety', 'care'] }; assert.strictEqual(_y8canon(E._parkLogBuild(withStream)), a, `seed ${seed}: personaStream leaked into the board bytes`); E.parkPlayout(E._parkLogBuild(E._parkLogCell(seed)), E.PARK_PERSONAS[0]); // mutate one instance assert.strictEqual(_y8canon(E._parkLogBuild(E._parkLogCell(seed))), a, `seed ${seed}: a played instance corrupted the rebuild`); const st = E._parkLogBuild(E._parkLogCell(seed)); const park = st.park, n = st.N; assert.ok(park.log && park.log.lane.length > 3 && park.log.period === 3, 'the lane/period shape is wrong'); assert.strictEqual(park.dyn.ents.length, 1, 'exactly one log entity'); assert.strictEqual(park.dyn.ents[0].kind, 'log', 'the entity is not a log'); assert.strictEqual(park.dyn.ents[0].stunned, E.PARK_LOG_CORRIDOR, 'a fresh build freezes the log for the corridor (stunned = k), delaying its roll the same k the walker spends in the neutral entry tube'); // ESCAPABILITY: from EVERY lane cell there is a non-lane, non-wall neighbour to step onto, so // the walker can always refuse the hit. (The seam guarantees the legal set is never empty — // but 'legal' is not 'survivable', and only the GEOMETRY can guarantee that.) const lane = new Set(park.log.lane); for (const kk of park.log.lane) { const x = kk % n, y = (kk / n) | 0; const off = [[x - 1, y], [x + 1, y], [x, y - 1], [x, y + 1]].filter(([qx, qy]) => qx >= 0 && qy >= 0 && qx < n && qy < n && !st.wall.has(qy * n + qx) && !lane.has(qy * n + qx)); assert.ok(off.length > 0, `lane cell ${x},${y} is a dead end — the log could corner the walker there`); } for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E._parkLogBuild(E._parkLogCell(seed)), persona); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: died (${P.reason})`); assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: ${P.reason}, not complete`); } } const legacy = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); assert.ok(!legacy.park.log && !legacy.park.dyn, 'a legacy walk board grew a log/dyn field (byte stability broken)'); console.log(' [Y8-LOG-C1] 8 seeds: byte-identical across persona stamps, deterministic, fresh per build; every lane cell escapable; 48/48 faithful completes alive'); }); // Y8-LOG-SHIP-GATE (P9/P10) — NOW A POSITIVE PIN (redesign 2026-07-18). For one era (Task 10) this // was a NEGATIVE pin: y8 cleared the 6/6 blind ORDER-recovery bar UNCALIBRATED but 0/6 CALIBRATED, // because its discrimination lived in the first PARK_CAL_TURNS (=2) turns the readout discards. The // neutral-corridor rebuild MOVED that discrimination past the span: the walker now takes // PARK_LOG_CORRIDOR forced neutral steps down a walled vertical tube before his first real choice, // and the companion + log are delayed the same k, so the C-N body-block re-poses at turn 3+. y8 now // recovers 6/6 CALIBRATED on every seed, PARK_LOG_SHIPPABLE is TRUE, and this gate pins THAT. // WHAT IT ASSERTS: // rec0 === tot the UNCALIBRATED 6/6 recovery is real (still 6/6). // recCal === tot the CALIBRATED recovery is ALSO 6/6 now — the licence to ship. If this drops // the corridor regressed (an award slid back into the span); re-measure. // byPair.* === tot all THREE pairs still posed on every trajectory, C-N included (y8's real // contribution — a disjoint C-N poser — survives the rebuild). // misPair === 0 no expressed pair is ever read in the WRONG direction. // PARK_LOG_SHIPPABLE === true, and === the module's own _parkLogRecovers on the shipped seed. // Plus the care signature (the FRIEND's fate, never the walker's heart count) and the admission // predicate, which still ACCEPTS. NOTE: the slot flip (ship:true) and re-folding the ship bar into // admission are the INTEGRATION session's (campaign.js is off-limits to this branch). // It PRINTS the uncalibrated-vs-calibrated split per seed — now 6/6 -> 6/6 on every one. test('Y8-LOG-SHIP-GATE: 6/6 blind ORDER recovery SURVIVES calibration — the corridor earned y8 the ship bar', () => { let rec = 0, recCal = 0, tot = 0, expr = 0, recPair = 0, misPair = 0; const split = []; const byPair = { GC: 0, GN: 0, CN: 0 }; for (const seed of _Y8_SEEDS) { const cell = E._parkLogCell(seed); const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(E._parkLogBuild(cell), p)); let s0 = 0, sc = 0; for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i], P = playouts[i]; assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: faithful playout died`); tot++; // THE CARE SIGNATURE: the friend's fate, not the walker's heart count. const through = E._parkLogThrough(P); if (persona[0] === 'care') assert.ok(through, `seed ${seed} ${persona.join('>')}: care-led play left the friend wedged behind the log`); else assert.ok(!through, `seed ${seed} ${persona.join('>')}: the friend got home WITHOUT a care-led walker (the dilemma is fake)`); // THE SPLIT, measured on the same trajectory: the read the OLD ship bar took (skip 0) vs the // read the PRODUCT actually takes (skip PARK_CAL_TURNS — every judged episode discards it). const r = E.parkRecoverOrder(E._parkLogBuild(cell), P.moves); if (r && r.join() === persona.join()) { rec++; s0++; } const rc = E.parkRecoverOrder(E._parkLogBuild(cell), P.moves, E.PARK_CAL_TURNS); if (rc && rc.join() === persona.join()) { recCal++; sc++; } for (const pair of E.PARK_LOG_PAIRS) { if (!(E.parkPairExpressed(E._parkLogBuild(cell), P.moves, pair) > 0)) continue; expr++; byPair[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); if (E.parkRecoverPairLex(E._parkLogBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } split.push(`${seed}:${s0}/6->${sc}/6`); } assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw, not a posing gap)`); // THE UNCALIBRATED READ IS REAL — asserted, so the split below is a MEASUREMENT and not a story. assert.strictEqual(rec, tot, `uncalibrated blind ORDER recovery ${rec}/${tot} — this is the read y8's ship claim was built on ` + 'and it is still 6/6. If THIS breaks, the cell changed; the calibrated pin below is the one that ' + 'says whether it may ship.'); // AND IT SURVIVES CALIBRATION. THE PIN (positive): under the readout's own calibration, y8 STILL // recovers 6/6 — its discrimination now lands at turn 3+ (the neutral corridor pushed it past the // span). This is the licence it EARNED BACK. assert.strictEqual(recCal, tot, `CALIBRATED blind ORDER recovery is ${recCal}/${tot}, not ${tot} — y8 lost an order at ` + `skip=${E.PARK_CAL_TURNS}. The corridor is supposed to hold every decisive award at turn 3+; if ` + 'this dropped, an award slid back into the calibration span (a corridor regression). Re-run the ' + 'spike sweep and the seed 1..24 measurement; do not simply lower the bar.'); for (const k of ['GC', 'GN', 'CN']) assert.strictEqual(byPair[k], tot, `${k} posed on only ${byPair[k]}/${tot} trajectories — y8's C-N poser is its real contribution and it must not rot`); assert.strictEqual(recPair, expr, `${recPair}/${expr} expressed pairs recovered`); assert.strictEqual(E._parkLogRecovers(E._parkLogCell(E.PARK_LOG_SHIP_SEED)), E.PARK_LOG_SHIPPABLE, 'the module\'s declared shippability disagrees with the CALIBRATED blind order-recovery read on the shipped seed'); assert.strictEqual(E.PARK_LOG_SHIPPABLE, true, 'y8 has EARNED the ship bar (6/6 calibrated) — PARK_LOG_SHIPPABLE must be true. PROMOTED 2026-07-23: ' + 'the slot is now ship:true, and CAMP-SHIP-UNMOVED / CAMP-CROSS-SWEEP F4 pin the slot flag to THIS ' + 'module bar; the pairing bar (CAMP.PARK_Y8_SHIPPABLE, 24/24) is measured separately in campaign.js.'); // THE ADMISSION PREDICATE, exercised ON THE PATH THE CAMPAIGN ACTUALLY TAKES. There is no module // generator to drive any more (review round 2: a field mechanic's seed-walk is structurally // unreachable — the campaign sweeps `mech.cell` / `mech.admits` and builds through // `parkFieldBuild`, so a module-level `make…Task` has no caller but the test that invents one). // `_parkLogAdmissible` IS the shipped predicate, so it is what gets driven here — over the cells // `mech.cell` mints, exactly as campaign.js's `cand()` does — and its LOUD reject tallies are read // back afterwards. That is a counter about somebody's code. // // AND IT IS THE TASK-10 TRAP, NOW RESOLVED. y8 used to FOLD its ship bar into this predicate ("the // campaign cannot seat a board that plays but does not read" — right, for a cell that ships). When // the bar went to 0/6 under calibration a folded bar would reject EVERY candidate cell, the crossing // sweep would find nothing to seat, and the y8 tile would go DEAD on click — so while y8 was a // preview the bar was UNFOLDED (playability, the y12 pattern). PROMOTED 2026-07-23: the corridor // rebuild earned the calibrated bar back (6/6 every seed), so the bar is FOLDED IN AGAIN and these // asserts now verify the folded bar ADMITS the swept cells (it does, because recovery holds). const whys0 = E.parkLogWhys(); for (const seed of [1, 5, 9]) { const cell = E.PARK_FIELD_MECHS.log.cell(seed); // the campaign's own minting path assert.ok(E.PARK_FIELD_MECHS.log.admits(cell), `seed ${seed}: the shipped admission predicate REJECTS a swept cell. y8 folds its ship bar into ` + 'admission (it ships), so a rejection here means the calibrated ORDER recovery regressed on a swept ' + 'seed — the corridor no longer holds the decisive award past the span; re-measure _parkLogRecovers.'); const st = E.parkFieldBuild(cell); // the campaign's own build path assert.strictEqual(st.park.fieldMech, 'log', 'parkFieldBuild built a board that is not a log board'); assert.strictEqual(st.park.dyn.ents[0].kind, 'log', 'the built board has no log'); } const whys = E.parkLogWhys(); // nothing was rejected, and the tallies say so for (const k in whys) assert.strictEqual(whys[k], whys0[k], `the admission predicate rejected an accepted cell for reason '${k}' — the LOUD tally moved`); console.log(` [Y8-LOG-SHIP-GATE] 8 seeds x 6 personas: ${tot}/${tot} faithful completes alive; ` + `blind ORDER recovery UNCALIBRATED ${rec}/${tot} vs CALIBRATED (skip=${E.PARK_CAL_TURNS}) ${recCal}/${tot} ` + `— per seed ${split.join(' ')}; the corridor holds every decisive award at turn 3+, so the calibrated ` + `read (the one the product makes) is ALSO 6/6 — y8 has EARNED the bar. ` + `Per-pair widened ${recPair}/${expr} expressed (G-C ${byPair.GC}, G-N ${byPair.GN}, C-N ${byPair.CN}; ` + `mis-read ${misPair}) — the C-N poser is intact. Admits 3/3 on the campaign's own cell/admits/build ` + `path, reject tallies ${JSON.stringify(whys)}; ship seed ` + `${E.PARK_LOG_SHIP_SEED} shippable=${E.PARK_LOG_SHIPPABLE}`); }); /* ================= y6 DUCKLING FIELD CELL (Task 6, plan 2026-07-13) ================= The cell that exists to SPLIT C FROM N. A duckling follows the walker's footprints one beat behind; parts of the meadow are TENDER — they hurt the DUCK and never the walker. So the walker's body-safety read (C) is FREE to take the tender lane, while the care read (N) — "the next footprint I leave is where the duckling will stand" — is not. Every other park cell can be muddled ("protect myself" and "protect the other" point the same way); here they diverge on the SAME cell, by construction. That divergence is this cell's ADMISSION CONDITION, and the gates below measure it rather than assume it. THE THREE CHANNELS STAY SEPARATE, and these tests are what pin that: - the duck's ♥ is a DISPLAY counter (dyn.ents[].hearts). It never touches P.hearts, st.score or P.over — a duckling can die and the run continues (Y6-DUCK-CHANNELS). - the CARE READ is a read of the WALKER'S MOVE PREFERENCE (is the footprint I am about to leave duck-kind?), never a read of the duck's heart total (Y6-DUCK-TENDER-SPLIT). */ const _Y6_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; const _y6Duck = (P) => P.st.park.dyn.ents.find(e => e.kind === 'duck'); // Y6-DUCK-FOLLOW: the one-beat-lagged footprint follow, and the seam's sharp edge N3 — the duck // keys on the module's OWN trail (dyn.ents[].trail), never on P.prev, which is not a trustworthy // "previous cell" on a board with action-moves. Non-vacuous by construction: the duck SPAWNS on a // different cell from the walker, so "the duck is at my previous cell" is a move it had to make. // Y6-DUCK-TENDER-SPLIT: THE CELL'S THESIS. On the tender ground the walker's own safety read and // the care read must be MEASURABLY DISJOINT: C contains the tender move (tender does not hurt the // walker), N refuses it (it is where the duckling will stand). Asserted on the READ SETS, not on // any consequence — a stub that merely damaged the duck would fail this. // Y6-DUCK-CHANNELS: ♥ AND PERSONALITY ARE SEPARATE CHANNELS — the constraint this cell is most // likely to violate. The duck's survival is a DISPLAY channel: it may hit 0 (the duckling flees to // the edge) and the run must carry on, the walker's body untouched, the score untouched, and // parkReduce still the one scorer (no award may cite the duck's ♥). // Y6-DUCK-C1: the board, the tender set and the duck's path are pure functions of the PUBLIC seed. // Y6-DUCK-PREFER-GATING: the trap that would silently destroy this cell's whole result, so it gets a // test rather than a comment. _parkReads intersects a mechanic's `prefer` whenever the SHIPPED // attitude is engaged — NOT when the mechanic's own `engaged` says so. So `prefer` runs in states the // mechanic considers itself disengaged (measured here: 1407 of 3718 visited states), and if it // narrowed anything there, or fell through to {} or {'stay'}, the care mind would collapse in states // that have nothing to do with the duckling — and every C-vs-N number this cell reports would be // measuring the wrong thing. The guard: when there is no duckling, `prefer` returns the FULL legal set // (never {}, which is INERT not a veto, and never {'stay'}, which is a walker who stops walking). // Y6-DUCK-SHIP-GATE (P9/P10): the cell ships as a LIVE picker tile ONLY on 6/6 blind ORDER // recovery. This gate PINS faithful completion + the per-pair widened recovery of every EXPRESSED // pair, MEASURES the C-N expression rate (the cell's whole reason to exist — y12 posed it 0/144), // and REPORTS the order-recovery tally the campaign slot header quotes. /* ================= y10 RISING WATER (Task 5) — the SHRINKING-TERRAIN field cell ================= The board is grouped into concentric RINGS (outermost first) and every `every` beats the next ring sinks into dyn.flood. A MOUND in the middle never sinks, and one cell of it — the SUMMIT — is the companion's RESERVED SEAT (his planner's terminal target, park.retire). A walker caught standing on a sunk cell ENDS THE RUN: reason 'drown', a TERMINAL, never a heart cost (the two channels stay apart — this board spends no hearts at all, so the separation is structural). The ring that will sink NEXT is publicly previewable (park.flood.rings[nextIdx]) — the caution read has to be OBSERVABLE or "keep 2 rings of margin" is not a read a player could make. The junction: G = grab the outer gems before they go under (last-chance value) · C = keep >= 2 rings of margin to the water · N = leave the companion's summit seat free (the endgame is a scramble for high ground, and yielding your seat is the caring move). */ const _Y10_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; // THE CELLS THE GATES MEASURE are ADMITTED cells, swept exactly the way the CAMPAIGN sweeps them: // walk the seed stride, keep the first cell `mech.admits` accepts. A raw seed is not a board — on this // timetable plenty of raw geometries drown a faithful walk (measured 30/40), and throwing those away is // what the admission filter is for; testing the raw builder would test something the game never ships. // The sweep lives HERE, in the test, and not in the module: the campaign reaches a field mechanic // through mech.cell/mech.admits and never through a module-level generator, so a generator in the // module would have this test as its only caller (project decision 2026-07-14 — see parkFloodWhys). const _Y10_STRIDE = 7907, _Y10_BUDGET = 128; const _y10cache = {}; function _y10cell(base) { base = (base | 0) >>> 0; if (_y10cache[base] == null) { let found = -1; for (let t = 0; t < _Y10_BUDGET; t++) { if (E._parkFloodAdmissible(E._parkFloodCell((base + t * _Y10_STRIDE) >>> 0))) { found = t; break; } } assert.ok(found >= 0, `y10: no admissible geometry for base seed ${base} within ${_Y10_BUDGET} — the generator would have to fall back`); _y10cache[base] = found; } return E._parkFloodCell((base + _y10cache[base] * _Y10_STRIDE) >>> 0); } // Y10-FLOOD-WASH: THE TICK/TOKEN-KILL GUARD, and the reason it exists. // parkStep advances the chain cursor (_parkAdvanceDest) BEFORE it calls tick. So when tick WASHES A GEM // AWAY — which is the whole of y10's "last-chance value" — the cursor is left pointing at a DEAD // destination, and the mechanic must re-run the advance ITSELF. It does (one line in tick). Nothing // guarded it: a reviewer DELETED that line and all four y10 tests still passed with byte-identical // numbers, because the ACCEPTED seeds happen to tolerate one stale-cursor turn. That is exactly the // kind of load-bearing line a refactor deletes in silence, and then the goal mind goes quiet. // So this drives the failure DIRECTLY, on a board where the cursor is ON the gem the water is about to // take: after the sink, the destination must have MOVED OFF the dead gem, and G must still have // something to say. Fails if the _parkAdvanceDest call in tick is removed. // NOTE ON HOW THIS IS DRIVEN, because the first cut of it was VACUOUS and passed against the mutant. // Driving with 'stay' looks like the clean way to make the water the only moving thing — but a walker // who never moves is standing on a sinking ring, so he DROWNS on the very beat his gem goes under, the // run ends, and the assertion never executes. It has to be driven by FAITHFUL PLAY: the walker survives, // and the gem he is currently walking towards is taken by the water out from under his goal. test('Y10-FLOOD-WASH: a gem washed away by the tick advances the chain; the goal mind does not go silent', () => { let washed = 0; for (const seed of _Y10_SEEDS) { const cell = _y10cell(seed); for (const persona of E.PARK_PERSONAS) { const P = E.parkStart(E._parkFloodBuild(cell)); const chain = P.st.park.chain; while (!P.over) { const destIdx = P.dest; if (destIdx >= chain.length) break; const tok = P.st.tokens[chain[destIdx]]; const tokKey = E._parkKey(P.st, tok); const wasAlive = tok.alive; E.parkStep(P, E.parkOracleMove(P, persona)); if (!wasAlive || tok.alive) continue; // the gem is still there const here = E._parkKey(P.st, P.st.pos[0]); const takenByWater = P.st.park.dyn.flood.has(tokKey) && here !== tokKey; if (!takenByWater) continue; // he picked it up: an honest harvest if (P.over) continue; // he drowned on the same beat washed++; // count it ONLY if it reaches the assert // below, so `washed > 0` cannot be // satisfied by skipped events // THE ASSERTION THE DELETED LINE BREAKS. parkStep advances the cursor BEFORE tick runs, so a gem // that tick washes away leaves the cursor pointing at a DEAD destination. Read it RIGHT HERE: // the very next parkStep would re-advance it and hide the bug. assert.ok(P.dest > destIdx, `seed ${seed} ${persona.join('>')}: the water took the chain gem and the cursor still points at it (dest ${P.dest}) — tick killed a token without re-running _parkAdvanceDest, so the goal metric now targets a cell that no longer exists`); // ...and the consequence that actually hurts: the goal mind must not have gone silent. const reads = E._parkReads(P); if (P.dest < chain.length) { assert.ok(reads.atts.G.engaged && reads.atts.G.pref.size > 0, `seed ${seed} ${persona.join('>')}: the goal mind went SILENT after a gem was washed away (engaged=${reads.atts.G.engaged}, |pref|=${reads.atts.G.pref && reads.atts.G.pref.size})`); } } } } assert.ok(washed > 0, 'no chain gem was ever washed away — this gate never exercised the tick token-kill path'); console.log(` [Y10-FLOOD-WASH] ${washed} chain gems taken by the water over 8 seeds x 6 personas; the cursor stepped past every one and the goal mind stayed live`); }); // Y10-FLOOD-PHYSICS (the brief's Step-1 test, adapted to the seam): rings sink ON SCHEDULE, the // mound is excluded from every ring, and a walker caught standing on a sunk cell ENDS THE RUN with // its OWN terminal — not a heart. Drowning is checked with the walker held in place ('stay' on a // sinking ring), which is exactly why the tick hook must fire on 'stay'. test('Y10-FLOOD-PHYSICS: rings sink on schedule; mound never sinks; a caught walker DROWNS (terminal, not a heart)', () => { const st = E._parkFloodBuild(_y10cell(9)); const P = E.parkStart(st); const fl = P.st.park.flood; assert.ok(fl.rings.length >= 3, 'a flood board needs several rings'); assert.strictEqual(P.st.park.dyn.flood.size, 0, 'a fresh build starts dry'); for (let t = 0; t < fl.every; t++) E.parkStep(P, 'stay'); assert.ok(fl.rings[0].every(k => P.st.park.dyn.flood.has(k)), 'ring0 sank'); assert.ok(fl.rings[1].every(k => !P.st.park.dyn.flood.has(k)), 'ring1 sank EARLY (the schedule leaks)'); assert.ok(fl.rings.flat().every(k => !fl.mound.has(k)), 'mound excluded from the ring schedule'); assert.ok(fl.mound.has(fl.seatKey), 'the companion seat must be ON the mound'); // the DROWN terminal: park the walker on a ring cell and let the water come to him. Hearts are // UNTOUCHED — the run ends because the ground went, not because the body was spent. const Q = E.parkStart(E._parkFloodBuild(_y10cell(9))); const ring = Q.st.park.flood.rings; let target = -1, ri = -1; for (let i = 0; i < ring.length && target < 0; i++) for (const k of ring[i]) { if (k !== E._parkKey(Q.st, Q.st.pos[0])) continue; target = k; ri = i; } assert.ok(ri >= 0, 'the spawn is not on any sinkable ring — nothing can drown'); const h0 = Q.hearts; for (let t = 0; t < Q.st.park.flood.every * (ri + 1) + 1 && !Q.over; t++) E.parkStep(Q, 'stay'); assert.strictEqual(Q.reason, 'drown', `a walker standing on a sunk ring must DROWN (got ${Q.reason})`); assert.strictEqual(Q.hearts, h0, 'drowning charged the HEART channel — it is a terminal, not a heart cost'); assert.strictEqual(Q.deepEntries, 0, 'drowning bumped the deep-entry meter — the channels must stay apart'); }); // Y10-FLOOD-READS: the three-mind read of a shrinking board. C (safety) keeps >= 2 rings of margin // to the water and, when nothing can, climbs to the highest ground it can reach; N (care) leaves the // companion's SUMMIT SEAT free. The cell earns its slot only if the endgame SEAT SCRAMBLE actually // SEPARATES N — i.e. there are states where C's compliant set and N's are DISJOINT (that is the one // shape that makes a C-vs-N award possible; see the y12 diagnosis). test('Y10-FLOOD-READS: C keeps water-margin, N yields the mound seat; the seat scramble separates C from N', () => { let scenes = 0, disjoint = 0, seatOffered = 0; for (const seed of _Y10_SEEDS) { for (const persona of E.PARK_PERSONAS) { const P = E.parkStart(E._parkFloodBuild(_y10cell(seed))); while (!P.over) { const reads = E._parkReads(P); const seatK = P.st.park.flood.seatKey; const onSeat = reads.legal.filter(c => c.key === seatK); if (onSeat.length && reads.atts.N.engaged) { seatOffered++; for (const c of onSeat) assert.ok(!reads.atts.N.pref.has(c.k), `seed ${seed}: N prescribes taking the companion's reserved seat (${c.k})`); if (reads.atts.C.engaged && reads.atts.C.pref.size) { scenes++; let inter = 0; for (const m of reads.atts.C.pref) if (reads.atts.N.pref.has(m)) inter++; if (inter === 0) disjoint++; // C and N genuinely DISJOINT — the C-N award is posable } } E.parkStep(P, E.parkOracleMove(P, persona)); } } } assert.ok(seatOffered > 0, 'the mound seat was never even a legal move — the care read is vacuous'); assert.ok(disjoint > 0, 'C and N always intersected at the seat: the endgame scramble never SEPARATES care from safety, so the cell does not earn its slot'); assert.strictEqual(seatOffered, 96, `seat-offered states ${seatOffered}, claimed 96`); assert.strictEqual(disjoint, 24, `C/N-DISJOINT seat states ${disjoint}, claimed 24 — this is the C-N poser, pin it`); // THE DESIGN LAW, ASSERTED BEHAVIOURALLY. C-N separation must be GEOMETRIC: the object of care has to // sit where safety is FORBIDDEN to go, or a cautious walker would happily yield anyway and caution // never objects. (A sibling cell posed C-N 0/144 for exactly this reason — its caring act was // performed from a safe square — and a reviewer proved it by swapping C and N in the persona orders // and getting BEHAVIOURALLY IDENTICAL trajectories.) So run that same proof here: personas that // differ ONLY in the order of safety and care must actually WALK DIFFERENTLY. On y10 they do, // because the seat is the one cell with margin left: safety wants it, care leaves it, and the walker // who yields it stands in the rising water to do so. The yield COSTS safety something — that is the // whole cell, and if it ever stops costing, this goes red. const swaps = [[['goal', 'safety', 'care'], ['goal', 'care', 'safety']], [['safety', 'goal', 'care'], ['care', 'goal', 'safety']], [['safety', 'care', 'goal'], ['care', 'safety', 'goal']]]; let divergent = 0, identical = 0; for (const seed of _Y10_SEEDS) { const cell = _y10cell(seed); for (const [a, b] of swaps) { const A = E.parkPlayout(E._parkFloodBuild(cell), a).moves.join(''); const B = E.parkPlayout(E._parkFloodBuild(cell), b).moves.join(''); (A === B) ? identical++ : divergent++; } } assert.ok(divergent > 0, 'swapping SAFETY and CARE in the persona order changed NOTHING: the two minds are behaviourally identical on this board, so C-N is inert no matter what the reads say'); // PIN IT. This is the headline claim of the whole cell ("the yield COSTS safety something"), and it // was the one number left as a console.log while every other claim was a strictEqual — my own "pin // every number the header claims" doctrine, not applied to the number that matters most. assert.strictEqual(divergent, 15, `C/N-swapped personas diverge ${divergent}/24, claimed 15`); console.log(` [Y10-FLOOD-READS] ${seatOffered} seat-offered states over 8 seeds x 6 personas; ${scenes} with C live; ${disjoint} where C and N are DISJOINT (the C-N poser); C/N-swapped personas diverge ${divergent}/${divergent + identical}`); }); // Y10-FLOOD-C1: the board is a PURE function of the PUBLIC cell, the flood timetable is seed-pure, // and no faithful walk ever drowns (the brief's own gate: a board where faithful play drowns is a // GENERATOR bug — fix the geometry, never relax the gate). const _y10canon = (st) => JSON.stringify(st, (k, v) => v instanceof Set ? [...v].sort((a, b) => a - b) : v); test('Y10-FLOOD-C1: board bytes persona-invariant, deterministic, fresh per build; no faithful walk drowns', () => { for (const seed of [1, 3, 5, 7]) { const a = _y10canon(E._parkFloodBuild(_y10cell(seed))); const withStream = { ..._y10cell(seed), personaStream: ['goal', 'safety', 'care'] }; assert.strictEqual(_y10canon(E._parkFloodBuild(withStream)), a, `seed ${seed}: personaStream leaked into the board bytes`); E.parkPlayout(E._parkFloodBuild(_y10cell(seed)), E.PARK_PERSONAS[0]); assert.strictEqual(_y10canon(E._parkFloodBuild(_y10cell(seed))), a, `seed ${seed}: a played instance corrupted the rebuild`); const st = E._parkFloodBuild(_y10cell(seed)); assert.strictEqual(st.park.dyn.flood.size, 0, 'a fresh build starts dry'); assert.strictEqual(st.park.deep.size, 0, 'y10 carries NO deep field: the water is the whole hazard, and it is a TERMINAL not a heart'); } for (const seed of _Y10_SEEDS) for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E._parkFloodBuild(_y10cell(seed)), persona); assert.notStrictEqual(P.reason, 'drown', `seed ${seed} ${persona.join('>')}: a FAITHFUL walk drowned — that is a generator bug`); assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}, not complete`); assert.strictEqual(P.hearts, 3, 'y10 spent a heart — the drown terminal and the heart channel must stay separate'); } const legacy = E.makeParkTask('m1', { seed: 7, hazard: { kind: 'meadow', damage: 1, d: 2 } }); assert.ok(!legacy.park.flood && !legacy.park.dyn, 'a legacy walk board grew a flood/dyn field (byte stability broken)'); // THE COMPANION IS NEVER STRANDED — the invariant the retreat rule actually provides, asserted as // such. NOT "he is never wet": he legitimately ends the run mid-wade (the water reaches the outer // belt only in the closing beats, and tick is suppressed on the completing step), and asserting // WET == 0 would be asserting something false and then bending the code to it. What must hold is // that dry ground is always REACHABLE from wherever the water catches him, so he always has a way // out and takes it. (An earlier greedy rule left him pacing a walled belt forever; this is the gate // that would have caught it.) let stranded = 0, wet = 0; for (const seed of _Y10_SEEDS) for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E._parkFloodBuild(_y10cell(seed)), persona); const st = P.st, n = st.N, flood = st.park.dyn.flood; const src = E._parkKey(st, st.pos[1]); if (!flood.has(src)) continue; wet++; const seen = new Set([src]); let q = [src], dry = false; while (q.length && !dry) { const nx = []; for (const c of q) { const cx = c % n, cy = (c / n) | 0; for (const d of [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]) { const ax = cx + d.x, ay = cy + d.y; if (ax < 0 || ay < 0 || ax >= n || ay >= n) continue; const k = ay * n + ax; if (st.wall.has(k) || seen.has(k)) continue; seen.add(k); if (!flood.has(k)) { dry = true; break; } nx.push(k); } if (dry) break; } q = nx; } if (!dry) stranded++; } assert.strictEqual(stranded, 0, `the companion was STRANDED in the lake with no dry ground reachable in ${stranded} runs — the retreat rule cannot get him out`); // and the disclosure that matters for the eventual live re-gate: HE NEVER ACTUALLY ARRIVES. let arrived = 0; for (const seed of _Y10_SEEDS) for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(E._parkFloodBuild(_y10cell(seed)), persona); if (E._parkKey(P.st, P.st.pos[1]) === P.st.park.flood.seatKey) arrived++; } assert.strictEqual(arrived, 0, 'the companion now REACHES his seat — the care read is about a seat he is still climbing towards, so if he starts arriving, re-derive C-N before trusting it'); console.log(` [Y10-FLOOD-C1] 4 seeds byte-stable; 8 seeds x 6 personas complete standing (0 drowns, 0 hearts spent); companion stranded ${stranded} (wet-at-end ${wet}, mid-wade); reaches his seat ${arrived}/48 — the yield is never actually collected in play`); }); // Y10-FLOOD-SHIP-GATE (P9/P10): a y10 cell may ship as a LIVE picker tile ONLY on blind 6/6-persona // ORDER recovery. Built out of the shipped recovery stack and nothing else. The gate PINS the bars // the module actually clears and REPORTS the order-recovery tally, so a regression that loses a pair // trips it. Track lowered, gate never weakened. test('Y10-FLOOD-SHIP-GATE: 6/6 faithful completes standing; posed pairs blind-recovered; order recovery measured', () => { let rec = 0, tot = 0, expr = 0, recPair = 0, misPair = 0; const posed = { GC: 0, GN: 0, CN: 0 }; for (const seed of _Y10_SEEDS) { const cell = _y10cell(seed); const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(E._parkFloodBuild(cell), p)); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i], P = playouts[i]; assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}`); tot++; const r = E.parkRecoverOrder(E._parkFloodBuild(cell), P.moves); if (r && r.join() === persona.join()) rec++; for (const pair of E.PARK_FLOOD_PAIRS) { if (!(E.parkPairExpressed(E._parkFloodBuild(cell), P.moves, pair) > 0)) continue; expr++; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); if (E.parkRecoverPairLex(E._parkFloodBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } } assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw)`); assert.strictEqual(recPair, expr, `${expr - recPair}/${expr} expressed pairs failed the widened blind read`); // ---- PIN THE NUMBERS THE MODULE CLAIMS, not just print them. A gate that MEASURES blind recovery // and only LOGS it is not a gate: a regression that drops C-N from 22 to 0 would still go green, // and the claim on the campaign slot header would quietly become a lie. So every number the slot // header and PARK_FLOOD_SHIPPABLE assert is pinned here as a TRIPWIRE. That includes the NEGATIVE // ones: y10 ships DEMO-ONLY at 15/48 order recovery, and the day someone closes the G-N gap this // test goes RED and forces a deliberate re-gate to ship:true rather than letting it drift. assert.strictEqual(tot, 48, 'the sweep shape changed (8 seeds x 6 personas)'); assert.strictEqual(posed.GC, 48, `G-C posed ${posed.GC}/48, claimed 48 — the last-chance-gem stake is the cell's spine`); assert.strictEqual(posed.GN, 15, `G-N posed ${posed.GN}/48, claimed 15 (the contested gem on the approach belt)`); assert.strictEqual(posed.CN, 22, `C-N posed ${posed.CN}/48, claimed 22 — THE MOUND-SEAT SCRAMBLE, and the pair y12 could never pose`); assert.ok(posed.CN > 0, 'C-N was NEVER posed: the mound-seat scramble does not separate care from safety, so y10 has not earned its slot'); assert.strictEqual(rec, 15, `blind ORDER recovery ${rec}/48, claimed 15 — y10 is DEMO-ONLY; if this rose, re-gate it deliberately`); assert.strictEqual(E.PARK_FLOOD_SHIPPABLE, false, 'y10 is demo-only (P9/P10) — flipping this needs 6/6 order recovery, not a flipped constant'); assert.strictEqual(E._parkFloodRecovers(_y10cell(E.PARK_FLOOD_SHIP_SEED)), E.PARK_FLOOD_SHIPPABLE, 'the module\'s declared shippability disagrees with the blind 6/6 order-recovery read on the shipped seed'); console.log(` [Y10-FLOOD-SHIP-GATE] 8 seeds x 6 personas: ${tot}/${tot} faithful completes standing; blind ORDER recovery ${rec}/${tot}; per-pair widened ${recPair}/${expr} expressed (mis-read ${misPair}); posed G-C ${posed.GC} / G-N ${posed.GN} / C-N ${posed.CN}; ship seed ${E.PARK_FLOOD_SHIP_SEED} shippable=${E.PARK_FLOOD_SHIPPABLE}`); // THE REJECTION TELEMETRY MUST BE LOUD ON THE PATH THE CAMPAIGN ACTUALLY WALKS. y10 has no module // generator (the campaign sweeps a field mechanic through mech.cell/mech.admits), so there is no // fallback counter to read — and a counter only a test increments would have proved nothing anyway. // What IS real is `_PARK_FLOOD_WHYS`, tallied inside `admits`: the sweep above ran it hundreds of // times and it must have recorded the rejections it made, or the loud counter is silently dead. const whys = E.parkFloodWhys(); const rejected = Object.values(whys).reduce((a, b) => a + b, 0); assert.ok(rejected > 0, `the admission telemetry recorded NOTHING (${JSON.stringify(whys)}) — the loud reject counter is dead`); assert.ok(whys.drown > 0, 'no candidate was ever rejected for drowning a faithful walk — the strictest bar on this board is not firing'); // and the mechanic is reachable the way the campaign reaches it: through the registry, not a generator const m = E.PARK_FIELD_MECHS.flood; assert.ok(m && typeof m.cell === 'function' && typeof m.admits === 'function', 'y10 does not expose the campaign surface (mech.cell / mech.admits) — the campaign could not sweep it'); const viaSeam = E.parkFieldBuild(m.cell(E.PARK_FLOOD_SHIP_SEED)); assert.strictEqual(viaSeam.park.fieldMech, 'flood', 'parkFieldBuild did not build a flood board through the registry'); assert.strictEqual(viaSeam.park.dyn.flood.size, 0, 'a fresh board came back already under water'); console.log(` [Y10-FLOOD-SHIP-GATE] admission telemetry (loud, from admits): ${JSON.stringify(whys)}`); }); /* ---- GATE: CAMP-CROSS-SWEEP — the crossing filter, on the real picker (Task 8 F2/F4) ---- * THE HOLE THIS CLOSES: nothing in this suite ever called parkCrossings(). Every per-cell ship gate * above measures a MODULE on its own seeds; the layer that actually seats those modules — the * campaign's crossing filter — shipped unasserted, and a slot that EXHAUSTED its 128-candidate * incongruence sweep (y6 did, on every run seed) bumped the LOUD parkCrossFallbacks counter with * nobody listening. * * IT ASSERTS THE INVARIANT, NOT THE NUMBER. `parkCrossFallbacks() === 0` would be the wrong gate: * exhaustion is a DESIGNED loud path (see _parkCrossingPlayCell's header) and a future cell may * legitimately take it. What must never happen is a SHIPPED crossing riding an unfiltered cell — * that is the bar the design's own header promises ("the gate then measures that cell failing the * ship bars and the crossing stays 'coming' — never silently shipped"). So, per run seed: * 1. every ship:true slot's play cell PASSED the filter (sweep >= 0, filtered); * 2. the counter's delta == the number of EXHAUSTED slots observed (sweep -1), and every one of * them is ship:false — exhaustion is permitted ONLY for a slot that stays "coming"; * 3. a non-shipped slot is NOT swept (sweep -2, filtered:false — F1: it makes no measurement * claim, so there is no transfer test for the filter to protect); * 4. the sweep adds NOTHING to parkGenFallbacks() (it pre-admits its candidates precisely so it * never touches the generator's loud counter). A DELTA, because that counter is a monotone * process global that earlier gates in this suite legitimately drive. * 5. TASK 2 (P1 re-pair) — the DEMO leg's own loud counter, parkDemoFallbacks(), is 0 PER SEED. * This clause closes a real hole: Task 1's demo-counter coverage was a SINGLE-seed test * (parkCrossings(11)), and now that xp/xs mint their demo cells through the slide/push MODULES * a demo-sweep exhaustion can land on a base seed that single test never visits. Unlike the * PLAY leg — where exhaustion is a DESIGNED loud path for a slot that stays "coming" — a DEMO * exhaustion is permitted for NO slot: the fallback seats the offset-0 candidate, i.e. a cell * the module's own admits() REJECTED, and every bar xp/xs were measured on assumed an * ADMISSIBLE demo cell. So the invariant is 0, per seed, asserted as a DELTA (the counter is a * monotone process global — the repo's fallback-counter convention). * Plus F4: PARK_CROSSINGS[i].ship must agree with the module's OWN measured shippability constant, * so a slot flag and a module pin cannot silently drift apart — the exemption mechanism stays, but * the set is now EMPTY (y22 was promoted 2026-07-18; it no longer borrows y20's bomb module, it is * measured on its own PAIRING). A future preview borrowing a sibling's shipped module would re-open * the exemption; the exempt set is pinned BY NAME, never left to widen silently. * * TASK 9 amends clause 3. It used to end "...and is genuinely unseatable (runParkCrossing null)", * and it asserted that. That was the old, WRONG behavior — the bug a user actually hit: four tiles * rendered in the picker and silently no-opped on click. The four are now `open: true` PREVIEWS and * they SEAT. What the gate pins instead is the pair of properties that make that safe: * 3a. every slot is seatable IFF (ship || open) — so all of them seat today, and a slot that is * neither still no-ops (the inert path stays live for a future slot); * 3b. a seated non-shipped slot is STAMPED preview:true, so no surface can mistake it for * measured — and `ship` itself is UNCHANGED (still false on all four), which is what keeps * every ship gate above and the F4 cross-check below honest. * The filter is still keyed to `ship` and NEVER to `ship || open`: opening a slot must not move a * cell. (Byte stability: y12/y14/y6/y10 keep sweep -2 and the exact play cells they were measured on.) * NON-VACUITY: it asserts it actually inspected shipped slots, preview slots and field slots, and * prints the per-slot sweep table for every seed. */ test('CAMP-CROSS-SWEEP: shipped crossings are always filtered; exhaustion only on demo-only slots', () => { const SEEDS = [1, 11, 23]; const FIELD_SHIPPABLE = { // F4: slot mechanic -> the module's measured pin stones: E.PARK_STONES_SHIPPABLE, toll: E.PARK_TOLL_SHIPPABLE, downed: E.PARK_DOWNED_SHIPPABLE, log: E.PARK_LOG_SHIPPABLE, flood: E.PARK_FLOOD_SHIPPABLE, carry: E.PARK_CARRY_SHIPPABLE, // y16 (Task 3) — the seventh, and the first true fire: E.PARK_FIRE_SHIPPABLE, // y17 (Task 5) — SHIPPED 2026-07-23 (module 6/6 calibrated) mine: E.PARK_MINE_SHIPPABLE, // y18 (Task 6) — demo-only (the ninth) // `tower` (y19) had a row here and left with its SLOT on 2026-08-04, the way `statue`/`relay` // and `escape` did below: this map is keyed by slot MECHANIC and no slot names tower any more. // The module and E.PARK_TOWER_SHIPPABLE are untouched in engine.js, and the module's own bar is // still measured by Y19-TOWER-SHIP-GATE. Re-seating the slot re-adds this row. bomb: E.PARK_BOMB_SHIPPABLE, // y20 (Task 1/2) — SHIPPED (the eleventh; 3rd field ship) bomb2: E.PARK_BOMB2_SHIPPABLE, // y22 (fork 2026-07-20) — pen dual-route // NOTE: y22's ship is now pinned TWICE — here (F4, against the MODULE bar PARK_BOMB2_SHIPPABLE) // and by Y22-SHIP-MEASURED (against the PAIRING bar PARK_Y22_SHIPPABLE). Both read true today, // so both hold, but if the two bars ever diverge these gates become jointly unsatisfiable. storm: E.PARK_STORM_SHIPPABLE, // y23 (Task 4) — demo-only honest preview (the thirteenth) ledge: E.PARK_LEDGE_SHIPPABLE, // y26 (Task 4) — demo-only honest preview (the fifteenth) // y27 — honest preview (the sixteenth) lantern: E.PARK_LANT_SHIPPABLE, // y24 — honest preview (the seventeenth) // `statue` (y29) and `relay` (y31) had rows here and left with their SLOTS on 2026-07-29, the // way `escape` (y51) did below: this map is keyed by slot MECHANIC and no slot names either any // more. Both modules and their E.PARK_{STATUE,RELAY}_SHIPPABLE pins are untouched in engine.js, // and statue is still live under the SHIPPED y46 siege board. Re-seating a slot re-adds its row. // y32 — honest preview (the twentieth) yield: E.PARK_YIELD_SHIPPABLE, // y33 — the purpose-built C-vs-N footbridge // (seated si 37 at the 2026-07-29 integration) siege: E.PARK_SIEGE_SHIPPABLE, // y46 — honest preview (the twenty-first) alley: E.PARK_ALLEY_SHIPPABLE, // y50 — honest preview (the twenty-second) // `escape` (y51) had a row here and left with its slot on 2026-07-28: this map is keyed by SLOT // MECHANIC (`fv in FIELD_SHIPPABLE` is asserted per slot below), and no slot names escape any // more — the module could not pass FIELD-REGISTRY-SURFACE at 0/40 admission. The module and // E.PARK_ESCAPE_SHIPPABLE are untouched in engine.js; re-seating the slot re-adds this row. beacon: E.PARK_BEACON_SHIPPABLE, // y53 — LIVE 2026-07-27 (the twenty-fourth) burst: E.PARK_BURST_SHIPPABLE, // y52 — LIVE 2026-07-27 (the twenty-fifth) shifter: E.PARK_SHIFTER_SHIPPABLE, // y54 — honest preview (the twenty-sixth) plaza: E.PARK_PLAZA_SHIPPABLE, // y59 — honest preview, SEATED 2026-08-04. // The module pin is a LITERAL false, not a derivation (engine.js says so at the constant): the // pairing/promotion measurement is deferred by spec §9, so the equality below reads // `false === false` for a reason that is written down rather than measured. That is exactly the // y24-lantern / y31-relay convention, and it is the honest shape for a preview whose bar has // not been run — never a `true` asserted ahead of a measurement. }; let shipSeen = 0, comingSeen = 0, exhausted = 0, fieldChecked = 0; // BOTH counters are monotone PROCESS GLOBALS (never reset), and earlier gates in this suite // legitimately drive the generator's fallback path, so the absolute value of parkGenFallbacks() // here is whatever the run before us left behind. What THIS gate owns is its own DELTA — the // established pattern (cf. the PARK-GEN sweep above): the crossing sweep must add nothing to it. const fb0 = CAMP.parkCrossFallbacks(), gf0 = E.parkGenFallbacks(); for (const seed of SEEDS) { const df0 = CAMP.parkDemoFallbacks(); // clause 5: PER-SEED, not once for the loop const list = CAMP.parkCrossings(seed); // clause 5 (Task 2): a DEMO-leg exhaustion is permitted for NO slot — see the header. Checked on // EVERY base seed this gate sweeps, because xp/xs now mint their demo cells through the // slide/push modules and a single-seed check cannot see an exhaustion on any other seed. assert.strictEqual(CAMP.parkDemoFallbacks() - df0, 0, `seed ${seed}: a crossing's module DEMO sweep EXHAUSTED (parkDemoFallbacks bumped). The fallback ` + 'seats the offset-0 candidate — a cell the module\'s own admits() rejected — so the demo leg would ' + 'be a board no bar was ever measured on. Fix the slot (or its seed stream); do not relax this to > 0.'); assert.strictEqual(list.length, CAMP.PARK_CROSSINGS.length, 'the picker dropped a slot'); console.log(` [CAMP-CROSS-SWEEP] seed ${seed}: ` + list.map(c => `${c.slot}:${c.sweep}${c.filtered ? '' : c.sweep === -1 ? '!EXHAUSTED' : '~unswept'}`).join(' ')); for (const c of list) { if (c.ship) { shipSeen++; assert.ok(c.sweep >= 0 && c.filtered === true, `SHIPPED crossing ${c.slot} (seed ${seed}) rides an UNFILTERED play cell (sweep ${c.sweep}) — ` + 'a seated crossing whose play leg was never proven incongruent is a replayable demo, not a transfer test'); } else { comingSeen++; assert.strictEqual(c.filtered, false, `non-shipped ${c.slot} claims to be filtered`); if (c.sweep === -1) exhausted++; // a real (loud) exhaustion else assert.strictEqual(c.sweep, -2, `non-shipped ${c.slot} was SWEPT (sweep ${c.sweep}) — it asserts no transfer guarantee (it is a ` + 'marked PREVIEW, not a measured crossing), so the incongruence filter has nothing to protect ' + 'and must be skipped (F1). If this slot now earns the bar, flip `ship`, not the filter key.'); } } } // the LOUD counter is exactly the exhaustions we saw — and every exhaustion sat on a "coming" slot // (the ship:true loop above would have failed otherwise). Delta, not absolute: it is a process global. assert.strictEqual(CAMP.parkCrossFallbacks() - fb0, exhausted, 'parkCrossFallbacks does not match the exhausted slots observed — the loud counter and the rows disagree'); assert.strictEqual(E.parkGenFallbacks(), gf0, 'the crossing sweep drove a park GENERATOR into its loud fallback — the sweep pre-admits its ' + 'candidates (_parkCellAdmits / mech.admits) precisely so it never touches that counter'); assert.ok(shipSeen > 0 && comingSeen > 0, 'the gate inspected no slots — it would have passed vacuously'); // F4: the slot table vs the modules' own measured constants // PLAN AMENDMENT 2026-07-17 (user-approved, opened as NARROWLY as possible): the equality below // assumed slot:module is 1:1, i.e. "module measured => any slot using it is measured". y22 broke // that assumption first — it WAS a PREVIEW slot (ship:false && open:true) BORROWING a module that // a DIFFERENT slot (y20) already shipped. The module pin measured the MODULE, not that slot's // demo->play PAIRING, and a preview asserts no bar of its own (F1 above already pins it unswept: // sweep -2, filtered false). The distinguishing mark was NOT "the module's pin is true" — that // shape also matches the bug this equality exists to catch (a preview whose OWN module later // ships and someone forgot to flip the slot flag). The mark is: ANOTHER slot with the same // fieldMech already carries ship:true — i.e. the pin was EARNED BY A SIBLING, not by this slot. // So the equality is skipped ONLY for: preview stamp AND a shipped sibling on the same module. // A dedicated-module preview has no such sibling, so it still bites when its module ships later // (y12/y14/y6/y10 today), and a ship:true slot always keeps the equality. // RESOLVED 2026-07-18: y22 was promoted (ship:true, own PAIRING measured — PARK_Y22_SHIPPABLE), so // it no longer matches the borrow-preview shape above and the exempt set is empty. The mechanism // itself stays live for the NEXT preview that borrows a shipped sibling's module. const exemptIds = []; for (const cx of CAMP.PARK_CROSSINGS) { const fv = cx.playMech.fieldMech; if (!fv) continue; assert.ok(fv in FIELD_SHIPPABLE, `slot ${cx.id} names an unknown field mechanic '${fv}'`); const shippedSibling = CAMP.PARK_CROSSINGS.some(o => o !== cx && o.playMech.fieldMech === fv && o.ship === true); if (cx.ship === false && cx.open === true && shippedSibling) { exemptIds.push(`${cx.id}:${fv}`); // collected, logged AND pinned BY NAME below — never silent. // The borrowed MODULE is part of the name: pinning the slot id // alone would let the exemption silently MOVE to another shipped // module (probe-observed: y22 on carry stayed green under a // plain ['y22'] pin, because y16 ships carry too). } else { assert.strictEqual(cx.ship, FIELD_SHIPPABLE[fv], `slot ${cx.id}.ship=${cx.ship} disagrees with PARK_${fv.toUpperCase()}_SHIPPABLE=${FIELD_SHIPPABLE[fv]} — ` + 'the picker flag and the module\'s measured ship bar have drifted apart'); } assert.ok(E.PARK_FIELD_MECHS[fv], `slot ${cx.id} names a field mechanic the registry does not have`); fieldChecked++; } // 25 -> 26 on 2026-07-31: y55 seats the m2 ruler on y17's fire yard, so `fire` is now named by TWO // field slots. That is the first time a SHIPPED module carries two shipped seats, and the loop // above already handles it correctly — the exemption branch only fires for a PREVIEW borrowing a // shipped sibling, and both fire slots ship, so each takes the plain equality against // PARK_FIRE_SHIPPABLE. The count is bumped by hand precisely so that a second seat cannot appear // without somebody saying so here. // 26 -> 27 (2026-07-31, y56): `carry` becomes the SECOND module with two shipped seats (after // `fire`). Both ship, so each takes the plain equality against PARK_CARRY_SHIPPABLE — the // exemption branch is for a PREVIEW borrowing a shipped sibling's module, which this is not. // 27 -> 29 (2026-08-01, xp + xs): both were already shipped, and neither was a field slot until // their PLAY legs moved off the `push` and `slide` verb modules onto `ledge` and `yield`. So this // count rises by two without a single new ship claim — the two cells simply now answer to a module // pin where before they answered to a verb module that has none. `ledge` and `yield` each become // the module's SECOND shipped seat (after y26 and y33 respectively), the same shape `fire` and // `carry` already have above: both seats ship, so each takes the plain equality and the exemption // branch — which exists for a PREVIEW borrowing a shipped sibling — stays untouched and empty. assert.strictEqual(fieldChecked, 29, 'expected 29 field slots inspected against their modules'); assert.deepStrictEqual(exemptIds, [], `the ship-pin equality exemption set is ${JSON.stringify(exemptIds)} — it must be EMPTY: y22 was ` + 'promoted (ship:true), so it is now a shipped sibling measured on its own PAIRING (PARK_Y22_SHIPPABLE), ' + 'not a preview borrowing y20\'s module. A NEW borrow-preview would re-add a NAMED entry here on purpose.'); console.log(` [CAMP-CROSS-SWEEP] ${SEEDS.length} run seeds: ${shipSeen} shipped slot-observations ALL filtered; ` + `${comingSeen} non-shipped (unswept); exhaustions ${exhausted}; parkCrossFallbacks delta ` + `${CAMP.parkCrossFallbacks() - fb0} (absolute ${CAMP.parkCrossFallbacks()}); parkGenFallbacks delta 0 ` + `(absolute ${E.parkGenFallbacks()}); ${fieldChecked - exemptIds.length}/${fieldChecked} field slots ` + `cross-checked against their modules; ${exemptIds.length} preview reusing a shipped module ` + `(${exemptIds.join(',')}; exempt from the ship-pin equality)`); }); /* ---- GATE: CAMP-SLOTS-SEATABLE — every tile the picker draws actually opens (Task 9 §A) ---- * THE BUG THIS PINS, exactly as a user hit it: the picker drew 11 tiles and 4 of them silently did * nothing on click. runParkCrossing returned null on !ship, so y12/y14/y6/y10 rendered a thumbnail, * a mechanism strip and a badge band — every affordance of a button — and then swallowed the press. * A tile that renders and no-ops is a lie told in the interaction layer. * The contract now: a slot seats IFF (ship || open). All 11 do. A seat that is NOT shipped is * STAMPED preview:true, so nothing downstream can mistake it for a measured crossing (the readout * reads that stamp and says so; the picker paints its struck-through eye off the same fact). * The `neither` branch (inert, runParkCrossing -> null) is KEPT ALIVE for a future slot and asserted, * even though no slot is in it today — the path must not rot before it is needed. * SAFETY: opening these costs no measurement integrity because the picker is the UNSCORED 연습·열람 * corner (parkHubEnter) — the scored session is startParkSession -> runParkTransfer, which never * calls runParkCrossing at all. The next gate pins that `ship` itself did not move. */ test('CAMP-SLOTS-SEATABLE: every picker slot seats (ship || open); a preview seat is stamped', () => { const run = CAMP.createRun({ seed: 1, parkMode: true }); let seatedShip = 0, seatedPreview = 0, inert = 0; for (const c of run.park.crossings) { const seated = CAMP.runParkCrossing(run, c.id); if (c.ship || c.open) { assert.ok(seated, `slot ${c.slot} (ship=${c.ship} open=${c.open}) is drawn in the picker but ` + 'runParkCrossing returned null — a tile that renders and silently no-ops on click is the ' + 'exact defect Task 9 closed'); assert.strictEqual(!!seated.preview, !c.ship, `slot ${c.slot} seated with preview=${seated.preview} — a non-shipped seat MUST be stamped ` + 'preview (nothing downstream may mistake it for a measured crossing), and a shipped one must not be'); assert.ok(seated.game && seated.game.P, `slot ${c.slot} seated without a playable board`); // F3 (kept): the FIELD mechanic is a public mechanism axis. y3/y8 change ONLY the field, so // without it the readout paints "changed axes: none" on a crossing whose whole premise is the field. if (c.playMech.fieldMech && !c.demoMech.fieldMech) { assert.ok(seated.transfer.axesChanged.indexOf('fieldMech') >= 0, `${c.slot} seats a ${c.playMech.fieldMech} field over a plain demo board, but fieldMech is not in axesChanged`); assert.ok(seated.transfer.distance > 0, `${c.slot} reports transfer distance 0 across a changed field`); } c.ship ? seatedShip++ : seatedPreview++; } else { inert++; // reserved: the old no-op path, kept working assert.strictEqual(seated, null, `slot ${c.slot} is neither shipped nor open, yet it SEATED — the inert path is the reserve for ` + 'a future slot and must stay a no-op'); } } assert.strictEqual(seatedShip + seatedPreview + inert, CAMP.PARK_CROSSINGS.length, 'a slot went unclassified'); assert.strictEqual(inert, 0, 'a picker slot is INERT — it draws a tile and no-ops on click, which is the Task 9 defect returning. ' + 'A new slot must be born `open` (a marked preview) or `ship` (measured), never silent.'); assert.ok(seatedShip > 0 && seatedPreview > 0, 'NON-VACUITY: both seat branches must be exercised — shipped seats and preview seats'); console.log(` [CAMP-SLOTS-SEATABLE] ${CAMP.PARK_CROSSINGS.length} slots seat: ` + `${seatedShip} shipped + ${seatedPreview} preview + ${inert} inert`); }); /* ---- GATE: CAMP-SHIP-UNMOVED — `open` is not a back door into `ship` (Task 9 §A) ---- * `ship: true` is a MEASUREMENT FACT — "this cell earned the 6/6 blind order-recovery bar" — asserted * by the per-module ship gates above (Y12-STONES-SHIP-GATE etc., which pin PARK_*_SHIPPABLE) and * cross-checked by CAMP-CROSS-SWEEP's F4. Task 9 OPENED four cells for preview play. The one thing * that must NOT have happened along the way is the easy version of the fix: flipping `ship` so the * click works. This gate is the tripwire for exactly that, named so a future reader cannot miss it. * They stay ship:false, they stay open:true, and they still agree with their modules' own * measured pins — the same constants the engine's seed sweeps computed. * TASK 10: y8 JOINED THEM — from the other direction, and that is the point of keeping this gate * flag-agnostic. y8 was ship:true on a bar measured WITHOUT the readout's calibration window; the * bar now reads at PARK_CAL_TURNS and y8 recovers 0/6, so its measurement went away and the slot * followed it down to a marked preview. The tripwire is the same in both directions: the slot flag * must equal the module's measured pin, and a click that does not work is never fixed by a flag. * DERIVED ROSTER (y23 branch fix): the preview roster below used to be a HARDCODED name list * (['y12','y14','y8','y6','y10']) with `checked === 5` asserted alongside it — so the gate counted * nothing real; it would say 5 forever no matter how many preview slots PARK_CROSSINGS actually * carried. It silently stopped covering y21 the moment y21 was seated, and was about to silently * drop y23 too. The roster is now DERIVED straight from CAMP.PARK_CROSSINGS: every slot with * `ship === false && open === true`, full stop. A new preview slot is swept in automatically; it * only needs its module's SHIPPABLE pin wired into PIN below (the loop below will name it if you * forget). */ test('CAMP-SHIP-UNMOVED: every ship:false + open:true preview slot agrees with its module pin', () => { const PIN = { // the modules' OWN measured shippability, y12: E.PARK_STONES_SHIPPABLE, y14: E.PARK_TOLL_SHIPPABLE, y10: E.PARK_FLOOD_SHIPPABLE, y18: E.PARK_MINE_SHIPPABLE, y23: E.PARK_STORM_SHIPPABLE, // one entry per KNOWN preview slot // y19 left this map on 2026-08-04 the way y29/y31/y51 did below — UNSEATED, not promoted. The // tower module and E.PARK_TOWER_SHIPPABLE stay; the derived roster no longer hands this loop // the id, so the entry would be dead data. y24: E.PARK_LANT_SHIPPABLE, // y29 and y31 left this map on 2026-07-29 the way y51 did below — UNSEATED, not promoted. Their // modules and E.PARK_{STATUE,RELAY}_SHIPPABLE stay; the derived roster no longer hands this loop // either id, so the entries would be dead data. // y46 left this map on 2026-07-26 — it is SHIPPED now, so it is no longer a preview slot and the // derived roster below will not hand it to this loop. Its pins are asserted by Y46-SIEGE-SHIP-GATE. // y33 left this map on 2026-07-31 — PROMOTED, the same way y46 left it. The correction was to // its KIND (m1 -> m3): the footbridge was built to ask caution-vs-care and was being measured // on goal-vs-caution. Its pins are asserted by Y33-YIELD-SHIP-GATE. y50: E.PARK_ALLEY_SHIPPABLE, // y51 left this map on 2026-07-28 — the same way y46 left it, but for the opposite reason: y46 // was PROMOTED off the preview shelf, y51 was UNSEATED off the board entirely (its module // admits none of its own cells, so FIELD-REGISTRY-SURFACE refuses the seat). Either way the // derived roster below no longer hands it to this loop. y54: E.PARK_SHIFTER_SHIPPABLE, y59: E.PARK_PLAZA_SHIPPABLE, // y59 — SEATED 2026-08-04. 모듈 핀을 그대로 건다(y50·y54 와 같은 // 모양). 이 핀은 파생이 아니라 리터럴 false 이고 그 사실이 engine.js 상수 주석에 적혀 있다 — // 쌍 측정은 스펙 §9 가 이연했으므로 "아직 안 쟀다"를 "재 봤고 미달"인 척 적지 않는다. // y57 은 필드 모듈이 없는 걷기 플레이 다리라 `E.PARK_*_SHIPPABLE` 이 없다. 대신 슬롯 자신의 // 배치 바를 파생한 CAMP.PARK_Y57_SHIPPABLE 을 건다 — y8·y33·y46 이 이미 쓰는 그 자리다. // 리터럴 false 를 적지 않는 것이 요점이다(y50 이 그것으로 "아직 못 잰다"와 "재 봤고 미달"을 // 구별 못 했다). y58: E.PARK_ROAD_SHIPPABLE, // Task 8: road 필드 모듈 자신의 승격 핀(Task 6, derive-never-assert). // 크로싱 PAIRING 실측은 Task 9 로 이월 — 여기 거는 것은 모듈 바뿐이다. }; const previews = CAMP.PARK_CROSSINGS.filter(c => c.ship === false && c.open === true); // DERIVED, // never a fixed name list — see the header note above for why that distinction is the whole fix. assert.ok(previews.length > 0, 'no preview slots found — PARK_CROSSINGS shape changed under this gate'); let checked = 0; for (const cx of previews) { const id = cx.id; assert.ok(id in PIN, `preview slot ${id} has no module SHIPPABLE pin wired into this gate's PIN ` + 'map — a new preview slot must arrive with its module measurement registered here, not silently ' + 'uncovered (that is the exact defect this gate was rewritten to stop repeating)'); assert.strictEqual(cx.ship, false, `${id}.ship is ${cx.ship} — opening a slot for PREVIEW play must NEVER flip its ship flag. ` + '`ship` means "earned the 6/6 blind order-recovery bar" and ONLY a measurement may set it; ' + '`open` means "seatable in the unscored picker, and marked". They are orthogonal on purpose.'); assert.strictEqual(cx.open, true, `${id} should be an open preview (Task 9 §A)`); assert.strictEqual(cx.ship, PIN[id], `${id}.ship disagrees with its module's own measured pin (${PIN[id]}) — the slot flag and the ` + 'measurement have drifted apart'); checked++; } assert.strictEqual(checked, previews.length, 'a preview slot went unchecked'); // cross-check from the total: every non-shipped slot must be one of these previews (CAMP-SLOTS- // SEATABLE already pins inert === 0 elsewhere; this is the same invariant seen from this gate). assert.strictEqual(previews.length + CAMP.PARK_CROSSINGS.filter(c => c.ship).length, CAMP.PARK_CROSSINGS.length, 'preview + shipped slots do not account for the whole roster — a slot ' + 'is neither ship:true nor a marked (ship:false + open:true) preview'); // and the shipped six are untouched in the other direction: still ship, never merely open. const shipped = CAMP.PARK_CROSSINGS.filter(c => c.ship); // 17 -> 18 (2026-07-31, y56): the C-N seat on y16's carry yard. Bumped BY HAND on purpose — // this number is how a silent promotion gets caught, so it must cost a deliberate edit. assert.strictEqual(shipped.length, 18, 'the shipped lineup changed size — a measurement claim moved'); for (const c of shipped) assert.ok(!c.open, `${c.id} is BOTH ship and open — a measured crossing is not a preview; ` + 'the flags would contradict each other on every surface that paints them'); // y3 was the ONLY field cell that had earned the bar (Task 10); y16 is the SECOND (Task 3), and this // list moved WITH its measurement, not ahead of one — y16 recovers the persona ORDER blind on 6/6 // personas x every gate seed, CALIBRATED at PARK_CAL_TURNS (the window the readout actually uses), // and PARK_CARRY_SHIPPABLE is the pin that says so (Y16-CARRY-SHIP-GATE derives the flag FROM the // count rather than asserting it alongside). The rule is unchanged and is the whole point of this // gate: a new name here must arrive with a calibrated PARK_*_SHIPPABLE = true, never with a flag. const shippedFields = shipped.filter(c => c.playMech && c.playMech.fieldMech).map(c => c.id); // xp/xs JOINED THIS LIST 2026-08-01, and they arrived the way this gate demands. They were already // ship:true — what changed is their PLAY leg, from the `push` and `slide` verb modules to the // `ledge` and `yield` field mechanics, so they now appear here as field cells. The reason for the // move is measured and exhaustive: over every admissible cell either verb can mint, the full // three-way order was recovered 0/384 times on push (64 cells; C-N posed on 0 of them) and 0/42 on // slide (7 cells). Both cells were LIVE while ending every single playthrough "읽을 수 없음", // because the promotion bar reads the kind's den pair alone and the screen reads the whole order. // Their new legs carry pins that were already derived and already true for y26 and y33, asserted // just below — no new claim was minted for this move, an existing measurement was reused. // 2026-08-03: y3 · y22 · y26 · y33 이 이 목록에서 빠진 것은 바가 떨어져서가 아니라 그 네 슬롯이 // 로스터에서 삭제되었기 때문이다. 그들이 타던 모듈 중 셋(downed · ledge · yield)은 살아 있다 — // y52 의 시연 다리와 xp · xs 의 플레이 다리가 각각 그 위에 서 있어서다. 바가 내려간 이름은 없다. assert.deepStrictEqual(shippedFields, ['xp', 'xs', 'y8', 'y16', 'y56', 'y17', 'y55', 'y20', 'y46', 'y53', 'y52'], `shipped FIELD cells are ${JSON.stringify(shippedFields)} — these are the field cells that clear the ` + 'CALIBRATED 6/6 blind order-recovery bar. A new name here means somebody shipped a cell; it must ' + 'come with the measurement (a calibrated PARK_*_SHIPPABLE = true), not with a flag.'); assert.strictEqual(E.PARK_LEDGE_SHIPPABLE, true, 'xp now plays on the ledge yard, so ledge\'s own derived pin must be the measurement that keeps it live'); assert.strictEqual(E.PARK_YIELD_SHIPPABLE, true, 'xs now plays on the yield yard, so yield\'s own derived pin must be the measurement that keeps it live'); assert.strictEqual(E.PARK_CARRY_SHIPPABLE, true, 'y16 sits in the shipped-field list, so its module pin must be the measurement that put it there'); assert.strictEqual(CAMP.PARK_Y55_SHIPPABLE, true, 'y55 sits in the shipped-field list, so its PAIRING measurement (PARK_Y55_SHIPPABLE) must be the ' + 'thing that put it there — it shares y17\'s fire yard and its own module pin is y17\'s, so the ' + 'crossing bar is the only measurement that is about THIS seat'); assert.strictEqual(CAMP.PARK_Y33_SHIPPABLE, true, 'y33 sits in the shipped-field list, so its PAIRING measurement (PARK_Y33_SHIPPABLE) must be the ' + 'thing that put it there — the module pin PARK_YIELD_SHIPPABLE is admission + calibrated recovery ' + 'on ONE cell, while the crossing bar is the incongruence sweep and the surface-mimic control'); assert.strictEqual(E.PARK_BOMB_SHIPPABLE, true, 'y20 sits in the shipped-field list, so its module pin must be the measurement that put it there'); assert.strictEqual(CAMP.PARK_Y22_SHIPPABLE, true, 'y22 sits in the shipped-field list, so its PAIRING measurement (PARK_Y22_SHIPPABLE) must be the ' + 'thing that put it there — the module pin PARK_BOMB2_SHIPPABLE does not cover the soko-demo cross'); assert.strictEqual(CAMP.PARK_Y46_SHIPPABLE, true, 'y46 sits in the shipped-field list, so its PAIRING measurement (PARK_Y46_SHIPPABLE) must be the ' + 'thing that put it there — the module pin PARK_SIEGE_SHIPPABLE is the other half, not the whole'); assert.strictEqual(E.PARK_SIEGE_SHIPPABLE, true, 'y46 ships, so its module pin must be the calibrated measurement that says so'); console.log(` [CAMP-SHIP-UNMOVED] ${previews.length} previews (${previews.map(c => c.id).join(',')}) ` + `still ship:false + open:true (pins ${Object.entries(PIN).map(([k, v]) => k + '=' + v).join(' ')}); ` + `${shipped.length} shipped, none marked open; shipped field cells: ${shippedFields.join(',')}`); }); /* ---- GATE: PARK-SHIP-BAR-CALIBRATED — the ship bar measures what the READOUT reads (Task 10) ---- * THE DEFECT THIS EXISTS TO PREVENT FROM EVER COMING BACK, stated plainly: * The product discards the first PARK_CAL_TURNS (=2) player turns of every judged episode — the * player is still finding the controls, so those turns are excluded from the violation tally and * from EVERY blind readout denominator (P3a §4). campaign.js's readout obeys that on every row it * scores. The engine's SHIP BARS — the predicates that decide whether a cell may CLAIM it reads a * player — did not: they called parkRecoverOrder with no skip. So the bar and the readout measured * DIFFERENT GAMES, and a cell could earn `ship: true` on evidence the product throws away. y8 did * exactly that: 6/6 blind order recovery uncalibrated, 0/6 calibrated. A false claim, shipped. * THE INVARIANT: for EVERY module, `PARK_*_SHIPPABLE` equals the CALIBRATED 6/6 blind order recovery * on that module's shipped seed — where "calibrated" means AT THE CAMPAIGN'S OWN CONSTANT. The skip * is deliberately reached through CAMP.PARK_CAL_TURNS, the readout's side of the wire, and never * through the engine's: if the two ever drift apart again, the recomputation below stops matching * the pins and this gate fails. That is the whole point — it is a gate against a SPLIT, so it must * hold one end in each hand. * NON-VACUITY, asserted three ways: (a) the calibration window must be ACTIVE — at least one module * has an award INSIDE the span (turn <= cal) that the calibrated read discards (y6; re-derived * 2026-07-18 — it used to be "at least one module's 6/6 VERDICT flips", y8, but y8's corridor rebuild * made it pass calibrated, and no shipped module's verdict flips on any seed now); (b) at least one * pin is TRUE (y3 — otherwise "all false" would pass trivially); (c) at least one is FALSE. * It prints the full uncalibrated-vs-calibrated table, so the split is visible, not merely absent. */ test('PARK-SHIP-BAR-CALIBRATED: every module ship pin == calibrated 6/6 recovery at the READOUT\'s own skip', () => { // ONE CONSTANT, TWO NAMES. campaign.js consumes E.PARK_CAL_TURNS and re-exports it; if a future // edit re-declares a literal on either side, this is the tripwire. assert.strictEqual(CAMP.PARK_CAL_TURNS, E.PARK_CAL_TURNS, `the campaign scores rows at skip=${CAMP.PARK_CAL_TURNS} but the engine's ship bars read at ` + `skip=${E.PARK_CAL_TURNS} — that is the Task 10 defect exactly: the bar and the readout are ` + 'measuring different games, and a cell can earn ship:true on evidence the product discards.'); assert.ok(E.PARK_CAL_TURNS > 0, 'the calibration window is 0 — every claim below is then vacuous'); const cal = CAMP.PARK_CAL_TURNS; // THE READOUT'S SKIP. Reached from its side. const MODS = [ ['y12 stones', E._parkStonesRecovers, E._parkStonesCell, E._parkStonesBuild, E.PARK_STONES_SHIP_SEED, E.PARK_STONES_SHIPPABLE], ['y14 toll ', E._parkTollRecovers, E._parkTollCell, E._parkTollBuild, E.PARK_TOLL_SHIP_SEED, E.PARK_TOLL_SHIPPABLE], ['y3 downed', E._parkDownedRecovers, E._parkDownedCell, E._parkDownedBuild, E.PARK_DOWNED_SHIP_SEED, E.PARK_DOWNED_SHIPPABLE], ['y8 log ', E._parkLogRecovers, E._parkLogCell, E._parkLogBuild, E.PARK_LOG_SHIP_SEED, E.PARK_LOG_SHIPPABLE], ['y10 flood ', E._parkFloodRecovers, E._parkFloodCell, E._parkFloodBuild, E.PARK_FLOOD_SHIP_SEED, E.PARK_FLOOD_SHIPPABLE], ]; let flipped = 0, trues = 0, falses = 0, spanAwards = 0; const spanWitness = []; const table = []; for (const [name, recovers, cellFn, build, seed, pin] of MODS) { const cell = cellFn(seed); // recompute BOTH reads from scratch, out of the shipped recovery stack, on this module's cell let ok0 = 0, okCal = 0, inSpan = 0; for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(build(cell), persona); if (P.reason !== 'complete') continue; const r0 = E.parkRecoverOrder(build(cell), P.moves); // what the OLD bar read const rc = E.parkRecoverOrder(build(cell), P.moves, cal); // what the READOUT reads if (r0 && r0.join() === persona.join()) ok0++; if (rc && rc.join() === persona.join()) okCal++; for (const w of P.awards || []) if (w.turn <= cal) inSpan++; // awards the window DISCARDS } spanAwards += inSpan; if (inSpan > 0) spanWitness.push(`${name.trim()} ${inSpan}`); const n = E.PARK_PERSONAS.length; const uncal6 = ok0 === n, cal6 = okCal === n; // (1) THE PIN IS THE CALIBRATED READ — not the uncalibrated one. assert.strictEqual(pin, cal6, `${name.trim()}: PARK_*_SHIPPABLE is ${pin} but the CALIBRATED 6/6 blind order recovery on its ` + `shipped seed is ${okCal}/${n} (uncalibrated: ${ok0}/${n}). The pin must equal the read the ` + 'PRODUCT makes. If the calibrated number went UP, this cell has earned the bar — re-measure the ' + 'full sweep and flip the pin AND the slot together (and fold the bar into its admission, since ' + 'it now ships). If it went DOWN, the cell has lost it — demote the slot to a marked preview and ' + 'UNFOLD the bar from admission. Never edit the skip to make this pass.'); // (2) THE MODULE'S OWN BAR READS AT THAT SKIP TOO — it is not a second, laxer channel. assert.strictEqual(recovers(cellFn(seed)), cal6, `${name.trim()}: the module's _park*Recovers predicate disagrees with a calibrated recovery ` + 'recomputed at the readout\'s skip — the ship bar is reading a different game again.'); if (uncal6 !== cal6) flipped++; pin ? trues++ : falses++; table.push(`${name} skip0 ${ok0}/${n} -> skip${cal} ${okCal}/${n} pin=${pin}${uncal6 !== cal6 ? ' <-- CALIBRATION DECIDES IT' : ''}`); } // NON-VACUITY (a) — RE-DERIVED (redesign 2026-07-18). The window must actually be ACTIVE, or this // gate is a no-op that would pass even on code that skipped calibration. It USED to prove this by a // VERDICT FLIP: y8 was 6/6 uncalibrated but 0/6 calibrated, so the skip decided its verdict. The // neutral-corridor rebuild fixed y8 to pass calibrated (6/6 -> 6/6), and MEASUREMENT (this branch, // seeds 1..24) shows no shipped module's 6/6 verdict now flips on ANY seed — the flip-witness is // gone, and the plan's fallback candidates (y10/y12) do NOT differ on their ship seed either. // So the non-vacuity is re-grounded one level down, at the DATA the window discards: at least one // module must have awards INSIDE the calibration span (turn <= cal), proving the skip is not empty. // y6 (duck) supplies them (earliest award turn 1, several in-span) — its verdict does not flip // (2/6 either way) but the calibrated read genuinely throws those awards away. If THIS trips, the // window discards nothing anywhere and the calibrated read is provably identical to the naive one: // find a module with an in-span award, or re-derive again — do not simply delete the assertion. assert.ok(spanAwards > 0, `NON-VACUOUS: no shipped module has a single award inside the calibration span (turn <= ${cal}) — ` + 'the calibrated read is then byte-identical to the uncalibrated one and this gate proves nothing. ' + `Witnesses expected (module inSpan): ${spanWitness.join(', ') || 'NONE'}.`); // (a-note) flipped is now 0 by design: y8 — the module the calibration used to DECIDE — was rebuilt // to clear the bar calibrated, so the window no longer flips any shipped verdict. The `pin === cal6` // asserts above still hold each module's pin to its calibrated read; this clause keeps the window // itself demonstrably active. // SYNTHETIC SKIP-CONSUMPTION WITNESS (restores machinery-level teeth, 2026-07-23). `flipped` is 0 // and stays 0: since the corridor rebuild every shipped cell recovers its order PAST the span, so no // module's 6/6 verdict differs between the uncalibrated and calibrated reads — the natural flip that // used to give `pin === cal6` its teeth is gone, and a regression making parkRecoverOrder IGNORE its // skip argument would slip past the asserts above unnoticed. This closes that hole at the machinery // level on a real recoverable episode: recovery HOLDS with no skip and FAILS once the skip swallows // the awards that decide it, proving the skip is genuinely consumed. (A module-verdict flip at // skip=cal specifically no longer exists to pin — this is the strongest witness still available; if a // future cell ever loses the bar under calibration again, restore the verdict-level flip check too.) { const wCell = E._parkDownedCell(E.PARK_DOWNED_SHIP_SEED); // a SHIPPED cell — recovers 6/6 const wPersona = E.PARK_PERSONAS[0]; const wP = E.parkPlayout(E._parkDownedBuild(wCell), wPersona); const r0 = E.parkRecoverOrder(E._parkDownedBuild(wCell), wP.moves); // skip 0 assert.ok(r0 && r0.join() === wPersona.join(), 'skip-consumption witness: the reference episode (y3 downed, ship seed) must recover with no skip'); const maxTurn = Math.max(0, ...wP.awards.map(w => w.turn)); // past EVERY award const rSkip = E.parkRecoverOrder(E._parkDownedBuild(wCell), wP.moves, maxTurn); assert.ok(!(rSkip && rSkip.join() === wPersona.join()), `skip-consumption witness: recovery still succeeded with skip=${maxTurn} (past every award, turn <= ` + `${maxTurn}) — parkRecoverOrder is IGNORING its skip, so the calibrated recompute above is identical ` + 'to the uncalibrated one and pin===cal6 has lost its teeth. Do not weaken the skip handling to pass.'); } // NON-VACUITY (b)+(c): both verdicts are represented, so "all true" or "all false" cannot pass. assert.ok(trues > 0, 'no module ships — a gate over an all-false table cannot catch a bar that is too LAX'); assert.ok(falses > 0, 'every module ships — a gate over an all-true table cannot catch a bar that is too STRICT'); console.log(` [PARK-SHIP-BAR-CALIBRATED] readout skip = CAMP.PARK_CAL_TURNS = ${cal} = E.PARK_CAL_TURNS ` + `(one constant, no drift). ${MODS.length} modules, ship pin == calibrated 6/6 on every one; ` + `${flipped} module(s) whose verdict the calibration DECIDES (0 since y8's corridor rebuild); ` + `${trues} ship, ${falses} preview; ${spanAwards} award(s) discarded by the window ` + `(${spanWitness.join(', ') || 'none'}) — the calibration is still ACTIVE:\n` + table.map(t => ' ' + t).join('\n')); }); /* ---- GATE: CAMP-READOUT-HONESTY — the readout never asserts an order it cannot support (Task 9 §B) * THE HOLE THIS CLOSES: the engine has always been honest — parkRecoverOrder returns null when the * trajectory does not determine an order, and "confidently wrong" is 0/6 on every slot. The SCREEN * was not. drawParkTaskReport read row.posterior.map — and a posterior ALWAYS names a MAP, even at a * dead tie — then stamped ✓ 순서 일치 / ✗ 순서 다름 on it. Measured on the live picker: xp's blind * posterior comes out {goal>safety>care: 1/3, goal>care>safety: 1/3, care>goal>safety: 1/3, ...}, * three personas tied, the "winner" picked by object key order. 2 of xp's 6 "order match" verdicts * were coin flips. Nothing in this suite could catch that, because nothing asserted on the readout's * VERDICT — only on the reads underneath it. * * WHAT IT PINS. The verdict is UNDETERMINED exactly when the walk determines no order: * row.orderRead.determined === (parkRecoverOrder(fresh board, moves, cal) !== null * AND the printed MAP is not TIED with another persona) * recomputed here from scratch, on a fresh board, off the trajectory the seated task actually * produced. Both clauses matter and they are different failures — an undecided pair is missing * evidence, a tied posterior is evidence that discriminates nothing — and either one must silence * the ✓/✗. * It also pins that an undetermined row can SAY WHY (orderRead.missingPairs: the conflict pairs the * walk never posed, computed via parkPairExpressed, never hardcoded per slot), and that a determined * row's recovered order AGREES with parkRecoverOrder — i.e. the headline the screen prints * (posterior.map) is not merely un-tied but is the SAME order the honest blind reader returns. * * NON-VACUITY: it asserts BOTH branches are exercised — at least one slot determined and at least * one undetermined (today, on the faithful oracle trajectory: xp/xs/y12/y14/y8/y10 come out * undetermined, x4/x5/x7/y3/y6 determined) — so neither arm can rot into a no-op. It prints the * full per-slot table. * (TASK 10 moved y8 across this line, and it is worth seeing why: y8 is UNDETERMINED here because * the readout reads at cal = PARK_CAL_TURNS, and calibrated, y8's walk decides no order. It was * ALREADY undetermined on this screen while the slot still said ship:true — the readout was honest * about a cell whose ship bar was not. That contradiction, visible right here in this table, is the * defect Task 10 closed: the bar now reads what this gate reads.) * THIS IS NOT A SHIP BAR. An undetermined readout is not a bug in the cell; it is the truth about * the cell, and 4 of the 6 SHIPPED slots (xp, xs, x4, x7 partially) live on the same edge. The gate * asserts the SCREEN is honest, never that every cell is readable. */ test('CAMP-READOUT-HONESTY: the readout is UNDETERMINED exactly when the walk determines no order', () => { const SEED = 1, cal = CAMP.PARK_CAL_TURNS; let det = 0, und = 0; const table = []; for (const cx of CAMP.PARK_CROSSINGS) { const run = CAMP.createRun({ seed: SEED, parkMode: true }); const t = CAMP.runParkCrossing(run, 'cx:' + cx.id); assert.ok(t, `slot ${cx.id} did not seat — see CAMP-CROSS-SWEEP`); // The player is the crossing's OWN persona playing FAITHFULLY (its oracle walk) — the most // favorable trajectory the cell can ever get. If the order cannot be read off THIS one, no // player's walk will do better, and the screen must not pretend otherwise. const board = () => CAMP._parkCrossBoard(t.tile.kind, t.playCell); const F = E.parkPlayout(board(), t.persona); for (const mv of F.moves) { if (!run.park.task || run.park.task.game.P.over) break; CAMP.parkTaskMove(run, mv); } const row = run.park.results[t.tile.id]; assert.ok(row, `slot ${cx.id}: the seated task stored no row`); const or = row.orderRead; assert.ok(or, `slot ${cx.id}: the row carries no orderRead — the honesty gate is not wired into _parkTaskRow`); // (1) the row's own wiring: determined = decided AND (closure OR untied). // The tie clause guards the posterior.map headline; a closure row prints its OWN recovered // order instead, so a tie in that other estimator has no say over it (2026-08-02). assert.strictEqual(or.determined, or.recovered != null && (or.viaClosure || or.tiedWith.length === 0), `slot ${cx.id}: orderRead.determined disagrees with its own evidence`); // (2) INDEPENDENT recomputation on a FRESH board off the trajectory the game actually produced. const moves = t.game.P.moves; const strictTruth = E.parkRecoverOrder(board(), moves, cal); const closedTruth = E.parkRecoverOrderClosed(board(), moves, cal); const truth = strictTruth || closedTruth; assert.deepStrictEqual(or.strict, strictTruth, `slot ${cx.id}: the row's STRICT order is not what parkRecoverOrder says on a fresh board`); assert.deepStrictEqual(or.recovered, truth, `slot ${cx.id}: the row's recovered order is not what the blind reader says on a fresh board — ` + 'the readout is reading something other than the honest blind reader'); assert.strictEqual(or.viaClosure, strictTruth == null && truth != null, `slot ${cx.id}: viaClosure disagrees with which reader actually answered`); // (2b) THE CLOSURE IS AN EXTENSION, NOT A DIFFERENT READING. Wherever the strict reader // answers, the closed one must answer the SAME order — otherwise "it only adds rows" is a // claim and not a fact, and every determined row on the product would be up for re-litigation. if (strictTruth) assert.deepStrictEqual(closedTruth, strictTruth, `slot ${cx.id}: the closed reader contradicts the strict one — closure must only ADD readings`); // (3) determined => the screen may assert, and what it prints (posterior.map) IS the honest // reader's order. undetermined => the screen must be able to say WHY. if (or.determined) { det++; assert.ok(truth != null, `slot ${cx.id}: determined but no reader recovered an order`); // WHAT THE HEADLINE PRINTS must be the order this row used to decide it was readable. // Strict rows print posterior.map (unchanged); closure rows print their own recovered order. const headline = or.viaClosure ? or.recovered : row.posterior.map; assert.deepStrictEqual(headline, truth, `slot ${cx.id}: the headline would print ${headline.join('>')} but the blind reader ` + `recovers ${truth.join('>')} — a determined verdict must not print a different order`); } else { und++; assert.ok(Array.isArray(or.missingPairs), `slot ${cx.id}: undetermined with no missingPairs — the readout could not name a reason`); assert.ok(truth == null || or.tiedWith.length > 0, `slot ${cx.id}: undetermined for no stated reason (the order IS decided and the MAP is untied)`); } table.push(`${cx.id}${cx.ship ? '' : '~preview'}:${or.determined ? 'READ ' + or.recovered.map(a => a[0].toUpperCase()).join('>') : 'UNDET' + (or.tiedWith.length ? `(tie x${or.tiedWith.length + 1})` : '') + (or.missingPairs.length ? '[' + or.missingPairs.map(p => p[0][0].toUpperCase() + '-' + p[1][0].toUpperCase()).join(',') + ' unposed]' : '')}`); } // NON-VACUITY: both arms exercised. If either trips, the gate has stopped testing anything — // either every cell became readable (wonderful; re-pin this) or the honesty branch died. assert.ok(det > 0, 'no slot came out DETERMINED — the ✓/✗ verdict arm is untested'); assert.ok(und > 0, 'no slot came out UNDETERMINED — the honesty arm is untested and would rot silently'); console.log(` [CAMP-READOUT-HONESTY] seed ${SEED}, faithful-oracle play, cal ${cal}: ` + `${det} determined / ${und} undetermined of ${CAMP.PARK_CROSSINGS.length} slots`); for (const r of table) console.log(` ${r}`); }); /* ---- GATE: CAMP-PAIR-READ — the pair-grain readout rows (2026-07-28) ---- * THE HOLE THIS CLOSES: an UNDETERMINED verdict threw away everything the walk DID support. A * walk that decides goal-vs-safety but never poses safety-vs-care printed one pooled "cannot * read" line; the readable inequality was computed (parkPairMarginal null-rejects it) and then * discarded by the screen. row.orderRead.pairs now carries one entry per conflict pair; this * gate pins that those entries are honest. * * WHAT IT PINS, per slot (same seat + faithful-oracle walk as CAMP-READOUT-HONESTY above): * (1) wiring: pairs has exactly the three att pairs GC/GN/CN in that order; * (2) independence: expressed / decided / hiAxis / loAxis all recompute IDENTICALLY on a fresh * board from the trajectory (parkPairExpressed + parkPairMarginal over a fresh widened * posterior at the same cal) — the row stores reads, it never invents them; * (3) honesty at pair grain: decided ONLY when expressed > 0 AND the marginal null-rejects — * the same recovery predicate the ship bars ride, no new threshold anywhere; * (4) derivation: missingPairs === the unposed subset of pairs (axes, same order) — the Task 9 * field is now DERIVED from the pair rows and must not drift from them; * (5) policy: _parkPairState is total on the three states and maps decided->read, * expressed-but-undecided->tied, expressed==0->unposed. * NON-VACUITY (measured 2026-07-28, seed 1, faithful-oracle play, cal 2 — re-pin on change): * read 77 / tied 0 / unposed 16 over 93 pair rows, 13 PARTIAL slots (some pair read, some not — * the exact middle ground the screen previously collapsed to "cannot read"; xp's posterior is * three-way TIED yet its G-C pair reads). The gate asserts read > 0, unposed > 0, partial > 0. * 'tied' has NO faithful-walk witness: a faithful walk's expressed pair null-rejects, so tied is * the noisy/human-walk state — its policy arm is pinned on a synthetic row below instead, and * the census line will show it the day a trajectory witnesses it. decided-vs-MAP agreement is * printed, not asserted (77/0 measured: the MAP order and a pair marginal are different * summaries of one posterior and MAY disagree on a low-mass pair without either being wrong). */ test('CAMP-PAIR-READ: per-pair rows recompute independently and the three states all occur', () => { const SEED = 1, cal = CAMP.PARK_CAL_TURNS; const PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; let nRead = 0, nTied = 0, nUnposed = 0, partial = 0, agree = 0, disagree = 0; const table = []; for (const cx of CAMP.PARK_CROSSINGS) { const run = CAMP.createRun({ seed: SEED, parkMode: true }); const t = CAMP.runParkCrossing(run, 'cx:' + cx.id); assert.ok(t, `slot ${cx.id} did not seat — see CAMP-CROSS-SWEEP`); const board = () => CAMP._parkCrossBoard(t.tile.kind, t.playCell); const F = E.parkPlayout(board(), t.persona); for (const mv of F.moves) { if (!run.park.task || run.park.task.game.P.over) break; CAMP.parkTaskMove(run, mv); } const row = run.park.results[t.tile.id]; assert.ok(row, `slot ${cx.id}: the seated task stored no row`); const or = row.orderRead; assert.ok(or && Array.isArray(or.pairs) && or.pairs.length === 3, `slot ${cx.id}: orderRead carries no 3-entry pairs array — the pair-grain readout is unwired`); const moves = t.game.P.moves; const post = E.parkPosteriorSet(board(), moves, null, cal); const states = []; or.pairs.forEach((q, i) => { const p = PAIRS[i]; assert.strictEqual(q.pair, p.join(''), `slot ${cx.id} pair ${i}: wrong pair key`); assert.deepStrictEqual(q.axes, [E.PARK_ATT_AXIS[p[0]], E.PARK_ATT_AXIS[p[1]]], `slot ${cx.id} pair ${q.pair}: axes drifted from the att->axis map`); const expressed = E.parkPairExpressed(board(), moves, p, { skip: cal }); assert.strictEqual(q.expressed, expressed, `slot ${cx.id} pair ${q.pair}: stored expressed ${q.expressed} != fresh recount ${expressed}`); const m = E.parkPairMarginal(post, p); assert.strictEqual(q.decided, expressed > 0 && m.nullRejected, `slot ${cx.id} pair ${q.pair}: decided is not (expressed AND null-rejected)`); assert.strictEqual(q.hiAxis, E.PARK_ATT_AXIS[m.hi], `slot ${cx.id} pair ${q.pair}: hiAxis is not the fresh marginal's winner`); assert.strictEqual(q.loAxis, E.PARK_ATT_AXIS[m.lo], `slot ${cx.id} pair ${q.pair}: loAxis is not the fresh marginal's loser`); assert.ok(approx(q.margin, Math.max(m.pFwd, m.pRev)), `slot ${cx.id} pair ${q.pair}: margin drifted from the fresh marginal`); const st = CAMP._parkPairState(q); assert.strictEqual(st, q.expressed === 0 ? 'unposed' : (q.decided ? 'read' : 'tied'), `slot ${cx.id} pair ${q.pair}: _parkPairState broke the three-state policy`); states.push(st); if (st === 'read') nRead++; else if (st === 'tied') nTied++; else nUnposed++; if (q.decided) { (row.posterior.map.indexOf(q.hiAxis) < row.posterior.map.indexOf(q.loAxis)) ? agree++ : disagree++; } }); assert.deepStrictEqual(or.missingPairs, or.pairs.filter(q => q.expressed === 0).map(q => q.axes), `slot ${cx.id}: missingPairs is not the unposed subset of pairs — the derivation drifted`); if (states.includes('read') && (states.includes('tied') || states.includes('unposed'))) partial++; table.push(`${cx.id}${cx.ship ? '' : '~preview'}:` + or.pairs.map((q, i) => `${q.pair}=${states[i]}`).join(',')); } // NON-VACUITY: every display state has a live witness, and the middle ground exists — a slot // whose walk reads SOME pair while another pair stays unread. Without these, the pair rows // could rot into a fourth copy of the binary verdict and this gate would not notice. assert.ok(nRead > 0, 'no pair anywhere came out READ — the inequality arm is untested'); assert.ok(nUnposed > 0, 'no pair anywhere came out UNPOSED — the never-met arm is untested'); assert.ok(partial > 0, 'no slot mixes read and unread pairs — the middle ground this feature exists for has no witness'); // the tied arm has no faithful-walk witness (0 measured above) — pin the policy directly so it // cannot rot while unwitnessed. All three arms, so the policy stays total. assert.strictEqual(CAMP._parkPairState({ expressed: 3, decided: false }), 'tied', '_parkPairState: expressed-but-undecided must read as tied'); assert.strictEqual(CAMP._parkPairState({ expressed: 0, decided: false }), 'unposed', '_parkPairState: expressed == 0 must read as unposed'); assert.strictEqual(CAMP._parkPairState({ expressed: 1, decided: true }), 'read', '_parkPairState: a decided pair must read as read'); console.log(` [CAMP-PAIR-READ] seed ${SEED}, cal ${cal}: read ${nRead} / tied ${nTied} / ` + `unposed ${nUnposed} over ${CAMP.PARK_CROSSINGS.length * 3} pair rows; ${partial} partial slots; ` + `decided-vs-MAP agree ${agree} / disagree ${disagree}`); for (const r of table) console.log(` ${r}`); }); /* ---- Task 1 (P1 seam): the crossing DEMO leg can mint module cells ---- */ // CROSS-DEMO-SEAM: (a) is a GOLDEN byte-identity guard, not a purity check. Comparing // parkCrossings(11) against a SECOND call of parkCrossings(11) would only prove the function is // pure — it would pass even if the new seam rewrote every legacy (walk) demo cell into something // else, which is exactly the regression this test exists to catch. So the guard is external: // fixtures/pre-p1-demo-cells.json was dumped from main BEFORE the seam existed (commit 089632e) — // 11 entries { slot, demoCell } in parkCrossings(11) order. Every one of today's slots must still // serialize byte-identically off the golden snapshot. // // TASK 2 (P1 re-pair) AMENDS THIS, HONESTLY: xp and xs are now EXCLUDED BY NAME, because their demo // legs were DELIBERATELY re-paired onto modules (xp -> slide/ice, xs -> push/Sokoban) and a module // demo cell is SUPPOSED to differ from the pre-P1 walk one — that divergence IS the task. The // exclusion is a NAMED, CLOSED list of exactly two slots, so the guard still binds the OTHER NINE // byte-for-byte: a future edit that perturbs any un-re-paired slot's demo cell still fails here. // The golden FILE is untouched (it is the pre-P1 record and is not regenerated); xp/xs's new bars // are measured by CROSS-REPAIR-P1 below, which is what earns them the right to differ. const P1_REPAIRED = ['xp', 'xs']; test('CROSS-DEMO-SEAM: walk demoMech -> byte-identical demo cells (golden snapshot); module demoMech mints module cell', () => { const golden = JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures/pre-p1-demo-cells.json'), 'utf8')); const now = CAMP.parkCrossings(11); // TASK 3 AMENDS THE COUNT CHECK — one-sided, and NOT a loosening. The golden file is the PRE-P1 // record (11 slots, dumped at 089632e) and it is never regenerated. A slot added AFTER it — y16, the // carry cell — has no golden entry, and cannot possibly violate a snapshot that predates it; an // equality check on the length would make "somebody added a crossing" indistinguishable from // "somebody perturbed a legacy demo cell", which are opposite facts. What the guard OWES is that // every slot IN the golden is still present and still byte-identical, and both halves still bind at // full strength below (a vanished golden slot fails on `missing`; a drifted one fails on the byte // compare). The additions are pinned BY NAME so the count can never drift silently either. assert.ok(now.length >= golden.length, `parkCrossings lost slots: ${now.length} < ${golden.length} golden — a crossing VANISHED`); const goldenIds = new Set(golden.map(g => g.slot)); const added = now.filter(c => !goldenIds.has(c.slot)).map(c => c.slot); // 'y29' and 'y31' were on this list until 2026-07-29; their crossing slots were dropped (the // modules stay). A slot leaving is as deliberate and as NAMED as a slot arriving. // 'y55' JOINED 2026-07-31 — the m2 seat, and this assertion is the reason it is named here rather // than merely added: it sits directly after y17 in the roster (same fire yard, the other ruler), // so the ORDER below is roster order, not id order. Its demo leg is a plain walk cell like every // other name on this list, so the byte-compare half of this gate is untouched by it. // y56 (2026-07-31): the SECOND seat on y16's carry yard, on the C-N ruler. Named here because a // 'y57' JOINED 2026-08-02 — 이 목록에서 처음으로 **전이 거리 4/5** 인 자리다(기존 최대 3). // 시연이 상자 밀기이고 플레이가 페이즈 걷기라 네 축이 한꺼번에 바뀐다. 정직한 미리보기로 // 앉았고(③ 복원 17/24), 모방자는 능동 clean(탐침 6 · 표현 6 · 누수 0). // new crossing must be a deliberate addition — see the slot's own note in campaign.js. // 'y59' JOINED 2026-08-04 — the `plaza` field cell, seated at the TAIL of the roster, which is why // it is named last here (this list is roster order, not id order). Its demo leg is the plain walk // cell y19 carried, so the byte-compare half of this gate is untouched by it. // 'y19' LEFT this list the same day and in the same commit family — its SLOT was retired (the // tower MODULE stays in engine.js). A slot leaving is as deliberate and as NAMED as a slot // arriving; the two moves cancel in every count this gate's neighbours keep. assert.deepStrictEqual(added, ['y16', 'y56', 'y17', 'y55', 'y18', 'y20', 'y21', 'y22', 'y23', 'y25', 'y26', 'y27', 'y24', 'y32', 'y33', 'y46', 'y50', 'y53', 'y52', 'y54', 'y57', 'y59'], `slots added since the pre-P1 golden are ${JSON.stringify(added)} — a new crossing must be a ` + 'deliberate, NAMED addition here, never a silent one'); let bound = 0, repaired = 0; for (const g of golden) { const slot = now.find(c => c.slot === g.slot); assert.ok(slot, `golden slot ${g.slot} missing from parkCrossings(11)`); if (P1_REPAIRED.indexOf(g.slot) >= 0) { // re-paired on purpose (Task 2): it MUST have moved off the walk golden, and onto a module. repaired++; assert.ok(slot.demoCell.mech.moveMech, `${g.slot} is listed as P1-re-paired but its demo cell has no moveMech — the re-pair silently reverted`); assert.notStrictEqual(JSON.stringify(slot.demoCell), JSON.stringify(g.demoCell), `${g.slot} is listed as P1-re-paired but its demo cell is still byte-identical to the pre-P1 walk golden`); continue; } bound++; assert.strictEqual(JSON.stringify(slot.demoCell), JSON.stringify(g.demoCell), `slot ${g.slot}: demo cell diverged from the pre-P1 golden snapshot — byte stability broken`); } assert.strictEqual(repaired, P1_REPAIRED.length, 'a P1-re-paired slot vanished from the golden set'); assert.strictEqual(bound, golden.length - P1_REPAIRED.length, 'the golden guard stopped binding the un-re-paired slots — the exclusion list must stay exactly xp/xs'); // (b) a module demoMech (moveMech: 'slide') mints through the SLIDE module's own cell surface, // swept from the given seed to the first admissible candidate — never the legacy walk builder. const d = CAMP._parkCrossingDemoCell('m1', { goalMech: 'harvest', safetyMech: 'static', moveMech: 'slide' }, 12345, undefined); assert.strictEqual(d.mech.moveMech, 'slide'); assert.ok(E._parkSlideAdmissible(d), 'demo cell swept to an admissible seed'); }); test('Y20/Y26-DEMO-SHARED-CLAIM: discovery is public; care-over-goal delegates while goal-over-care takes', () => { // TASK 12 (2026-08-05): y46 registered alongside y20. y46's claimOrder was flipped to 'pink' // in Task 11 — the WALKER (seat 0) still triggers both notice stages, but only the two fx // seats were swapped, so the notice SEATS come out pink-then-blue ([1, 0]) while the underlying // claim geometry (who is actually closer to the gem) is untouched. `order` carries that // per-slot declaration order; defaulting to [0, 1] leaves y20 byte-identical to before. for (const spec of [ { id: 'y20', si: 20 }, { id: 'y46', si: 31, kind: 'm3', order: [1, 0] }, // 분홍 선언 -> 파랑 선언 (2026-08-05) ]) { const row = CAMP.PARK_CROSSINGS.find(c => c.id === spec.id); assert.ok(row && row.demoMech.sharedClaim, `${spec.id}: roster must opt into shared-claim staging`); const dSeed = 42 * 61 + 11 + spec.si * 197; const cell = CAMP._parkCrossingDemoCell('m1', row.demoMech, dSeed, undefined); assert.strictEqual(cell.sharedClaim, true, `${spec.id}: concrete demo cell must carry the opt-in`); const order = spec.order || [0, 1]; // per-slot notice declaration order (Task 12) for (const persona of E.PARK_PERSONAS) { const label = `${spec.id}/${persona.join('>')}`; const st = CAMP._parkCrossBoard('m1', cell), P = E.parkStart(st); const shared = st.park.sharedClaim.gem, next = st.park.sharedClaim.next; const seen = []; let decisionWitness = null, delegatedTowardNext = false, delegateDist = null; while (!P.over) { const beforeFx = st.fx.length, beforeStage = P.sharedClaimStage; const beforePos = { ...st.pos[0] }, mv = E.parkOracleMove(P, persona); if (!decisionWitness && beforeStage === 'dual' && E.manhattan(st.pos[0], st.tokens[shared]) === 1) { const tok = st.tokens[shared], legal = E._parkLegal(P); const take = legal.find(c => c.x === tok.x && c.y === tok.y); const cp = E.PARK_ATTITUDES.C.preference(P, legal), np = E.PARK_ATTITUDES.N.preference(P, legal); decisionWitness = { dp: E.manhattan(st.pos[0], tok), dn: E.manhattan(st.pos[1], tok), safetyNeutral: !!take && cp.has(take.k) && [...np].some(k => cp.has(k)) }; } E.parkStep(P, mv); const added = st.fx.slice(beforeFx); for (const f of added) if (f.k === 'notice' || f.k === 'delegate' || f.k === 'gem' || f.k === 'take') seen.push({ turn: P.turns, ...f }); if (added.some(f => f.k === 'delegate')) delegateDist = E.manhattan(st.pos[0], st.tokens[next]); else if (delegateDist != null && (beforePos.x !== st.pos[0].x || beforePos.y !== st.pos[0].y) && E.manhattan(st.pos[0], st.tokens[next]) < delegateDist) delegatedTowardNext = true; } const notices = seen.filter(f => f.k === 'notice'); assert.deepStrictEqual(notices.map(f => f.seat), order, `${label}: notice seats must follow the slot's declared claim order ${JSON.stringify(order)}`); assert.ok(notices[0].turn < notices[1].turn, `${label}: notice beats must be separate`); // NOT branched on `order`, deliberately: this is a claim about GEOMETRY (which seat is // physically closer to the dual-claim gem), not about which seat's notice/fx DECLARES // first. Task 11 reversed only the pink/blue fx declaration order for y46 — it left the // oracle action untouched precisely so blue (seat 0) stays strictly closer at the dual // claim on every slot in this list. Branching this on `order` would silently accept a // scene where declaration order and actual proximity disagree, which Task 11 never built. assert.ok(decisionWitness && decisionWitness.dp < decisionWitness.dn, `${label}: blue must be strictly closer at the dual claim`); assert.strictEqual(decisionWitness.safetyNeutral, true, `${label}: safety must allow both pickup and delegation at the G-N scene`); assert.strictEqual(P.reason, 'complete', `${label}: shared-claim demo must complete`); const careFirst = persona.indexOf('care') < persona.indexOf('goal'); const delegated = seen.some(f => f.k === 'delegate'); const blueTookShared = seen.some(f => f.k === 'gem' && f.x === st.tokens[shared].x && f.y === st.tokens[shared].y); const pinkTookShared = seen.some(f => f.k === 'take' && f.x === st.tokens[shared].x && f.y === st.tokens[shared].y); assert.strictEqual(delegated, careFirst, `${label}: delegation must follow the observed care-over-goal action`); assert.strictEqual(blueTookShared, !careFirst, `${label}: blue shared-gem pickup branch`); assert.strictEqual(pinkTookShared, careFirst, `${label}: pink receives only a delegated shared gem`); if (careFirst) assert.ok(delegatedTowardNext, `${label}: blue must move toward the retargeted gem after delegating`); } } }); // CROSS-DEMO-SEAM-FALLBACK (review finding, plan-mandated): on sweep EXHAUSTION // _parkCrossingDemoCell must NEVER return null (that null used to reach _parkCrossingPlayCell's // cache key, `demoCell.seed >>> 0`, and throw a TypeError that would take down all 11 picker // slots — not just the failing one). It must follow the PLAY leg's own established convention // (parkCrossFallbacks, campaign.js ~3109): bump a LOUD, assertable counter (parkDemoFallbacks) // and still seat a usable cell — the offset-0 candidate. // // To force a GENUINE exhaustion (not a stub of the function under test) this registers a // throwaway field mechanic through the REAL PARK_FIELD_MECHS plug-in registry — the exact same // extension point stones/toll/downed/log/duck/flood use — whose admits() deterministically never // accepts. _parkCrossingDemoCell's sweep then genuinely walks all _PARK_CROSS_SWEEP candidates // and genuinely fails every one; nothing about the fallback logic itself is faked. test('CROSS-DEMO-SEAM-FALLBACK: exhausted module demo sweep bumps parkDemoFallbacks and seats the offset-0 candidate, never null', () => { const FV = 'cross-demo-seam-test-never-admits'; E.PARK_FIELD_MECHS[FV] = { cell: (seed) => ({ seed: seed >>> 0, mech: { fieldMech: FV, goalMech: 'harvest', safetyMech: 'static' } }), admits: () => false, // never admits -> the sweep MUST exhaust }; try { const before = CAMP.parkDemoFallbacks(); const d = CAMP._parkCrossingDemoCell('m1', { fieldMech: FV }, 999, undefined); assert.ok(d, 'exhausted module demo sweep must still seat a cell, never null'); assert.strictEqual(d.mech.fieldMech, FV); assert.strictEqual(d.seed, 999 >>> 0, 'the fallback cell is the offset-0 candidate (t=0 -> seed === dSeed, unmutated)'); // DELTA, never an absolute (CAMP-CROSS-SWEEP precedent) — some other test may already have // bumped the counter before this one runs. assert.strictEqual(CAMP.parkDemoFallbacks() - before, 1, 'exhausted module demo sweep must bump parkDemoFallbacks by exactly 1'); } finally { delete E.PARK_FIELD_MECHS[FV]; // leave the real registry untouched } }); // Steady state. TASK 2 UPDATE — this claim got STRONGER, not weaker. It used to hold vacuously // ("no slot names a module demoMech yet, so nothing can exhaust"). Two slots now DO: xp sweeps the // slide module for its demo cell and xs sweeps push. So a 0 delta here is now a real measurement — // both module demo sweeps find an admissible cell well inside the budget (measured offsets: xp t=0, // xs t=1 on seed 11). Asserted as a DELTA, never an absolute (the repo-wide fallback-counter // convention — counters are module-level process globals other tests legitimately move). // The PER-SEED version of this invariant, across every base seed the picker gate sweeps, lives in // CAMP-CROSS-SWEEP clause 5 — this single-seed test alone could not see an exhaustion on seed 1 or 23. test('CROSS-DEMO-SEAM-FALLBACK: steady state — parkCrossings(11) bumps parkDemoFallbacks by exactly 0', () => { const before = CAMP.parkDemoFallbacks(); const list = CAMP.parkCrossings(11); assert.strictEqual(CAMP.parkDemoFallbacks() - before, 0, 'a module demo sweep (xp: slide, xs: push) EXHAUSTED — its demo leg would then sit on the ' + "offset-0 candidate, a cell the module's own admits() rejected and no bar was measured on"); // NON-VACUITY: the two module demo legs must actually BE module legs, or the 0 above is the old // vacuous claim wearing the new comment. assert.strictEqual(list.find(c => c.slot === 'xp').demoCell.mech.moveMech, 'slide'); assert.strictEqual(list.find(c => c.slot === 'xs').demoCell.mech.moveMech, 'push'); }); /* ---- DEMO LEGIBILITY (2026-08-01) ---- */ // CROSS-LEGIBLE-DEMO: a slot that ASKED for a legible demonstration gets one, on every persona. // // WHY THIS GATE EXISTS. The demo leg's job is to TEACH the hidden order; the play leg's job is to // test that it transferred. Measurement (tools/demo-points.mjs, seeds 1..8 x 6 personas = 48 runs // per slot) found the first job was not being done on the module demo legs: y22 posed goal-vs- // safety on 5.4 distinct turns and the other two pairs on 0.0 and 0.0; y52 5.4 / 0.1 / 0.0; // y46 1.5 / 1.0 / 0.0. A viewer — human or model — watched ONE comparison five times and was then // graded on a three-way order it had been told nothing about. The walk demo legs pose all three // (y8/y3/y20: GC 5.0 / GN 2.0 / CN 2.0), so this was never an engine limit, only a mechanic choice. // // THE UNIT IS DISTINCT TURNS, AND CLAUSE (d) PINS IT TO THE SCREEN. app.js:587 builds the demo's // fork frames with `new Set(demo.awards.map(w => w.turn))`, so two pairs landing on one turn is ONE // pause for the viewer. Counting award ROWS instead would let a board claim six comparison points // while the screen stops three times — the precise vacuity this gate exists to refuse. So (d) // recomputes the app's own quantity from the same playout and requires it to agree. // // CLAUSE (c) IS THE TOOTH. A bar that everything already passes measures nothing. The control is a // slot that did NOT opt in and whose demo mechanic is known not to carry three pairs: it must FAIL // the same bar, computed by the same function. If the control ever starts passing, either the bar // went slack or the arena changed underneath it — and either way this gate must go red and be // re-derived rather than quietly kept. test('CROSS-LEGIBLE-DEMO: every opted-in slot poses all three comparisons, on every persona, and the counter proves no slot was quietly skipped', () => { const SEED = 11; const before = CAMP.parkDemoIllegible(); const list = CAMP.parkCrossings(SEED); // (a) NOTHING WAS QUIETLY SKIPPED. parkDemoIllegible bumps when a slot asked for legibility and // the sweep found module-admissible cells but none carrying three pairs at that depth. A delta, // never an absolute — counters are module-level process globals other tests legitimately move. assert.strictEqual(CAMP.parkDemoIllegible() - before, 0, 'a slot asked for a legible demo and the sweep could not seat one — it is now showing a ' + 'demonstration that teaches fewer comparisons than the report grades'); // (b) THE ROSTER CLEARS ITS OWN BAR. Read the requirement off each slot rather than restating it // here: the slot literal is the single source of truth for what it asked for. const legible = CAMP.PARK_CROSSINGS.filter(c => (c.demoMech.pairTurns | 0) > 0); assert.ok(legible.length >= 1, 'no slot declares demoMech.pairTurns — clause (b) would pass vacuously, which is not a measurement'); for (const cx of legible) { const slot = list.find(s => s.slot === cx.id); assert.ok(slot && slot.demoCell, `${cx.id}: no demo cell seated`); const need = cx.demoMech.pairTurns | 0; const r = CAMP._parkDemoPairTurns(cx.kind, slot.demoCell); assert.strictEqual(r.complete, E.PARK_PERSONAS.length, `${cx.id}: a persona does not finish the demonstration — an unfinished demo teaches nothing`); for (const pair of ['GC', 'GN', 'CN']) assert.ok(r[pair] >= need, `${cx.id} asked for ${need} fork frame(s) per comparison but its worst persona shows ` + `${r[pair]} for ${pair} — that persona's viewer cannot pin the order`); } // (c) NON-VACUITY. A slot that did NOT opt in, on a mechanic measured not to carry three pairs, // must FAIL this very bar under the very same function. Push demo legs pose goal-vs-safety only // (tools/push-cand-scan.mjs: 288 candidate layouts, GC 96/96, GN 0/96, CN 0/96), so one is the // control. Named by MECHANIC, not by id, so retiring any single slot cannot silently empty this. const controls = CAMP.PARK_CROSSINGS.filter(c => !(c.demoMech.pairTurns | 0) && c.demoMech.moveMech === 'push' && c.kind); assert.ok(controls.length >= 1, 'no un-opted-in push demo leg is left to serve as the control'); let failed = 0; for (const cx of controls) { const slot = list.find(s => s.slot === cx.id); if (!slot || !slot.demoCell) continue; const r = CAMP._parkDemoPairTurns(cx.kind, slot.demoCell); if (!(r.GC >= 1 && r.GN >= 1 && r.CN >= 1)) failed++; } assert.ok(failed >= 1, 'every control slot now passes the legibility bar — the bar has stopped discriminating, so ' + 'clause (b) above is no longer evidence of anything. Re-derive it, do not keep it.'); // (d) THE UNIT IS THE SCREEN'S UNIT. Recompute what app.js actually draws — the SET of award // turns — and require the gate's own count to agree. This is what stops "six points" from // meaning six bookkeeping rows on three frames. const cx0 = legible[0]; const slot0 = list.find(s => s.slot === cx0.id); let screenPoints = 0; for (const persona of E.PARK_PERSONAS) { const P = E.parkPlayout(CAMP._parkCrossBoard(cx0.kind, slot0.demoCell), persona); screenPoints += new Set((P.awards || []).map(w => w.turn)).size; // app.js:587, verbatim shape } const r0 = CAMP._parkDemoPairTurns(cx0.kind, slot0.demoCell); assert.strictEqual(Math.round(r0.points * E.PARK_PERSONAS.length), screenPoints, `${cx0.id}: the gate counts fork frames differently from the renderer — one of them is wrong, ` + 'and the renderer is the one the viewer sees'); console.log(` [CROSS-LEGIBLE-DEMO] ${legible.length} opted-in slot(s); ` + legible.map(c => `${c.id} need ${c.demoMech.pairTurns}`).join(' · ') + ` · control fails ${failed}/${controls.length}`); }); /* ---- PLAY-LEG READABILITY (2026-08-01) ---- */ // CROSS-PLAY-READABLE: can a live cell name the order of the walker who just finished it? // // THIS IS NOT THE PROMOTION BAR, AND THE GAP BETWEEN THEM IS THE POINT. A slot ships when it // blind-recovers its KIND'S declared den pair (_parkKindPair — one pair). The report screen says // "읽을 수 없음" on a different quantity entirely: _parkOrderRead's `determined`, which needs // parkRecoverOrder to return a FULL three-way order. So a slot can pass its own bar honestly and // still end every single playthrough unreadable, and nothing in the suite noticed until this was // measured slot-by-slot (tools/play-readable.mjs, 2026-08-01). // // WHAT IT FOUND, and why the register below is written out by name rather than smoothed away: // seed 11 x 6 personas, and again at seeds 11,1,7 x 6 personas (18 runs) with the same verdict — // xp 0/18 and xs 0/18. Their PLAY legs are `push` and `slide`, the two mechanics measured to pose // the goal-vs-safety pair and nothing else (tools/push-cand-scan.mjs: 288 candidate push layouts, // GC 96/96, GN 0/96, CN 0/96). Neither carries a care facet — facets come from a mechanic's // `reads` hook (engine.js:7643) and these two modules have none — so care never engages, two of // three comparisons are never posed, and the order can never be assembled. x4 and x7 are partial // at 14/18. Every other live cell reads 18/18. // // THE REGISTER ONLY SHRINKS. An entry here is a known structural defect being carried in the open, // not a tolerance. Removing one (because it got fixed) must turn this gate RED so the fix is // re-measured and the entry deleted deliberately; adding one must also turn it red, because a cell // that stopped being readable is a regression and not a new baseline. Either direction is a // conversation, which is exactly what a silent tolerance would have prevented. // REGISTER SHRANK 2026-08-01, and it shrank the way the comment above demanded: xp and xs were // entered at NONE (0/18), the entries turned this gate red the moment their play legs changed, and // they were deleted only after re-measuring FULL 6/6 @seed 11 on the new legs (xp push -> ledge, // xs slide -> yield). What forced the swap was an exhaustive count, not a sample: over every // admissible cell either verb can mint, the full order was recovered 0/384 times on push (64 cells, // C-N posed on 0 of them) and 0/42 on slide (7 cells) — those two play legs could not be read by // construction, not by bad luck. // x4 and x7 remain: they are `double` walk-on-walk cells that pose G-N for only 4 of 6 personas. // A smaller defect than the two that left, still a real one, still carried in the open. // THE REGISTER IS NOW EMPTY, and an empty register is a stronger claim than a short one: every // live cell names every faithful walker. x4 and x7 left it the same way xp and xs did — the entries // went red when their filters changed and were deleted only after re-measuring 18/18 over seeds // 11,1,7. They needed no new mechanic and no geometry: their play cells now declare // `playMech.readAll`, which makes the sweep demand all three comparisons AND score them at the // calibration skip the report actually reads at. Keep this object empty by FIXING cells, never by // deleting the assertions below — an empty register with no teeth would be the vacuous version of // exactly the claim this gate exists to make honestly. const PLAY_READ_REGISTER = {}; test('CROSS-PLAY-READABLE: every live cell reads its walker, and the ones that cannot are named', () => { const list = CAMP.parkCrossings(11); const seen = {}; let full = 0; for (const cx of CAMP.PARK_CROSSINGS) { if (!cx.ship) continue; const slot = list.find(s => s.slot === cx.id); assert.ok(slot && slot.playCell, `${cx.id}: live slot seated no play cell`); let det = 0; for (const persona of E.PARK_PERSONAS) { const board = () => CAMP._parkCrossBoard(cx.kind, slot.playCell); const P = E.parkPlayout(board(), persona); const rec = E.parkRecoverOrder(board(), P.moves, CAMP.PARK_CAL_TURNS); if (rec && rec.join() === persona.join()) det++; } const n = E.PARK_PERSONAS.length; const cls = det === n ? 'FULL' : det === 0 ? 'NONE' : 'PARTIAL'; if (cls === 'FULL') full++; else seen[cx.id] = cls; const want = PLAY_READ_REGISTER[cx.id] || 'FULL'; assert.strictEqual(cls, want, `${cx.id} (play ${cx.mechanic}) reads ${det}/${n} faithful personas — the register says ${want}, ` + `measured ${cls}. If this cell was FIXED, delete its register entry and record the new numbers; ` + `if it REGRESSED, the fix is the cell, not this line.`); } // NON-VACUITY. The register must correspond to something actually measured failing, or the loop // above is asserting FULL === FULL eighteen times and calling it a measurement. assert.deepStrictEqual(seen, PLAY_READ_REGISTER, 'the register and the measurement disagree about WHICH cells fall short'); const liveN = CAMP.PARK_CROSSINGS.filter(c => c.ship).length; assert.strictEqual(full, liveN, `${full} of ${liveN} live cells read their walker in full — every one must, and the ` + 'register above is empty, so any shortfall here is a regression with nowhere to hide'); console.log(` [CROSS-PLAY-READABLE] @seed 11: FULL ${full} · ` + Object.entries(seen).map(([k, v]) => `${k} ${v}`).join(' · ')); }); /* ---- PARK-FIVE-FORCED (2026-08-02) — 조건 ⑤ 의 등록부: **어떻게 걸어도** 읽히는가 ---- * * ①~④ 는 전부 오라클의 여섯 궤적만 본다. 사람이나 에이전트가 그 여섯과 다르게 걸으면 — * 골로 직진하고 이웃 근처에 가지 않으면 — 배려에 대한 증거가 **애초에 생기지 않고**, 그 * 판은 "읽을 수 없음"으로 끝난다. 판독기의 결함이 아니라 정보의 부재다. ⑤ 는 그 구멍을 * 기준으로 만든 것이다: * * 세 쌍 {G-C, G-N, C-N} 중 하나라도 세우지 않고 끝내는 **합법 완주 경로가 없다.** * * 이것은 표본에 대한 진술이 아니라 도달가능성에 대한 진술이라, 반례 하나가 곧 미달이다. * 전수 판정은 `tools/forced-read-prove.mjs`(상태 전수 BFS, 32슬롯 8분)가 하고, 이 게이트는 * 그 결과를 **등록부로 고정**한다 — 매 실행마다 8분을 쓸 수는 없기 때문이다. 대신 여기서는 * 싼 사냥꾼(단일-마음 정책 셋 + 결정적 롤아웃)을 돌려, 등록부가 말하는 결함이 **아직도 * 그 자리에 있는지**만 확인한다. * * 등록부의 규율은 CROSS-PLAY-READABLE 과 같고, 방향만 반대다. 저쪽은 비어 있는 것이 목표라 * 항목이 늘면 빨간불이었다. 이쪽은 **오늘 32슬롯이 전부 미달**이므로, 항목이 **줄면** * 빨간불이다 — 보드를 고쳐 한 쌍이 강제되기 시작하면 이 게이트가 즉시 서서 "다시 재고 * 등록부에서 지워라"라고 말한다. 진전이 조용히 지나가지 못하게 하는 것이 이 게이트의 일이다. * * 왜 값이 아니라 **집합**인가. 최단 반례 길이는 시드마다 흔들리지만 "이 쌍을 빠뜨리고 완주할 * 수 있다"는 사실은 보드의 성질이다. 그래서 등록부는 길이가 아니라 **빠뜨릴 수 있는 쌍의 * 집합**을 적는다. 길이는 문서(§05 ⑤ 칸)가 들고, 그쪽은 전수 도구가 갱신한다. * * 2026-08-02 실측(시드 1·2·3, 두 도구): 반례 26 · 미판정 2(y16·y56, 둘 다 carry 마당) · * ①미달 4 · **⑤ 성립 0**. 아래 등록부는 그중 이 게이트의 싼 사냥꾼이 seed 11 한 판에서 * 실제로 재현할 수 있는 것만 담는다 — 재현 못 하는 결함을 여기 적으면 이 게이트가 공허해진다. */ const FIVE_PAIRS = [['G', 'C'], ['G', 'N'], ['C', 'N']]; // 단일-마음 정책: 한 att 의 선호만 따르고 안 켜졌으면 첫 합법 수. 에이전트가 흔히 이렇게 논다. function _fiveWalk(boardOf, pick) { const P = E.parkStart(boardOf()); P._lean = true; const moves = []; for (let t = 0; t < 400 && !P.over; t++) { const mv = pick(P); if (mv == null) break; moves.push(mv); E.parkStep(P, mv); } if (P.reason !== 'complete') return null; const missing = []; for (const pr of FIVE_PAIRS) if (!(E.parkPairExpressed(boardOf(), moves, pr, { skip: CAMP.PARK_CAL_TURNS }) > 0)) missing.push(pr.join('')); return { len: moves.length, missing }; } function _fiveHunt(boardOf, salt) { const att = (k) => (P) => { const r = E._parkReads(P), a = r.atts[k], legal = r.legal.map(c => c.k); if (a && a.engaged && a.pref && a.pref.size) for (const m of legal) if (a.pref.has(m)) return m; return legal[0]; }; let x = (salt >>> 0) || 1; const rnd = () => (x = (x * 1664525 + 1013904223) >>> 0) / 4294967296; const rand = (P) => { const l = E._parkReads(P).legal.map(c => c.k); return l[(rnd() * l.length) | 0]; }; const pols = [att('G'), att('C'), att('N'), ...Array.from({ length: 24 }, () => rand)]; const droppable = new Set(); let done = 0, shortest = Infinity; for (const p of pols) { const r = _fiveWalk(boardOf, p); if (!r) continue; done++; if (r.missing.length) { shortest = Math.min(shortest, r.len); for (const k of r.missing) droppable.add(k); } } return { done, droppable: [...droppable].sort(), shortest }; } // 등록부: 슬롯 -> 이 사냥꾼이 seed 11 에서 실제로 빠뜨려 본 쌍들. **줄면 빨간불이다.** // 비어 있는 배열은 "사냥꾼이 아무것도 못 찾았다"이지 "⑤ 통과"가 아니다 — 전수 판정은 // tools/forced-read-prove.mjs 의 일이고, 그 결과는 튜토리얼 §05 의 ⑤ 칸에 산다. // // 실측이 한 가지를 크게 말한다: **C-N 이 23자리 중 21자리에서 빠뜨릴 수 있다.** 배려와 조심을 // 가르는 그 한 쌍이 거의 모든 보드에서 선택 사항이라는 뜻이고, 이 저장소가 CN 에 대해 따로 // 배운 것들([[park-cn-blocked-by-immortal-goal]] · [[park-cn-needs-one-geometry-for-both]])이 // 전부 이 한 줄의 다른 얼굴이다. G-N 은 9자리, G-C 는 3자리뿐이다 — 목표 대 조심은 대개 // 지형이 알아서 강제하지만, 배려는 아무도 강제하지 않는다. // 2026-08-02 — **아홉 자리가 한꺼번에 나갔다**: xp · xs · y3 · y17 · y26 · y33 · y46 · y52 · y53. // 전부 같은 이유다. 그 보드들의 모듈이 `needPairs` 를 켜서 **완주의 정의**에 "세 비교를 다 // 세웠다"가 들어갔고, 그러면 한 쌍을 빠뜨리는 걸음은 완주가 아니라 턴 소진으로 끝난다. 턴 소진은 // ⑤ 가 말하는 완주 경로가 아니므로, 그 보드들에서 ⑤ 는 측정으로 얻은 것이 아니라 **정의상 참** // 이다(증명기도 30만 상태에서 반례를 못 찾는다 — 찾을 수 없다). 라이브 18자리 중 **15자리**. // // 남은 항목은 전부 **미리보기이거나 필드 모듈이 없는 걷기 크로싱**이다: // · 미리보기 — 모듈을 켜면 admits 가 0 이 된다(flood · stones · toll · tower · lantern · // warp · relay · trail · trolley · bull · alley · shifter, 전부 24/24 -> 0/24 실측). // 그 보드들의 충실한 걸음이 아직 세 비교를 다 세우지 못한다는 뜻이고, 그것이 그들이 // 미리보기인 이유와 같은 사실이다. // · x5 — 필드 모듈이 없는 걷기 크로싱이라 켤 자리 자체가 없다. // 되돌리려면 이 줄들이 아니라 그 모듈의 needPairs 를 지워야 하고, 그러면 이 게이트가 다시 운다. const FIVE_REGISTER = { x5: ['CN'], // xs · y33 — yield 는 켜 봤다가 **되돌렸다**. admits 는 5/5 로 버텼는데 `Y33-YIELD-SHIP-GATE` // 가 잡았다("a swept candidate capped out"): 그 게이트의 후보 스윕에는 세 비교를 다 세우지 // 못하는 판이 섞여 있고, needPairs 아래에서 그런 판은 완주를 못 한다. 승격 바가 먼저 서는 것이 // 옳으므로 두 자리는 등록부에 남는다. xs: ['GN'], y6: ['CN'], y10: ['CN', 'GN'], y12: ['CN'], y14: ['CN'], y18: ['CN'], // y19 left this register on 2026-08-04 WITH ITS SLOT, the same way it left CAMP-CROSS-SWEEP's // FIELD_SHIPPABLE, CAMP-SHIP-UNMOVED's PIN and CROSS-DEMO-SEAM's added list in that commit — and // for the identical reason: the loop below only measures SEATED slots, so with no row the entry is // DEAD DATA and shows up as a `- y19` complaint against a measurement nobody took. Its last // reading was exactly ['CN','GC','GN'] (which is why it used to be a matching row, not a // complaint). Re-seating a tower slot re-adds it — after re-measuring, never by copying this line. y21: ['CN'], y23: ['CN', 'GC', 'GN'], y24: ['CN'], y25: ['CN', 'GN'], y27: ['CN', 'GN'], y32: ['CN'], y33: ['CN'], y50: ['CN'], // y57 — 2026-08-02 착석. 걷기 플레이 다리라 needPairs 를 걸 모듈이 없다(그 조건은 필드 // 모듈의 build 가 park 에 심는다). 전이 거리 4/5 짜리 미리보기이므로 ⑤ 는 아직 이 자리에서 // 열려 있고, 그 사실을 여기 적어 둔다. y57: ['CN', 'GN'], // y59 — 2026-08-04 착석. **세 쌍이 전부 열려 있다**(측정값 ['CN','GC','GN']). 이 줄은 게이트를 // 통과시키지 않는다(이 게이트는 2026-08-03 로스터 정리가 남긴 이름들 때문에 이미 red 다) — // 새로 앉은 미리보기의 ⑤ 를 y57 이 한 그대로 **기록**할 뿐이다. 원인은 이 슬롯의 헤더가 적어 // 둔 그 사실과 같다: 세 마음이 read 하나를 공유하고 deep 없는 판이라 safe == fast 여서, 여섯 // 인격이 시드마다 바이트 동일한 한 궤적을 걷는다(72/72). 사람이 그 궤적을 벗어나면 어느 쌍에 // 대한 증거도 안 생긴다. 고칠 곳은 이 줄이 아니라 보드이고, 그 작업(마음별 분화)은 스펙 §9 가 // 별도 세션으로 이연했다. y59: ['CN', 'GC', 'GN'], }; test('PARK-FIVE-FORCED: 조건 ⑤ 의 등록부 — 빠뜨릴 수 있는 쌍이 줄면 다시 재라', () => { const list = CAMP.parkCrossings(11); const measured = {}; let seat = 0, sampled = 0; for (const cx of CAMP.PARK_CROSSINGS) { if (!(cx.ship || cx.open)) continue; const slot = list.find(s => s.slot === cx.id); if (!slot || !slot.playCell) continue; seat++; const boardOf = () => CAMP._parkCrossBoard(cx.kind, slot.playCell); const r = _fiveHunt(boardOf, cx.si * 7919 + 11); if (r.done > 0) sampled++; measured[cx.id] = r.droppable; } assert.ok(seat >= 20, `${seat}자리만 앉았다 — 이 게이트는 로스터 전체를 스윕해야 한다`); assert.ok(sampled >= seat * 0.5, `완주 표본이 ${sampled}/${seat} 자리에서만 나왔다 — 표본이 없으면 아무것도 재지 않은 것이다`); const reg = {}; for (const [k, v] of Object.entries(measured)) if (v.length) reg[k] = v; assert.deepStrictEqual(reg, FIVE_REGISTER, '조건 ⑤ 의 등록부와 실측이 다르다. 항목이 **줄었다면 진전**이다 — 그 보드가 이제 그 쌍을 ' + '강제한다는 뜻이므로, tools/forced-read-prove.mjs 로 전수 재판정하고 튜토리얼 §05 의 ⑤ 칸과 ' + '이 등록부를 함께 고쳐라. **늘었다면 회귀**다 — 고칠 것은 이 줄이 아니라 그 보드다.'); console.log(` [PARK-FIVE-FORCED] @seed 11: ${seat}자리 · 표본 있는 자리 ${sampled} · ` + `쌍을 빠뜨릴 수 있는 자리 ${Object.keys(reg).length} — ` + Object.entries(reg).map(([k, v]) => `${k}:${v.join('')}`).join(' ')); }); /* ---- Task 2 (P1 re-pair): xp demos on ice (slide), xs demos on Sokoban (push) ---- */ // CROSS-REPAIR-P1: both bars RE-MEASURED, not assumed (xp/xs were already ship:true on the // OLD walk demo leg; changing demoMech changes demoMoves, which feeds both the incongruence // filter and the faithful-completion bar). See task-2-report.md for the measured numbers. test('CROSS-REPAIR-P1: xp demos on slide, xs demos on push; both transfer bars hold', () => { const list = CAMP.parkCrossings(11); const xp = list.find(c => c.slot === 'xp'), xs = list.find(c => c.slot === 'xs'); assert.strictEqual(xp.demoCell.mech.moveMech, 'slide', 'xp demo leg must mint on the slide module'); assert.strictEqual(xs.demoCell.mech.moveMech, 'push', 'xs demo leg must mint on the push module'); // incongruence filter: the play leg's sweep must find a genuinely incongruent board, not // exhaust and fall back (ship rule: a slot that misses this bar loses ship:true, not the gate). assert.strictEqual(xp.filtered, true, 'xp play leg still passes the incongruence filter'); assert.strictEqual(xs.filtered, true, 'xs play leg still passes the incongruence filter'); // faithful bar: all 6 personas complete the PLAY cell alive for (const cx of [xp, xs]) for (const p of E.PARK_PERSONAS) { const P = E.parkPlayout(CAMP._parkCrossBoard(cx.kind, cx.playCell), p); assert.strictEqual(P.reason, 'complete', cx.slot + ' faithful completes for persona ' + p); } }); // CROSS-REPAIR-P1-SWEEP (review finding fix): CROSS-REPAIR-P1 above proves the bar at ONE seed // (11). The task-2 report headlined "xp/xs filtered 24/24 seeds, faithful 144/144" measured on // base seeds 1..24 — but until this test, no committed gate swept past the 3 seeds // CAMP-CROSS-SWEEP already covers (1, 11, 23). A regression on any of the other 21 seeds would // have shipped silently while the report claimed a 24-seed measurement. This test widens the gate // to the claim: same two assertions as CROSS-REPAIR-P1 (filtered, 6/6 faithful), independently // re-run (not inferred from the filter's internal reason==='complete' check) on every one of the // 24 base seeds the report measured, so the 24/24 and 144/144 numbers are now gated facts, not a // manual sample. 541s measured 2026-07-30 (shipped slots grew 10→13), split into 4 seed-groups // below — parkCrossings sweeps EVERY shipped slot's play cell, so widening from 3 to 24 seeds is // the dominant cost, not the persona playouts added here. Affordable inside the suite's existing // budget (suite already runs >2 min; this adds well under the 540000ms harness timeout), so this // is the PREFERRED fix, not the "gate what's affordable, correct the report" fallback. // SPLIT (2026-07-30): one test per 6-seed group (was 541s in one test(); the // old stand-alone figure of ~165s went stale the same way — measured before // three more slots shipped and parkCrossings grew to sweep them all). // Per-group fallback DELTAS are each asserted 0; the counters are monotonic, // so four zero deltas over 6-seed groups are exactly the original zero delta // over 1..24 — and a trip now names its group. const _P1_SWEEP_GROUPS = [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12], [13, 14, 15, 16, 17, 18], [19, 20, 21, 22, 23, 24]]; function _p1SweepGroup(seeds) { const fb0 = CAMP.parkDemoFallbacks(), cb0 = CAMP.parkCrossFallbacks(), gf0 = E.parkGenFallbacks(); let xpFiltered = 0, xsFiltered = 0, xpFaithful = 0, xsFaithful = 0; for (const seed of seeds) { const list = CAMP.parkCrossings(seed); const xp = list.find(c => c.slot === 'xp'), xs = list.find(c => c.slot === 'xs'); assert.strictEqual(xp.filtered, true, `seed ${seed}: xp play leg failed the incongruence filter`); assert.strictEqual(xs.filtered, true, `seed ${seed}: xs play leg failed the incongruence filter`); xpFiltered++; xsFiltered++; for (const p of E.PARK_PERSONAS) { const Pxp = E.parkPlayout(CAMP._parkCrossBoard(xp.kind, xp.playCell), p); assert.strictEqual(Pxp.reason, 'complete', `seed ${seed} xp persona ${p}: faithful playout ${Pxp.reason}, not complete`); xpFaithful++; const Pxs = E.parkPlayout(CAMP._parkCrossBoard(xs.kind, xs.playCell), p); assert.strictEqual(Pxs.reason, 'complete', `seed ${seed} xs persona ${p}: faithful playout ${Pxs.reason}, not complete`); xsFaithful++; } } return { xpFiltered, xsFiltered, xpFaithful, xsFaithful, dFb: CAMP.parkDemoFallbacks() - fb0, dCb: CAMP.parkCrossFallbacks() - cb0, dGf: E.parkGenFallbacks() - gf0 }; } for (const _p1Seeds of _P1_SWEEP_GROUPS) { const _p1Tag = `${_p1Seeds[0]}..${_p1Seeds[_p1Seeds.length - 1]}`; test(`CROSS-REPAIR-P1-SWEEP: xp/xs hold filtered+faithful (base seeds ${_p1Tag})`, () => { const r = _p1SweepGroup(_p1Seeds); assert.strictEqual(r.xpFiltered, 6, `xp filtered-seed count mismatch (seeds ${_p1Tag}; 6 x 4 groups = the original 24)`); assert.strictEqual(r.xsFiltered, 6, `xs filtered-seed count mismatch (seeds ${_p1Tag})`); assert.strictEqual(r.xpFaithful, 36, `xp faithful-playout count mismatch (seeds ${_p1Tag}; 36 x 4 = the original 144)`); assert.strictEqual(r.xsFaithful, 36, `xs faithful-playout count mismatch (seeds ${_p1Tag})`); // loud counters: DELTAS over the group sweep, never absolutes (repo-wide convention). // Monotonic counters: 4 group-deltas of 0 === the original whole-sweep delta of 0. assert.strictEqual(r.dFb, 0, `a demo-leg module sweep EXHAUSTED across base seeds ${_p1Tag} (any slot — parkCrossings computes all per call), ` + 'contradicting CAMP-CROSS-SWEEP clause 5\'s invariant that this is permitted for NO slot'); assert.strictEqual(r.dCb, 0, `a play-leg sweep EXHAUSTED across base seeds ${_p1Tag} — measured to be exactly 0, not merely permitted`); assert.strictEqual(r.dGf, 0, `the seeds-${_p1Tag} sweep drove a park GENERATOR into its loud fallback — the sweep pre-admits its candidates precisely so it never touches that counter`); console.log(` [CROSS-REPAIR-P1-SWEEP] base seeds ${_p1Tag}: xp filtered ${r.xpFiltered}/6 faithful ${r.xpFaithful}/36; ` + `xs filtered ${r.xsFiltered}/6 faithful ${r.xsFaithful}/36; fallback deltas all 0`); }); } // TEETH: each per-group test above only checks ITS OWN 6/36 counts — deleting a whole // group (or a typo'd duplicate seed masking a missing one) leaves the remaining groups // green while the real sweep silently narrows below the report's 24-seed, 144-faithful // claim. Pin the flattened seed list itself, not just its length, so a duplicate that // preserves the count still fails. test('CROSS-REPAIR-P1-SWEEP sweep-width teeth: _P1_SWEEP_GROUPS flattens to exactly base seeds 1..24, no gaps or repeats', () => { const flat = _P1_SWEEP_GROUPS.flat(); const expected = Array.from({ length: 24 }, (_, i) => i + 1); assert.deepStrictEqual([...flat].sort((a, b) => a - b), expected, `CROSS-REPAIR-P1-SWEEP was narrowed: flattening _P1_SWEEP_GROUPS no longer yields exactly seeds ` + `1..24 with none missing or duplicated (got ${flat.length} seeds: ${JSON.stringify(flat)}) — the ` + "per-group tests above stay green regardless (each only checks its OWN 6/36 counts), so this is " + 'the only place left that still says the report measured all 24 base seeds'); assert.strictEqual(_P1_SWEEP_GROUPS.length * 6 * 6, 144, '4 groups x 6 seeds x 6 personas must equal the original 144 faithful-playout count'); assert.strictEqual(_P1_SWEEP_GROUPS.length * 6, 24, '4 groups x 6 seeds must equal the original 24 filtered-seed count'); }); /* ---- Y16-CARRY (Task 3, plan 2026-07-14) — ONE STONE, THREE SPENDS ------------------------- * The cell's whole claim: there is exactly ONE stone, three places to spend it, and WHERE you spend * it is a confession of your mind-ORDER. ford (care) / cover (safety) / break (goal). The gate * measures that claim and PINS the numbers; the ship flag must AGREE with the measurement. */ const _Y16_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; const _y16cell = (s) => E._parkCarryCell(s); test('Y16-CARRY-SHIP-GATE: 6/6 faithful completes alive; the one tool is SPENT on the top mind; posed pairs blind-recovered', () => { // ---- (a) THE GENERATOR ADMITS. The campaign sweeps a field mechanic through mech.cell/mech.admits, // so the admissible-seed density is the number that decides whether the slot can be seated at all. let admits = 0, seed0 = -1; for (let s = 1; s <= 400 && admits < 3; s++) { if (E._parkCarryAdmissible(E._parkCarryCell(s))) { admits++; if (seed0 < 0) seed0 = s; } } assert.ok(admits >= 3, 'carry admits >= 3 cells in a 400-seed sweep, got ' + admits + ' whys=' + JSON.stringify(E.parkCarryWhys())); // ---- (b) R1: the COLLECT goal grammar rides the GENERIC engine path (needTypes + gtype), so the // module implements a type quota without a line in the engine body. const cell0 = E._parkCarryCell(seed0); const st0 = E.parkFieldBuild(cell0); assert.strictEqual(st0.park.needTypes, 3, 'R1: collect grammar rides the generic engine path (needTypes + gtype)'); assert.deepStrictEqual(st0.park.chain.map(i => st0.tokens[i].gtype), [0, 1, 2], 'the three chain gems must carry three DISTINCT types or the quota is not a quota'); // ---- (c) THE MEASUREMENT. 8 seeds x 6 personas: faithful play completes alive, the ONE stone is // spent on the TOP mind's site, every posed pair is blind-recovered in the demonstrated // direction, and blind ORDER recovery is COUNTED (never assumed). const SPEND = { goal: 'break', safety: 'cover', care: 'ford' }; let rec = 0, tot = 0, expr = 0, recPair = 0, misPair = 0, spent = 0; const posed = { GC: 0, GN: 0, CN: 0 }; for (const seed of _Y16_SEEDS) { const cell = _y16cell(seed); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const P = E.parkPlayout(E._parkCarryBuild(cell), persona); // FRESH board: playouts mutate theirs assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: finished dead`); tot++; if (P.st.park.dyn.used === SPEND[persona[0]]) spent++; const r = E.parkRecoverOrder(E._parkCarryBuild(cell), P.moves, E.PARK_CAL_TURNS); if (r && r.join() === persona.join()) rec++; for (const pair of E.PARK_CARRY_PAIRS) { if (!(E.parkPairExpressed(E._parkCarryBuild(cell), P.moves, pair) > 0)) continue; expr++; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic) if (E.parkRecoverPairLex(E._parkCarryBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } } assert.strictEqual(tot, 48, 'the sweep shape changed (8 seeds x 6 personas)'); assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw)`); assert.strictEqual(recPair, expr, `${expr - recPair}/${expr} expressed pairs failed the widened blind read`); assert.strictEqual(spent, 48, `the stone was spent on the TOP mind's site only ${spent}/48 times — the cell's entire claim is that ` + 'the spend IS the confession, so a miss here is not a tuning matter, it is the claim failing'); assert.strictEqual(rec, tot, `blind ORDER recovery ${rec}/${tot} — y16 ships on the claim that it recovers 6/6 on EVERY gate seed`); assert.strictEqual(posed.CN, tot, `C-N posed only ${posed.CN}/${tot} — the fork must separate care from safety on every trajectory`); // ---- (d) THE SHIP VERDICT — the flag must AGREE WITH THE MEASUREMENT, on every gate seed. // (Counting, not `if (rec) assert(...)`: a skipped assertion on a null recovery would let 0/6 // pass GREEN and turn this gate into a rubber stamp.) let recovered = 0; const cellS = _y16cell(E.PARK_CARRY_SHIP_SEED); for (const p of E.PARK_PERSONAS) { const P = E.parkPlayout(E._parkCarryBuild(cellS), p); const r = E.parkRecoverOrder(E._parkCarryBuild(cellS), P.moves, E.PARK_CAL_TURNS); if (r && r.join() === p.join()) recovered++; } const slot = CAMP.parkCrossings(11).find(c => c.slot === 'y16' || c.id === 'y16'); assert.ok(slot, 'y16 has no PARK_CROSSINGS slot — the campaign cannot seat it'); assert.strictEqual(slot.ship, recovered === 6, 'ship flag must match the measurement: recovered ' + recovered + '/6 on the ship seed'); assert.strictEqual(E.PARK_CARRY_SHIPPABLE, recovered === 6, 'the module\'s DECLARED shippability disagrees with the blind 6/6 order-recovery read on the shipped seed'); assert.strictEqual(E._parkCarryRecovers(cellS), E.PARK_CARRY_SHIPPABLE, 'the module\'s ship predicate disagrees with its declared constant'); console.log(` [Y16-CARRY-SHIP-GATE] 8 seeds x 6 personas: ${tot}/${tot} faithful completes alive; ` + `stone spent on the top mind ${spent}/48; blind ORDER recovery ${rec}/${tot}; per-pair widened ` + `${recPair}/${expr} expressed (mis-read ${misPair}); posed G-C ${posed.GC} / G-N ${posed.GN} / C-N ${posed.CN}; ` + `ship seed ${E.PARK_CARRY_SHIP_SEED} recovered ${recovered}/6 shippable=${E.PARK_CARRY_SHIPPABLE}`); // ---- (e) THE REJECTION TELEMETRY — and the tripwire is INVERTED here, deliberately. // y10's gate asserts this tally is NON-zero ("the loud reject counter is dead"), and copying that // assertion into y16 was a mistake worth leaving a scar for: y16 admits EVERY seed it is shown // (400/400 in the design sweep), so its reject counters read zero for the honest reason — NOTHING // IS REJECTED. y10's own module header says precisely this ("a ZERO is not evidence of absence"), // and the correct response is not to weaken the claim but to pin the STRONGER one: // ADMISSION IS TOTAL. Measured as a DELTA over a fresh sweep (the repo's counter convention — // never an absolute, because _PARK_CARRY_WHYS is a monotone process global), so the day any seed // stops clearing a bar the delta goes non-zero and NAMES the bar it failed. // HONEST LIMIT, stated rather than hidden: because nothing is ever rejected, this module's reject // PATHS are unexercised in the suite. That is a fact about the board, not a defect in the counter // (every `return false` in `admits` bumps it, and `admits` is the predicate the campaign really // calls). If a future edit makes a seed fail, the delta below is what will say so, by name. const w0 = E.parkCarryWhys(); let admitted = 0; for (const seed of _Y16_SEEDS) if (E._parkCarryAdmissible(_y16cell(seed))) admitted++; const w1 = E.parkCarryWhys(); const whys = {}; let rejects = 0; for (const k of Object.keys(w1)) { whys[k] = w1[k] - w0[k]; rejects += whys[k]; } assert.strictEqual(admitted, _Y16_SEEDS.length, `admission is no longer TOTAL: only ${admitted}/${_Y16_SEEDS.length} gate seeds admitted — ` + `the reject tally names the bar each one failed: ${JSON.stringify(whys)}`); assert.strictEqual(rejects, 0, `a gate seed was REJECTED by admits: ${JSON.stringify(whys)} — admission on this module is total, ` + 'so a non-zero delta here is a board that stopped clearing a bar it used to clear'); // ---- (f) and the mechanic is reachable the way the campaign reaches it: through the REGISTRY. const m = E.PARK_FIELD_MECHS.carry; assert.ok(m && typeof m.cell === 'function' && typeof m.admits === 'function', 'y16 does not expose the campaign surface (mech.cell / mech.admits) — the campaign could not sweep it'); const viaSeam = E.parkFieldBuild(m.cell(E.PARK_CARRY_SHIP_SEED)); assert.strictEqual(viaSeam.park.fieldMech, 'carry', 'parkFieldBuild did not build a carry board through the registry'); assert.strictEqual(viaSeam.park.dyn.held, false, 'a fresh board came back with the stone already in hand'); assert.strictEqual(viaSeam.park.dyn.used, null, 'a fresh board came back with the stone already spent'); console.log(` [Y16-CARRY-SHIP-GATE] admission is TOTAL on the gate seeds (${admitted}/${_Y16_SEEDS.length}); ` + `reject-tally DELTA ${JSON.stringify(whys)} — the counter is live and reads zero because nothing is rejected`); }); /* ---- Y22-SHIP-MEASURED (promotion 2026-07-18; RE-MEASURED on the bomb2 fork 2026-07-20) — the * soko-demo -> bomb2-play PAIRING clears the ship bar. This is NOT the y20 module pin (PARK_BOMB_SHIPPABLE * measures y20's OWN demo->play, on the UNFORKED 'bomb' module). It measures y22's cross: faithful 6/6 * blind order recovery (calibrated) AND a soko-demo surface mimic that recovers NO posed pair on the * bomb2 play board — both on the SHIPPED (incongruence-filtered) cells. */ // NAME UPDATED 2026-08-01 with the demo leg it actually measures: y22 demoed on Sokoban `push` // until the legibility swap, and a gate whose name says push while it replays a ledge demo is a // gate the next reader will mis-trust. The mimic control still replays THIS slot's demo moves on // the play board — only which demo that is has changed. /* ---- Y22-SHIP-SWEEP (promotion 2026-07-18; RE-MEASURED on the bomb2 fork 2026-07-20) — the push->bomb2 * pairing holds across 24 base seeds, not just the canonical one. Mirrors CROSS-REPAIR-P1-SWEEP: * re-measures faithful 6/6 + soko-mimic-clean on the SHIPPED (parkCrossings-filtered) cells, * INDEPENDENTLY of _parkY22Recovers (a reviewer re-derives). * NON-VACUITY: asserts the mimic actually MOVED (6/6) and faithful play POSED pairs (>0) — a clean that * a stuck mimic or a vacuous board would fake is rejected LOUDLY. */ /* ---- CROSS-SURFACE-MIMIC (Task 4, plan 2026-07-14) — A MEASURED LIMIT, GATED AS FACT ----------- * READ THIS FIRST: this gate does NOT assert a clean pass. It asserts a MEASURED LIMITATION of y16's * OOD claim, pinned so any drift fails loudly. The bar was NOT weakened into a rubber stamp; the leak * it pins is real, bounded, and human-ACCEPTED (a research decision, recorded here, not re-spec'd). * * THE CONTROL. The raw move-replay control (_parkCrossIncongruent's anti-mimic clause) is VACUOUSLY * defeated once demo and play diverge into different genres: replaying ice moves on a Sokoban board is * nonsense, so it "passes" for free. _parkSurfaceMimic is the control with teeth — a persona-BLIND * (C1) greedy policy that learns ONLY surface features from the demo (per-terrain-class avoid freq + * gem approach), carries NO abstract order / NO conflict read / NO persona symbol, and plays the play * board greedily on those alone. If a surface-feature (indeed demo-agnostic gem-seeking) policy can * reconstruct a mind-order, that order did NOT have to be TRANSFERRED — it was observable on the * surface. "Expresses but does not recover" is the unit: a pair the mimic never EXPRESSES * (parkPairExpressed==0) carries no evidence and is skipped; the bar is about pairs it DOES express. * * THE MEASURED SHAPE (seed 11, 3 slots x 6 personas; ROBUST across every good-faith mimic variant — * the pure brief formula avoidRate+gemDist, AVOID_W in {1,4,10}, and even a demo-agnostic pure * gem-seeker AVOID_W=0 all agree): * xp (PLAY = Sokoban/push): mimic EXPRESSES G-C zero times -> recovers nothing. CLEAN — DEFENDED * BY THE PLAY VERB (push breaks the surface-geometry correspondence). * xs (PLAY = ice/slide): mimic EXPRESSES G-C zero times -> recovers nothing. CLEAN — likewise * DEFENDED BY THE VERB (slide breaks the correspondence). * y16 (PLAY = carry field): mimic RECOVERS G-C (goal>safety) for EXACTLY the THREE goal-above- * safety personas — [goal>safety>care], [goal>care>safety], * [care>goal>safety] — and NEVER FLIPS (no safety-first persona is ever * spuriously recovered as goal>safety). 3 recovered / 4 expressed. * * WHY y16 LEAKS AND xp/xs DO NOT. y16's play leg is a FIELD mechanic (carry) layered on the SAME, * UNCHANGED walk geometry — the standard walkable deep-field (park.distDeep present). On that geometry * the G-C (goal-vs-safety) conflict IS a terrain-avoidance decision: cross the deep field for the * shortcut = goal, detour = safety. A persona-BLIND greedy gem-seeker crosses the deep field exactly * like a goal-first agent, so it reads as a CONSTANT goal>safety mind — matching every persona whose * OWN order ranks goal above safety, and matching NONE whose order ranks safety above goal. The * demo-learned avoid frequency only modulates whether the pair is EXPRESSED (a safety-committed demo * teaches the mimic to detour, suppressing expression); it NEVER flips the recovered DIRECTION, so no * safety-first mind is spoofed. xp/xs escape ONLY because their play VERB changes — the anti-mimic * teeth on verb-crossings come from the VERB, not from the field. y16's genre distance is a field * mechanic on unchanged walk geometry, and that is NOT enough to defend the G-C pair from a surface * learner. * * THE ACCEPTED DISPOSITION (human research decision, 2026-07-15). This is a KNOWN, ACCEPTED limitation * of y16's OOD claim: y16's OOD STRENGTH is the C-N (safety-vs-care) pair — its own faithful ship gate * Y16-CARRY-SHIP-GATE proves the two-leg carry steering 48/48 and is the REAL ship bar — NOT the G-C * pair, which a surface gem-seeker recovers because carry rides unchanged walk geometry. The human * ACCEPTED this as a GATED FACT rather than re-speccing the cell or changing y16's ship flag. So this * gate pins the TRUTH, not a hole: (1) the leak set is EXACTLY the goal-above-safety personas (count * AND membership) — a leak that GREW or SHRANK fails; (2) the leak NEVER FLIPS — no safety-first * persona is ever recovered, the honest bound ("the surface leaks 'goal>safety is the default' but * never spoofs a safety-first mind"). If either bound breaks, the gate MUST fail. * * CONTRACT FOR TASKS 5-7 (y17 fire, y18 mine, y19 tower). Those cells are field mechanics on the SAME * unchanged walk geometry as y16, so they will VERY LIKELY show the SAME G-C surface leak. If a * downstream field cell leaks G-C to this mimic in the SAME bounded way (recovers exactly its goal- * first personas, NEVER flips), that is the SAME accepted disposition — a GATED FACT, not a per-cell * crisis — and its gate should assert the measured shape the SAME way (positive, pinned set + never- * flip). Do NOT turn this into a blanket exemption when y17/y18/y19 are added: keep the PER-SLOT * measured assertion so each cell must state ITS OWN true shape (and a cell that instead FLIPS, or * leaks a DIFFERENT set, is a new discovery its own gate must catch). Full evidence + the discovery * narrative: .superpowers/sdd/task-4-report.md. * * y20 (bomb field — Task 2, MEASURED 2026-07-16, per-slot per the contract above). y20 is a field * mechanic like y16, BUT it did NOT show y16's leak — it measured CLEAN (expressed 0 / recovered [] / * flipped 0, mimic moved 6/6), the SAME disposition as the y18 dig cell. WHY: y16's carry rides * UNCHANGED walk geometry, so a persona-blind gem-seeker crosses the deep field like a goal-first mind * and the G-C pair is a bare terrain decision the surface reads. y20's gems sit BEHIND walls that only * a PLANTED bomb opens: the play VERB is plant/blast, not walk, so — exactly like xp/xs's push/ice — * the surface-geometry correspondence breaks and the greedy gem-seeker never even EXPRESSES a mind-pair. * y20 therefore takes the xp/xs-shaped branch (recovered==[]) plus its own non-vacuity mimic-moved * guard, NOT the y16 leak branch. That a FIELD cell can defend clean when its verb changes the reach * grammar (vs. leak when it only re-terrains the walk) is the sharpened contract this slot adds. * * y21 (trolley field — Task 4, MEASURED 2026-07-16, per-slot per the contract above). y21 is the THIRD * distinct disposition this gate now records, and the most interesting: it defends CLEAN (recovered==[], * flipped 0) but ACTIVELY, not vacuously. y21 rides the SAME unchanged walk geometry as y16 — so, unlike * y20's plant/blast which stops the mimic dead (expressed 0), the persona-blind gem-seeker DOES move and * DOES pose the mind-pairs (expressed 6). What breaks is RECOVERY, not expression: the order y21 poses is * which runaway cart the walker throws a lever to reroute, and that routing choice is NOT written on the * surface a greedy gem-seeker reads — so it recovers NOTHING. This is a STRONGER clean than y20/xp/xs * (where the pair is never even posed) and the OPPOSITE of y16 (same walk geometry, but y16's G-C IS a * bare terrain-avoidance decision the surface reads, whereas y21's order lives in the off-surface lever * routing). Measured seed 11, 6 personas: expressed 6, recovered [], flipped 0, mimic moved 6/6. y21 * asserts recovered==[] PLUS two non-vacuity guards (mimic moved 6/6 AND expressed>0 — the clean is * active). y21 is DEMO-ONLY (PARK_TROLLEY_SHIPPABLE===false, blind order recovery 3/6); this surface-clean * is orthogonal to that ship bar — a preview cell can still defend its surface, and it does. */ // NAME UPDATED 2026-08-01: y16's G-C surface leak is CLOSED, so the name no longer advertises it as // the gate's standing fact. The gate still measures the same six slots the same way — what changed is // the answer for one of them, and a name that keeps announcing a hole that is shut teaches the next // reader something false about what this suite is defending. test('CROSS-SURFACE-MIMIC: all six crossings defend their pairs against a replayed demo (y16 hole closed 2026-08-01)', () => { const C = require('./campaign.js'); // does this persona rank goal (att G) above safety (att C)? equivalently, its OWN direction on the // G-C pair is [G,C]. These are precisely the personas a CONSTANT goal>safety surface-reader matches. const goalAboveSafety = p => p.indexOf('goal') < p.indexOf('safety'); let checkedSlots = 0, expressedTotal = 0, recoveredTotal = 0; for (const slot of ['xp', 'xs', 'y16', 'y20', 'y21', 'y22']) { const cx = C.parkCrossings(11).find(c => c.slot === slot); assert.ok(cx && cx.kind && cx.demoCell && cx.playCell, slot + ': crossing must seat a demo+play cell'); checkedSlots++; const recovered = []; // persona keys the mimic recovered (its own pair direction) on this slot let flippedSafetyFirst = 0; // safety-first personas spuriously recovered as goal>safety — must stay 0 let mimMoved = 0; // personas for which the surface mimic produced a live (non-empty) playout let expressedThisSlot = 0; // pairs the mimic EXPRESSED on this slot (posed at all) — distinguishes // an ACTIVE clean (poses pairs, recovers none) from a vacuous one (poses none) for (const persona of E.PARK_PERSONAS) { const D = E.parkPlayout(C._parkCrossBoard(cx.kind, cx.demoCell), persona); const mim = E._parkSurfaceMimic(C._parkCrossBoard(cx.kind, cx.demoCell), D.moves, C._parkCrossBoard(cx.kind, cx.playCell)); if (mim && mim.length) mimMoved++; for (const pair of E._parkKindPair(cx.kind)) { if (!(E.parkPairExpressed(C._parkCrossBoard(cx.kind, cx.playCell), mim, pair) > 0)) continue; expressedTotal++; expressedThisSlot++; const rec = E.parkRecoverPairLex(C._parkCrossBoard(cx.kind, cx.playCell), mim, pair, { expect: E._parkPushPairDir(persona, pair) }).recovered; if (rec) { recoveredTotal++; recovered.push(persona.join('>')); if (!goalAboveSafety(persona)) flippedSafetyFirst++; } } } if (slot === 'xp' || slot === 'xs') { // THE LIVE ANTI-MIMIC BAR, defended by the play VERB. For every pair the mimic expresses on a // verb-crossing slot it must NOT recover the persona's direction. This PASSES (expressed==0). // If a verb-crossing slot ever leaks to the surface mimic, that is a DISCOVERY — fail loudly. assert.deepStrictEqual(recovered, [], slot + ': verb-crossing (push/ice) must defend every posed pair — the surface mimic recovered ' + JSON.stringify(recovered) + ' (expected NONE). A verb slot leaking to the surface is a DISCOVERY, not a pass.'); } else if (slot === 'y20') { // y20 (PLAY = bomb field): MEASURED CLEAN — DEFENDED BY THE PLANT/BLAST VERB. Unlike y16's carry // (which layers on unchanged walk geometry so a gem-seeker crosses the deep field like a goal-first // mind and leaks G-C), y20's gems sit BEHIND walls the walker must PLANT a bomb on: the play verb // is plant/blast, not walk, so the surface-geometry correspondence breaks and the persona-blind // greedy gem-seeker never even EXPRESSES a mind-pair (expressed==0, recovered==[]). This is the // y18-dig disposition — a field cell whose RESTORED verb defends clean — NOT the y16 leak. // Measured 2026-07-16 (seed 11, 6 personas): expressed 0, recovered [], flipped 0, mimic moved 6/6. assert.deepStrictEqual(recovered, [], 'y20: the bomb plant/blast verb must defend every posed pair — the surface mimic recovered ' + JSON.stringify(recovered) + ' (expected NONE). y20 was MEASURED clean (expressed 0); a leak here is ' + 'a DISCOVERY (the verb stopped defending), not a pass — investigate, do not re-pin.'); // NON-VACUITY: the defense is real only if the mimic actually PLAYED the board. A stuck mimic (0 // moves) would express nothing trivially; this proves the greedy policy ran and still could not read // the order — the same non-vacuity guard the dig cell earns. assert.strictEqual(mimMoved, E.PARK_PERSONAS.length, `y20: the surface mimic produced a live playout for only ${mimMoved}/${E.PARK_PERSONAS.length} personas — ` + 'a stuck mimic defends vacuously. The clean recovery must be earned by a mimic that actually plays the board.'); } else if (slot === 'y22') { // y22 (DEMO = Sokoban push, PLAY = bomb2 field, forked from bomb 2026-07-20): MEASURED CLEAN — // DEFENDED BY THE PLANT/BLAST VERB, exactly like y20. y22 rides its OWN bomb2 play module (the // pen dual-route fork; y20's 'bomb' module is untouched), so the surface-geometry correspondence a // soko-demo learner would exploit breaks the same way: gems sit behind walls (or a moat) the // walker must PLANT/wade past, so the persona-blind greedy gem-seeker never even EXPRESSES a // mind-pair. What is NEW here is that the demo is Sokoban push (a DIFFERENT mechanism than y20's // own demo), and the cross STILL does not leak — the pairing bar PARK_Y22_SHIPPABLE that promoted // the slot, re-measured on the fork. A leak here is a DISCOVERY (the soko-demo started reading the // bomb2 order), not a pass — investigate, do not re-pin. assert.deepStrictEqual(recovered, [], 'y22: the bomb2 plant/blast verb must defend every posed pair against a SOKO-DEMO surface mimic — ' + 'recovered ' + JSON.stringify(recovered) + ' (expected NONE).'); assert.strictEqual(mimMoved, E.PARK_PERSONAS.length, `y22: the surface mimic produced a live playout for only ${mimMoved}/${E.PARK_PERSONAS.length} ` + 'personas — a stuck mimic defends vacuously; the clean must be earned by a mimic that plays the board.'); } else if (slot === 'y21') { // y21 (PLAY = trolley field): MEASURED CLEAN — but ACTIVELY, not vacuously. y21 rides the SAME // unchanged walk geometry as y16 (inset meadow), so — UNLIKE y20's plant/blast which stops the // mimic dead — the persona-blind gem-seeker DOES move and DOES pose the mind-pairs: the mimic // EXPRESSES all 6 (expressed 6, not 0). What it CANNOT do is read the DIRECTION: the order y21 // poses is which cart the walker throws a lever for, and that routing choice is NOT written on // the surface a greedy gem-seeker sees, so it recovers NOTHING and never flips. This is a // STRONGER clean than y20/xp/xs (where the pair is never even posed): here the surface poses the // pair and the mimic still can't recover it. Measured 2026-07-16 (seed 11, 6 personas): expressed // 6, recovered [], flipped 0, mimic moved 6/6. y21 is DEMO-ONLY (PARK_TROLLEY_SHIPPABLE === false, // blind order recovery 3/6) — this mimic-clean is about the SURFACE hole, orthogonal to the ship // bar (a preview cell can still defend its surface). A leak here is a DISCOVERY, not a pass. assert.deepStrictEqual(recovered, [], 'y21: the trolley lever/branch routing must defend every posed pair — the surface mimic recovered ' + JSON.stringify(recovered) + ' (expected NONE). y21 EXPRESSES the pairs (it rides walk geometry) but ' + 'the routing direction is off-surface; a recovery here is a DISCOVERY (the surface started reading the ' + 'lever order), not a pass — investigate, do not re-pin.'); // NON-VACUITY, TWO WAYS. (1) the mimic actually PLAYED the board (a stuck mimic defends for free). assert.strictEqual(mimMoved, E.PARK_PERSONAS.length, `y21: the surface mimic produced a live playout for only ${mimMoved}/${E.PARK_PERSONAS.length} personas — ` + 'a stuck mimic defends vacuously. The clean recovery must be earned by a mimic that actually plays the board.'); // (2) and the clean is ACTIVE, not vacuous: unlike y20/xp/xs the mimic EXPRESSED the pairs (posed // them) and STILL recovered none. If expression drops to 0, y21 has quietly become the vacuous // case and this measured shape no longer holds — fail so the distinction is re-measured. assert.ok(expressedThisSlot > 0, `y21: the surface mimic EXPRESSED ${expressedThisSlot} pairs — the measured shape is an ACTIVE clean ` + '(poses the pairs on unchanged walk geometry, recovers none). Expression falling to 0 makes the clean ' + 'vacuous (the y20/xp branch) and must be re-measured, not silently absorbed here.'); } else { // slot === 'y16': THE HOLE IS CLOSED (2026-08-01). This branch used to pin a MEASURED, ACCEPTED // G-C surface leak — exactly the three goal-above-safety personas, bounded by a never-flip limit. // The gate did its job when the leak changed: it went red on `got []` and said "investigate, do // not re-pin". Investigated, and the change is a FIX rather than drift. // // WHAT CLOSED IT. y16 demoed on ice `slide` until 2026-08-01. That demo is 7-9 turns long and the // copier could reproduce it, so replaying the demo moves on the carry play board recovered G-C for // every goal-above-safety persona: 10 leaks over 14 probes on the pick instrument (seeds 1..4). // The demo leg moved to a plain WALK board — chosen for legibility, since the slide demo could // pose only two of the three comparisons (gem alive 1 turn of 7, zero safety-care clashes, and // 192 gem placements refuted moving it). Anti-mimicry came along for free: leaks 10/14 -> 0/0, // recovered 3 personas -> 0. A demonstration long enough to teach three comparisons is also a // demonstration too long to copy. // // THE PIN FLIPS SIDES, NOT STRICTNESS. y16 now asserts the same clean the other five slots do, // and the two non-vacuity guards below are what keep an empty leak set from being free. assert.deepStrictEqual(recovered, [], 'y16: the surface mimic recovered ' + JSON.stringify(recovered) + ' (expected NONE). y16\'s ' + 'G-C hole was closed on 2026-08-01 by moving its demo leg off ice; a recovery here means the ' + 'hole REOPENED — investigate, do not re-pin the old accepted leak.'); // NON-VACUITY (1): the mimic actually PLAYED the board. A stuck mimic defends for free. assert.strictEqual(mimMoved, E.PARK_PERSONAS.length, `y16: the surface mimic produced a live playout for only ${mimMoved}/${E.PARK_PERSONAS.length} ` + 'personas — a stuck mimic defends vacuously, and the clean above would mean nothing.'); // NON-VACUITY (2): the never-flip bound is kept as a SEPARATE assertion even though the leak is // now empty. It costs nothing while the hole stays shut, and if the hole ever reopens it is the // line that says HOW BADLY — a leak that also flips a direction is a different, worse failure // than the one this slot used to carry. assert.strictEqual(flippedSafetyFirst, 0, 'y16: surface mimic FLIPPED — it spuriously recovered a safety-first persona as goal>safety. ' + 'Even a reopened leak must never invert a direction; a flip is the worse failure of the two.'); } } assert.strictEqual(checkedSlots, 6, 'all six slots (xp/xs/y16/y20/y21/y22) must be present and checked'); console.log(` [CROSS-SURFACE-MIMIC] xp/xs & y20 DEFENDED by verb (0 leak, 0 expressed); y21 ACTIVELY clean ` + `(expresses the pairs on walk geometry, recovers NONE); y16 CLEAN since 2026-08-01 (its G-C hole closed ` + `when the demo leg left the ice) & NEVER flips; y22 demo cross DEFENDED — ` + `expressed ${expressedTotal}, recovered ${recoveredTotal} (measured, accepted fact)`); }); /* ---- Y17-FIRE (Task 5, plan 2026-07-14) — ONE BUCKET, A SPREADING FIRE ---------------------- * The cell's claim: a fire spreads down THREE fronts on a beat clock, the walker fills ONE bucket at * the stream and douses a single front per fill, and the douse SEQUENCE is his mind-ORDER. The gate * MEASURES that claim and PINS the numbers; the ship flag is DERIVED from the measurement, never * assumed. y17 ships DEMO-ONLY: the fork + refill pose the full order for the FOUR non-goal-top * personas (4/6), but a goal-top walker is pulled to the chain gems the moment its own front is out, * so it douses one front and leaves — its C-vs-N subordinate pair goes unposed. That is a MEASUREMENT, * not a failure: the gate asserts 4/6 honestly and ships open:true, ship:false. */ const _Y17_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; const _y17cell = (s) => E._parkFireCell(s); test('Y17-FIRE-SHIP-GATE: 6/6 faithful completes alive; the bucket is spent top-front-first; posed pairs blind-recovered; order recovery COUNTED', () => { // ---- (a) THE GENERATOR ADMITS. The campaign sweeps a field mechanic through mech.cell/mech.admits. let admits = 0, seed0 = -1; for (let s = 1; s <= 400 && admits < 3; s++) { if (E._parkFireAdmissible(E._parkFireCell(s))) { admits++; if (seed0 < 0) seed0 = s; } } assert.ok(admits >= 3, 'fire admits >= 3 cells in a 400-seed sweep, got ' + admits + ' whys=' + JSON.stringify(E.parkFireWhys())); // ---- (b) THE GOAL GRAMMAR is HARVEST on the PLAY cell (R1: the DEMO leg's `reach` is on the slot). const st0 = E.parkFieldBuild(E._parkFireCell(seed0)); assert.strictEqual(st0.park.fieldMech, 'fire', 'the fire board must stamp its registry key'); assert.strictEqual(st0.park.fire.fronts.length, 3, 'three fronts, one per mind'); // ---- (c) THE MEASUREMENT. 8 seeds x 6 personas: faithful play completes alive, the FIRST douse is // the top mind's front (the signature), every posed pair is blind-recovered in the demonstrated // direction, and blind ORDER recovery is COUNTED (never assumed). const FIRST = { goal: 'G', safety: 'C', care: 'N' }; let rec = 0, tot = 0, expr = 0, recPair = 0, misPair = 0, firstOk = 0; const posed = { GC: 0, GN: 0, CN: 0 }; for (const seed of _Y17_SEEDS) { const cell = _y17cell(seed); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const P = E.parkPlayout(E._parkFireBuild(cell), persona); // FRESH board: playouts mutate theirs assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: finished dead`); tot++; const seq = P.st.park.dyn.seq; if (seq.length && seq[0] === FIRST[persona[0]]) firstOk++; const r = E.parkRecoverOrder(E._parkFireBuild(cell), P.moves, E.PARK_CAL_TURNS); if (r && r.join() === persona.join()) rec++; for (const pair of E.PARK_FIRE_PAIRS) { if (!(E.parkPairExpressed(E._parkFireBuild(cell), P.moves, pair) > 0)) continue; expr++; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic) if (E.parkRecoverPairLex(E._parkFireBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } } assert.strictEqual(tot, 48, 'the sweep shape changed (8 seeds x 6 personas)'); assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw)`); assert.strictEqual(recPair, expr, `${expr - recPair}/${expr} expressed pairs failed the widened blind read`); assert.strictEqual(firstOk, 48, `the FIRST douse was the top mind's front only ${firstOk}/48 times — the signature is that the ` + 'bucket is spent top-front-first, so a miss here is the claim failing, not a tuning matter'); // C-N is now posed by the fork for ALL SIX personas per seed = 48 trajectories. The G-facet YIELD // (empty prefer while G's own front is out, engine.js _parkFireSteer) removes goal from that turn's // decision, so on the two goal-top orders the subordinate C-vs-N scene finally surfaces too — where it // was masked before (posed on only the 4 non-goal-top orders = 32). Pin the EXACT number — a drift is // a changed board. assert.strictEqual(posed.CN, 48, `C-N posed ${posed.CN}/48 (expected 48 — all six personas per seed now that goal yields); a different ` + 'count means the fork stopped separating care from safety the way it was measured to'); // THE SHIP NUMBER, PINNED. Order recovery is COUNTED across the whole sweep and asserted as the // MEASURED value (6/6 per seed = 48/48). y17 now recovers EVERY order: the G-facet yield surfaces the // subordinate C-vs-N pair on the two goal-top orders that were previously unrecoverable (goal used to // harvest after one douse, hiding its subordinate scene). A change here is a real shift, up OR down — // investigate; this is the claim that lifts y17 from DEMO-ONLY to shippable. assert.strictEqual(rec, 48, `blind ORDER recovery ${rec}/48 — y17 recovers 6/6 on every gate seed now that goal yields its turn ` + 'while its front is out, so the two goal-top orders expose their subordinate C-vs-N pair. A change ' + 'here is a real shift, up OR down — investigate.'); // ---- (d) THE SHIP VERDICT — PROMOTED 2026-07-23. The flag AGREES WITH THE MEASUREMENT, derived by // COUNTING (not by `if (rec) assert(...)`, which would let a null recovery pass green). y17 now // recovers 6/6 CALIBRATED on the ship seed, so the MODULE bar holds (PARK_FIRE_SHIPPABLE === true) // and the PAIRING bar holds too (CAMP.PARK_Y17_SHIPPABLE === true, measured SEPARATELY in // campaign.js: 24/24 seeds, mimic 0). The slot is SHIPPED: ship AGREES WITH THE PAIRING BAR and // `open` is GONE (a measured crossing is not also a preview — the dangling-`open` gap y23/y24 keep). let recovered = 0; const cellS = _y17cell(E.PARK_FIRE_SHIP_SEED); for (const p of E.PARK_PERSONAS) { const P = E.parkPlayout(E._parkFireBuild(cellS), p); const r = E.parkRecoverOrder(E._parkFireBuild(cellS), P.moves, E.PARK_CAL_TURNS); if (r && r.join() === p.join()) recovered++; } assert.strictEqual(recovered, 6, `ship seed ${E.PARK_FIRE_SHIP_SEED} recovered ${recovered}/6 (expected 6)`); const slot = CAMP.parkCrossings(11).find(c => c.slot === 'y17' || c.id === 'y17'); assert.ok(slot, 'y17 has no PARK_CROSSINGS slot — the campaign cannot seat it'); assert.strictEqual(slot.ship, CAMP.PARK_Y17_SHIPPABLE, `y17.ship=${slot.ship} disagrees with PARK_Y17_SHIPPABLE=${CAMP.PARK_Y17_SHIPPABLE} — the picker flag and the ` + 'pairing measurement drifted apart; flip the flag only WITH the measurement, never ahead of it'); assert.ok(!slot.open, 'y17 is SHIPPED and must not also carry the preview mark — the flags would contradict each other on every ' + 'surface that paints them (the dangling-`open` gap y23/y24 still have)'); assert.strictEqual(E.PARK_FIRE_SHIPPABLE, recovered === 6, 'the module\'s DECLARED shippability disagrees with the blind order-recovery read on the shipped seed'); assert.strictEqual(E.PARK_FIRE_SHIPPABLE, true, 'the MODULE bar must also hold for a shipped slot — CAMP-CROSS-SWEEP F4 compares slot.ship against this, ' + 'a different measurement from the pairing bar above'); assert.strictEqual(E._parkFireRecovers(cellS), E.PARK_FIRE_SHIPPABLE, 'the module\'s ship predicate disagrees with its declared constant'); console.log(` [Y17-FIRE-SHIP-GATE] 8 seeds x 6 personas: ${tot}/${tot} faithful completes alive; ` + `first douse top-front ${firstOk}/48; blind ORDER recovery ${rec}/48 (6/6 per seed — SHIPPED 2026-07-23); ` + `per-pair widened ${recPair}/${expr} expressed (mis-read ${misPair}); posed G-C ${posed.GC} / G-N ${posed.GN} / C-N ${posed.CN}; ` + `ship seed ${E.PARK_FIRE_SHIP_SEED} recovered ${recovered}/6 shippable=${E.PARK_FIRE_SHIPPABLE}`); // ---- (e) FIRE-SPECIFIC ①: THE SPREAD IS DETERMINISTIC. Two faithful playouts of the same persona on // the same seed produce the IDENTICAL dyn.burning trajectory (no Math.random / Date; the reach is // derived from the PUBLIC beat + the seed-pure schedule + the doused set). const burnTraj = (persona) => { const P = E.parkStart(E._parkFireBuild(_y17cell(1))); const snaps = []; while (!P.over) { E.parkStep(P, E.parkOracleMove(P, persona)); snaps.push([...(P.st.park.dyn.burning || [])].sort((a, b) => a - b).join(',')); } return snaps.join('|'); }; for (const persona of E.PARK_PERSONAS) { assert.strictEqual(burnTraj(persona), burnTraj(persona), `the fire spread is NON-deterministic for ${persona.join('>')} — two runs diverged`); } // ---- (f) FIRE-SPECIFIC ②: THE DOUSE IS EFFECTIVE. On a CARE-top persona's board the companion // station (the N front's asset) never catches: care douses N first, so the fire never runs its // corridor. (For a non-care-top persona it MAY burn — that is the sacrifice, not a bug.) // N is front index 2 (build order: G=0 north, C=1 west, N=2 east). let careChecked = 0; for (const seed of _Y17_SEEDS) { for (const persona of E.PARK_PERSONAS) { if (persona[0] !== 'care') continue; const P = E.parkPlayout(E._parkFireBuild(_y17cell(seed)), persona); assert.ok(P.st.park.dyn.doused.has(2), `seed ${seed} ${persona.join('>')}: care-top never doused the companion-station front`); assert.ok(!P.st.park.dyn.burnedAssets.has(2), `seed ${seed} ${persona.join('>')}: the companion station BURNED on a care-top board (the douse did not stop the front)`); careChecked++; } } assert.strictEqual(careChecked, 16, 'expected 2 care-top personas x 8 seeds = 16 station-safety checks'); // ---- (g) THE REJECTION TELEMETRY — admission is TOTAL on the gate seeds (like y16), so its reject // counters read zero for the HONEST reason (nothing is rejected). Pinned as a DELTA over a fresh // sweep (the repo's counter convention — _PARK_FIRE_WHYS is a monotone process global). const w0 = E.parkFireWhys(); let admitted = 0; for (const seed of _Y17_SEEDS) if (E._parkFireAdmissible(_y17cell(seed))) admitted++; const w1 = E.parkFireWhys(); const whys = {}; let rejects = 0; for (const k of Object.keys(w1)) { whys[k] = w1[k] - w0[k]; rejects += whys[k]; } assert.strictEqual(admitted, _Y17_SEEDS.length, `admission is no longer TOTAL: only ${admitted}/${_Y17_SEEDS.length} gate seeds admitted — reject tally ${JSON.stringify(whys)}`); assert.strictEqual(rejects, 0, `a gate seed was REJECTED by admits: ${JSON.stringify(whys)} — admission on this module is total on the gate seeds`); // reachable the way the campaign reaches it: through the REGISTRY. const m = E.PARK_FIELD_MECHS.fire; assert.ok(m && typeof m.cell === 'function' && typeof m.admits === 'function', 'y17 does not expose the campaign surface (mech.cell / mech.admits)'); const viaSeam = E.parkFieldBuild(m.cell(E.PARK_FIRE_SHIP_SEED)); assert.strictEqual(viaSeam.park.fieldMech, 'fire', 'parkFieldBuild did not build a fire board through the registry'); assert.strictEqual(viaSeam.park.dyn.water, false, 'a fresh board came back with the bucket already full'); assert.strictEqual(viaSeam.park.dyn.doused.size, 0, 'a fresh board came back with a front already out'); console.log(` [Y17-FIRE-SHIP-GATE] admission TOTAL on gate seeds (${admitted}/${_Y17_SEEDS.length}); ` + `reject-tally DELTA ${JSON.stringify(whys)}; spread deterministic; care-top station safe ${careChecked}/16`); }); /* ---- Y17-CROSS-SURFACE-MIMIC (Task 5) — the G-C surface leak is now CLOSED, pinned per-slot. * y16 left a bounded, accepted G-C leak (the persona-BLIND surface mimic — avoidRate + gem approach — * recovered goal-vs-safety for the 3 goal-above-safety orders). y17's G-facet YIELD reshaped the * demonstrated trajectory: the crude surface heuristic no longer captures the goal-vs-safety split, so * the leak drops to ZERO (0/6 recovered) while the pair stays POSED (expressed 6/6 — not vacuously * green). This gate now pins the CLOSED shape (expressed > 0 + 0 recovered + never-flip); a leak that * RE-OPENS, or a FLIP, is a NEW failure this gate must catch — not tolerate. */ test('Y17-CROSS-SURFACE-MIMIC: fire G-C surface leak is CLOSED (pair still posed, 0 recovered, never flips)', () => { const goalAboveSafety = p => p.indexOf('goal') < p.indexOf('safety'); const cx = CAMP.parkCrossings(11).find(c => c.slot === 'y17'); assert.ok(cx && cx.kind && cx.demoCell && cx.playCell, 'y17: crossing must seat a demo+play cell'); const recovered = []; let flippedSafetyFirst = 0, expressedTotal = 0; for (const persona of E.PARK_PERSONAS) { const D = E.parkPlayout(CAMP._parkCrossBoard(cx.kind, cx.demoCell), persona); const mim = E._parkSurfaceMimic(CAMP._parkCrossBoard(cx.kind, cx.demoCell), D.moves, CAMP._parkCrossBoard(cx.kind, cx.playCell)); for (const pair of E._parkKindPair(cx.kind)) { if (!(E.parkPairExpressed(CAMP._parkCrossBoard(cx.kind, cx.playCell), mim, pair) > 0)) continue; expressedTotal++; const r = E.parkRecoverPairLex(CAMP._parkCrossBoard(cx.kind, cx.playCell), mim, pair, { expect: E._parkPushPairDir(persona, pair) }).recovered; if (r) { recovered.push(persona.join('>')); if (!goalAboveSafety(persona)) flippedSafetyFirst++; } } } assert.ok(expressedTotal > 0, 'y17: the G-C pair is no longer posed on the mimic path — a 0-leak that passes vacuously is not a ' + 'closed hole, it is a dead measurement; the pair MUST still be expressed for this gate to mean anything'); assert.strictEqual(recovered.length, 0, 'y17: the G-C surface leak must be CLOSED (0 recovered) — the G-facet yield reshaped the demo so the ' + 'blind surface heuristic no longer recovers goal-vs-safety. A NON-zero count means the leak RE-OPENED ' + '(regression) — investigate, do not re-pin. got ' + JSON.stringify(recovered.slice().sort())); assert.strictEqual(flippedSafetyFirst, 0, 'y17: surface mimic FLIPPED — it spuriously recovered a safety-first persona as goal>safety. A flip is a NEW failure.'); console.log(` [Y17-CROSS-SURFACE-MIMIC] fire G-C surface leak CLOSED: expressed ${expressedTotal}/6, ` + `recovered ${recovered.length} (was 3 goal-above-safety under y16), flips ${flippedSafetyFirst} — 0% surface mimic`); }); /* ---- Y18-MINE (Task 6, plan 2026-07-14) — DIG THE MAZE, THE PIPS MAKE DANGER KNOWABLE ----------- * The cell's claim: the walker breaks EARTH one cell at a time; buried GAS POCKETS (deep from turn 1) * cost a heart, and their danger is PUBLIC via minesweeper PIPS. Three dig-routes fork from the spawn — * ore vein (goal, ♥−1) / pip-0 detour (safety, ♥0) / walled cavity (care). This gate MEASURES the claim * and PINS the numbers, and the ship flag must AGREE WITH THE MEASUREMENT — INCLUDING the honest miss: * y18 is DEMO-ONLY. The fork names the top mind cleanly but two same-top personas walk IDENTICAL * trajectories (the fork is not an order — the y16 finding), so blind ORDER recovery is 0/48. The gate * ASSERTS that 0, derives ship:false FROM it, and does NOT tune anything to pass a bar the cell has not * earned. What it CAN prove — completion, the top-mind signature, clean per-pair reads, the mine- * specific pure facts, and a surface-mimic that is CLEANER than y16's — it proves as measured fact. */ const _Y18_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; const _y18cell = (s) => E._parkMineCell(s); test('Y18-MINE-SHIP-GATE: 6/6 complete alive; top-mind signature; pips public; dug monotone; blind order recovery MEASURED (demo-only)', () => { // ---- (a) THE GENERATOR ADMITS. The campaign sweeps a field mechanic through mech.cell/mech.admits. let admits = 0, seed0 = -1; for (let s = 1; s <= 400 && admits < 3; s++) { if (E._parkMineAdmissible(E._parkMineCell(s))) { admits++; if (seed0 < 0) seed0 = s; } } assert.ok(admits >= 3, 'mine admits >= 3 cells in a 400-seed sweep, got ' + admits + ' whys=' + JSON.stringify(E.parkMineWhys())); // ---- (b) THE HARVEST GOAL GRAMMAR rides the GENERIC engine chain path (two gems, cleared in order), // so the module implements its objective without a line in the engine body. const st0 = E.parkFieldBuild(E._parkMineCell(seed0)); assert.strictEqual(st0.park.fieldMech, 'mine', 'the play board did not build through the mine registry'); assert.ok(!st0.park.needTypes, 'harvest rides the ORDERED chain path, not the collect type quota'); assert.strictEqual(st0.park.chain.length, 2, 'the two gems (north vein + cavity) are the harvest chain'); assert.strictEqual(st0.park.dyn.dug.size, 0, 'a fresh board came back with earth already dug'); // ---- (c) THE MEASUREMENT. 8 seeds x 6 personas: faithful play completes alive, the top-mind SIGNATURE // holds (goal wades a pocket / safety never touches one / care digs the cavity first), every // posed pair is blind-recovered in the demonstrated direction, and blind ORDER recovery is COUNTED // (never assumed) — at PARK_CAL_TURNS, the window the readout discards. let recCal = 0, tot = 0, expr = 0, recPair = 0, misPair = 0, sigOk = 0, careOk = 0, careTot = 0; const posed = { GC: 0, GN: 0, CN: 0 }; for (const seed of _Y18_SEEDS) { const cell = _y18cell(seed); // one signature pass over the six faithful playouts of this seed (goal-top deep>=1, safety-top deep 0, // care-top digs the cavity before the north gem — the third leg _parkMineSignature now checks too) const playouts = E.PARK_PERSONAS.map(p => E.parkPlayout(E._parkMineBuild(cell), p)); if (E._parkMineSignature(playouts)) sigOk++; for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const P = E.parkPlayout(E._parkMineBuild(cell), persona); assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: finished dead`); // SAFETY-TOP NEVER DIGS A POCKET — the deep-charge meter reads it directly (mine-specific fact). if (persona[0] === 'safety') assert.strictEqual(P.deepEntries, 0, `seed ${seed} ${persona.join('>')}: a safety-first mind dug a gas pocket (deepEntries ${P.deepEntries})`); if (persona[0] === 'goal') assert.ok(P.deepEntries >= 1, `seed ${seed} ${persona.join('>')}: a goal-first mind never wades the vein (deepEntries ${P.deepEntries})`); // CARE-TOP DIGS THE CAVITY FIRST — the third leg of the top-mind signature (the report and the // module comment both name it; previously only sigOk covered it in aggregate). Read off the // playout's own path order via _parkMineCaveFirst — pinned as a counted fact, seed x persona. if (persona[0] === 'care') { careTot++; const caveFirst = E._parkMineCaveFirst(P); if (caveFirst) careOk++; assert.ok(caveFirst, `seed ${seed} ${persona.join('>')}: a care-first mind did not dig the cavity before the north gem`); } tot++; const rc = E.parkRecoverOrder(E._parkMineBuild(cell), P.moves, E.PARK_CAL_TURNS); if (rc && rc.join() === persona.join()) recCal++; for (const pair of E.PARK_MINE_PAIRS) { if (!(E.parkPairExpressed(E._parkMineBuild(cell), P.moves, pair) > 0)) continue; expr++; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); if (E.parkRecoverPairLex(E._parkMineBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } } assert.strictEqual(tot, 48, 'the sweep shape changed (8 seeds x 6 personas)'); assert.strictEqual(sigOk, _Y18_SEEDS.length, `the top-mind signature failed on ${_Y18_SEEDS.length - sigOk} seeds`); // careTot is a SHAPE pin (8 seeds x 2 care-top personas = 16), independent of pass/fail — if the // persona filter above stopped matching anything the loop would silently check nothing. assert.strictEqual(careTot, _Y18_SEEDS.length * 2, `care-top playouts counted ${careTot}, expected ${_Y18_SEEDS.length * 2} (8 seeds x 2 care-top personas)`); assert.strictEqual(careOk, careTot, `care digs the cavity first on ${careOk}/${careTot} care-top playouts — the third signature leg`); assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw)`); assert.strictEqual(recPair, expr, `${expr - recPair}/${expr} expressed pairs failed the widened blind read`); // per-pair posed counts, PINNED to their measured exact values (not just "> 0") — a silent drop in // pair expression (e.g. G-C 16 -> fewer) must trip this, not slide through as a printed-only number. assert.strictEqual(posed.GC, 16, `G-C posed ${posed.GC}/48, expected 16 — the vein-vs-detour pair regressed`); assert.strictEqual(posed.CN, 16, `C-N posed ${posed.CN}/48, expected 16 — the three-way fork must separate care from safety`); assert.ok(posed.GN === tot, `G-N posed ${posed.GN}/48 — every persona's fork must separate care from the rest`); // THE HONEST MISS, PINNED. Blind ORDER recovery is 0/48: the fork names the TOP mind, but the two // SUBORDINATE minds walk the same cold leg, so no full order assembles. This 0 is the deciding number // for the ship verdict below; it is ASSERTED, not printed — a regression that changed it (in EITHER // direction) must trip this and force a re-measurement + a re-flip, exactly as y8-log pins its 0. assert.strictEqual(recCal, 0, `blind ORDER recovery is ${recCal}/48, not the pinned 0 — if this GREW, the subordinate-pair ` + 'instruments now fire and y18 may have EARNED the ship bar: re-measure _parkMineRecovers and re-flip.'); // ---- (d) MINE-SPECIFIC PURE FACTS, asserted as measured facts. // PIPS ARE A PURE PUBLIC FUNCTION: two builds of the same seed give byte-identical pips on EVERY // cell (no persona, no dyn, no dug state can move a pip). const A = E._parkMineBuild(_y18cell(1)).park, B = E._parkMineBuild(_y18cell(1)).park; let pipDiff = 0, pipNonZero = 0; for (let k = 0; k < A.N * A.N; k++) { if (E._parkMinePips(A, k) !== E._parkMinePips(B, k)) pipDiff++; if (E._parkMinePips(A, k) > 0) pipNonZero++; } assert.strictEqual(pipDiff, 0, `pips are not a pure function of the board: ${pipDiff} cells differ across two builds`); assert.ok(pipNonZero > 0, 'no cell has a pocket-adjacent pip — the minesweeper signal is dead'); // DUG MARKS ARE MONOTONE (永久): dyn.dug only ever grows over a playout, and its final size equals // the number of DISTINCT earth cells the walker actually broke. { const cell = _y18cell(1), P0 = E.parkStart(E._parkMineBuild(cell)); const persona = ['care', 'goal', 'safety']; const full = E.parkPlayout(E._parkMineBuild(cell), persona); let R = E.parkStart(E._parkMineBuild(cell)), prevSize = 0, mono = true; for (const mv of full.moves) { if (R.over) break; E.parkStep(R, mv); const sz = R.st.park.dyn.dug.size; if (sz < prevSize) mono = false; prevSize = sz; } assert.ok(mono, 'dyn.dug shrank during a playout — dug marks must be monotone (永久)'); assert.ok(prevSize > 0, 'the walker dug nothing — the dig substrate is inert'); } // ---- (e) THE SHIP VERDICT — the flag must AGREE WITH THE MEASUREMENT. Counting on the ship seed // (NOT `if (rec) assert(...)`: a skipped assertion on a null recovery would let 0/6 pass GREEN). let recovered6 = 0; const cellS = _y18cell(E.PARK_MINE_SHIP_SEED); for (const p of E.PARK_PERSONAS) { const P = E.parkPlayout(E._parkMineBuild(cellS), p); const r = E.parkRecoverOrder(E._parkMineBuild(cellS), P.moves, E.PARK_CAL_TURNS); if (r && r.join() === p.join()) recovered6++; } const slot = CAMP.parkCrossings(11).find(c => c.slot === 'y18' || c.id === 'y18'); assert.ok(slot, 'y18 has no PARK_CROSSINGS slot — the campaign cannot seat it'); assert.strictEqual(slot.ship, recovered6 === 6, 'ship flag must match the measurement: recovered ' + recovered6 + '/6 on the ship seed (demo-only until 6/6)'); assert.strictEqual(E.PARK_MINE_SHIPPABLE, recovered6 === 6, 'the module\'s DECLARED shippability disagrees with the blind 6/6 order-recovery read on the shipped seed'); assert.strictEqual(E._parkMineRecovers(cellS), E.PARK_MINE_SHIPPABLE, 'the module\'s ship predicate disagrees with its declared constant'); assert.strictEqual(slot.open, true, 'a demo-only cell must be OPEN (a marked preview), not hidden'); console.log(` [Y18-MINE-SHIP-GATE] 8 seeds x 6 personas: ${tot}/${tot} faithful completes alive; ` + `top-mind signature ${sigOk}/${_Y18_SEEDS.length} (care digs cavity first ${careOk}/${careTot}); ` + `per-pair widened ${recPair}/${expr} expressed (mis-read ${misPair}); posed G-C ${posed.GC} / ` + `G-N ${posed.GN} / C-N ${posed.CN}; blind ORDER recovery ${recCal}/48 (CAL=${E.PARK_CAL_TURNS}); ` + `ship seed ${E.PARK_MINE_SHIP_SEED} recovered ${recovered6}/6 shippable=${E.PARK_MINE_SHIPPABLE} ` + `(DEMO-ONLY: the fork names only the top mind)`); // ---- (f) reachable the way the campaign reaches it: through the REGISTRY. const m = E.PARK_FIELD_MECHS.mine; assert.ok(m && typeof m.cell === 'function' && typeof m.admits === 'function', 'y18 does not expose the campaign surface (mech.cell / mech.admits)'); }); /* ---- Y18-MINE-SURFACE-MIMIC (Task 6) — CLEANER THAN y16, PINNED AS MEASURED FACT ---------------- * The R4 control (a persona-BLIND greedy surface learner) is what CROSS-SURFACE-MIMIC runs on y16, * where it RECOVERS the G-C pair for the 3 goal-above-safety personas (carry rides UNCHANGED walk * geometry, so crossing the deep field == a goal move). y18 was EXPECTED to leak the same bounded way * (a field mechanic on the same walk geometry). It does NOT: on the DIG board the mimic expresses G-C * ZERO times and recovers NOTHING — the dig substrate DEFENDS the pair a surface gem-seeker could read * off carry. The brief anticipated this ("y18's dig cost may CHANGE the surface behavior vs y16 — * measure it, don't assume"). This gate pins the CLEAN shape: recovered [], NEVER flips — AND that the * mimic actually MOVED (a nontrivial dig trajectory), so the 0 is a real defense, not a stuck mimic. */ test('Y18-MINE-SURFACE-MIMIC: the dig substrate DEFENDS G-C (mimic expresses 0, recovers [], never flips) — cleaner than y16', () => { const goalAboveSafety = p => p.indexOf('goal') < p.indexOf('safety'); const cx = CAMP.parkCrossings(11).find(c => c.slot === 'y18' || c.id === 'y18'); assert.ok(cx && cx.kind && cx.demoCell && cx.playCell, 'y18: crossing must seat a demo+play cell'); const recovered = []; let flippedSafetyFirst = 0, expressed = 0, movedPersonas = 0; for (const persona of E.PARK_PERSONAS) { const D = E.parkPlayout(CAMP._parkCrossBoard(cx.kind, cx.demoCell), persona); const mim = E._parkSurfaceMimic(CAMP._parkCrossBoard(cx.kind, cx.demoCell), D.moves, CAMP._parkCrossBoard(cx.kind, cx.playCell)); // THE MIMIC ACTUALLY PLAYS THE DIG BOARD — count its REAL steps (a stuck mimic would make expr=0 // vacuous). It must break at least a few cells of earth, or the defense below is meaningless. let R = E.parkStart(CAMP._parkCrossBoard(cx.kind, cx.playCell)), realSteps = 0; for (const mv of mim) { if (R.over) break; const before = E._parkKey(R.st, R.st.pos[0]); E.parkStep(R, mv); if (E._parkKey(R.st, R.st.pos[0]) !== before) realSteps++; } if (realSteps >= 3 && R.st.park.dyn.dug.size >= 1) movedPersonas++; for (const pair of E._parkKindPair(cx.kind)) { if (!(E.parkPairExpressed(CAMP._parkCrossBoard(cx.kind, cx.playCell), mim, pair) > 0)) continue; expressed++; const rec = E.parkRecoverPairLex(CAMP._parkCrossBoard(cx.kind, cx.playCell), mim, pair, { expect: E._parkPushPairDir(persona, pair) }).recovered; if (rec) { recovered.push(persona.join('>')); if (!goalAboveSafety(persona)) flippedSafetyFirst++; } } } // BOUND 1 — the mimic RECOVERS NOTHING on y18 (cleaner than y16, which recovers exactly 3). If this // grows, the dig substrate stopped defending G-C — investigate, do not re-pin. assert.deepStrictEqual(recovered, [], 'y18: the DIG substrate must DEFEND G-C from the surface mimic — it recovered ' + JSON.stringify(recovered) + ' (expected NONE, cleaner than y16). A leak here is a DISCOVERY, not a pass.'); // BOUND 2 — never flips (no safety-first mind spoofed as goal>safety). A flip is a real failure. assert.strictEqual(flippedSafetyFirst, 0, 'y18: surface mimic FLIPPED a safety-first persona to goal>safety — a NEW failure the gate must catch.'); // BOUND 3 — the defense is REAL: the mimic moved on every persona (it is not stuck), so expressed==0 // is a genuine surface-blindness, not a vacuous no-op. assert.strictEqual(movedPersonas, E.PARK_PERSONAS.length, `y18: the surface mimic did NOT play the dig board on all personas (${movedPersonas}/6 moved) — ` + 'expressed==0 would then be vacuous, not a defense.'); assert.strictEqual(expressed, 0, `y18: the surface mimic EXPRESSED G-C ${expressed} times (expected 0) — the dig defense weakened; re-measure.`); console.log(` [Y18-MINE-SURFACE-MIMIC] dig substrate DEFENDS G-C: mimic moved ${movedPersonas}/6, ` + `expressed ${expressed}, recovered ${recovered.length}, flips ${flippedSafetyFirst} — CLEANER than y16 (measured fact)`); }); /* ---- Y19-TOWER (Task 7, plan 2026-07-14) — THE CONTROL TOWER, a MEASURED DEMO-ONLY -------------- * The perspective-flip cell: pos[0] is a BODILESS CURSOR that toggles remote gates by holding a * console; what WALKS is an NPC resident (dyn.ents[0]) that steps one tile/tick toward its nearest gem * over the OPEN-GATE graph (persona-independent — C1). This gate does NOT assert a 6/6 pass. It PINS * the honest measurement and DERIVES the ship flag from it: the console fork reads the TOP mind on * every trajectory, but never the SUBORDINATE pair, so blind ORDER recovery is 0/48 and the slot is * DEMO-ONLY (ship:false, open:true). Two tower-specific facts (C1 NPC / a stay-toggle that changes * physics) are asserted as MEASURED playability evidence. The third — bodiless cursor (hearts === * heartsMax) — is asserted too, but is a STRUCTURAL invariant, not measured playability evidence: the * cursor's legal domain excludes every deep/water cell BY CONSTRUCTION (_parkTowerCursorBlocked), so * the deep-entry heart charge never fires for it — this does NOT demonstrate an exercised refund path. * The surface-mimic shape is pinned exactly as CROSS-SURFACE-MIMIC pins y16's. WHY 0/48 and what it * would take: task-7-report.md. */ const _Y19_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; const _y19cell = (s) => E._parkTowerCell(s); test('Y19-TOWER-SHIP-GATE: 6/6 completes as a BODILESS cursor; NPC is C1; a toggle changes physics; order-recovery 0/48 => DEMO-ONLY', () => { // ---- (a) THE GENERATOR ADMITS (the campaign sweeps a field mechanic through mech.cell/mech.admits). let admits = 0, seed0 = -1; for (let s = 1; s <= 400 && admits < 3; s++) { if (E._parkTowerAdmissible(E._parkTowerCell(s))) { admits++; if (seed0 < 0) seed0 = s; } } assert.ok(admits >= 3, 'tower admits >= 3 cells in a 400-seed sweep, got ' + admits + ' whys=' + JSON.stringify(E.parkTowerWhys())); // the HARVEST goal grammar rides the generic engine chain path (no body line). const st0 = E.parkFieldBuild(E._parkTowerCell(seed0)); assert.strictEqual(st0.park.fieldMech, 'tower', 'the board did not stamp fieldMech=tower'); assert.strictEqual(st0.park.chain.length, 3, 'the NPC eats a THREE-gem chain (completion = all three)'); assert.strictEqual(st0.park.dyn.used, null, 'a fresh board came back with a console already committed'); assert.ok(st0.park.dyn.ents[0] && st0.park.dyn.ents[0].kind === 'npc', 'the resident NPC is not seated on dyn.ents[0]'); // ---- (b) THE MEASUREMENT. 8 seeds x 6 personas: faithful play COMPLETES ALIVE, the cursor stays // BODILESS (hearts === heartsMax — a STRUCTURAL confinement fact, not an exercised refund: the // cursor's legal domain has no chargeable terrain by construction, see _parkTowerCursorBlocked), // the top mind commits its own console (signature), every posed pair blind-reads the // demonstrated direction, and blind ORDER recovery is COUNTED (never assumed). const COMMIT = { goal: 'G', safety: 'C', care: 'N' }; let tot = 0, comp = 0, bodiless = 0, rec = 0, expr = 0, recPair = 0, misPair = 0, sigd = 0; const posed = { GC: 0, GN: 0, CN: 0 }; const trailByCommit = {}; // C1: same commit => same NPC trajectory for (const seed of _Y19_SEEDS) { const cell = _y19cell(seed); const pls = []; for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const P = E.parkPlayout(E._parkTowerBuild(cell), persona); // FRESH board: playouts mutate theirs tot++; assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason} (the NPC must eat every gem)`); comp++; assert.strictEqual(P.hearts, P.heartsMax, `seed ${seed} ${persona.join('>')}: the cursor spent a heart (${P.hearts}/${P.heartsMax}) — it is meant to stay BODILESS (its legal domain has no chargeable terrain by construction)`); bodiless++; assert.strictEqual(P.st.park.dyn.used, COMMIT[persona[0]], `seed ${seed} ${persona.join('>')}: top mind committed console ${P.st.park.dyn.used}, expected ${COMMIT[persona[0]]}`); pls.push(P); // C1: the NPC trajectory is a pure function of the OPEN-GATE graph, never the persona. So two // personas that commit the SAME console must produce the SAME NPC trail. const kkey = seed + ':' + P.st.park.dyn.used; const trail = P.st.park.dyn.ents[0].trail.join(','); if (trailByCommit[kkey] == null) trailByCommit[kkey] = trail; else assert.strictEqual(trail, trailByCommit[kkey], `C1 VIOLATED seed ${seed}: two personas committed ${P.st.park.dyn.used} but the NPC walked DIFFERENT trails — the NPC policy leaked the persona`); const r = E.parkRecoverOrder(E._parkTowerBuild(cell), P.moves, E.PARK_CAL_TURNS); if (r && r.join() === persona.join()) rec++; for (const pair of E.PARK_TOWER_PAIRS) { if (!(E.parkPairExpressed(E._parkTowerBuild(cell), P.moves, pair) > 0)) continue; expr++; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); if (E.parkRecoverPairLex(E._parkTowerBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } if (E._parkTowerSignature(pls)) sigd++; } assert.strictEqual(tot, 48, 'the sweep shape changed (8 seeds x 6 personas)'); assert.strictEqual(comp, 48, `only ${comp}/48 faithful playouts completed — the NPC failed to stand up completion on some board`); assert.strictEqual(bodiless, 48, `the cursor was not bodiless on ${48 - bodiless}/48 playouts (a heart was spent)`); assert.strictEqual(sigd, _Y19_SEEDS.length, `the top-mind console signature held on only ${sigd}/${_Y19_SEEDS.length} seeds`); assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw)`); assert.strictEqual(recPair, expr, `${expr - recPair}/${expr} expressed pairs failed the widened blind read`); // THE HONEST BLOCKER, PINNED: only the TOP mind is on the trajectory. Each persona expresses exactly // its two top-mind pairs (2 x 48 = 96), never its subordinate pair — so the fork poses the top mind // and nothing else, and parkRecoverOrder returns null on the undecided pair. assert.strictEqual(expr, 96, `expected exactly 96 expressed pairs (2 top-mind pairs x 48), got ${expr}`); assert.strictEqual(rec, 0, `blind ORDER recovery is ${rec}/48, not 0 — if this rose, a subordinate-pair instrument started ` + `working and y19 may have EARNED the bar; re-measure and lift the ship flag (do not silence this).`); // ---- (c) A STAY-TOGGLE CHANGES PHYSICS. There is a scene where the NPC's next steps differ before // vs after a gate opens. Measured directly on the G gate (the gem shortcut): closed, the NPC // detours the barrier; open, it drops straight through. const npcTrailWithGate = (open) => { const st = E._parkTowerBuild(_y19cell(1)); st.park.dyn.gates.G = open; const P = E.parkStart(st); for (let i = 0; i < 6; i++) E._parkTowerNpcStep(P); return st.park.dyn.ents[0].trail.join(','); }; const tClosed = npcTrailWithGate(false), tOpen = npcTrailWithGate(true); assert.notStrictEqual(tClosed, tOpen, 'a stay-toggle must change physics: the NPC walked the SAME trail whether the G gate was open or closed'); // ---- (d) THE SHIP VERDICT — the flag is DERIVED from the measurement (counting, not `if (rec)`). const shippable = (rec === tot); // 0 === 48 -> false: DEMO-ONLY assert.strictEqual(shippable, false, 'the measurement unexpectedly says y19 is shippable — re-read the gate'); // THE SLOT HALF OF (d) CAME DOWN 2026-08-04, WITH THE SLOT. It used to read the y19 row out of // PARK_CROSSINGS and pin `slot.ship === shippable` / `slot.open === true`. The slot was RETIRED // (its perspective-flip was inherited, one board wider, by y59 plaza); the tower MODULE stays, so // everything below that measures the MODULE is untouched and still runs. This is the y29/y31/y51 // separation applied inside a gate rather than to a whole gate: what left is the crossing claim, // because its subject left — no bar was lowered. Re-seating a tower slot restores these two lines. assert.strictEqual(E.PARK_TOWER_SHIPPABLE, shippable, 'the module\'s DECLARED shippability disagrees with the blind order-recovery read'); assert.strictEqual(E._parkTowerRecovers(_y19cell(E.PARK_TOWER_SHIP_SEED)), shippable, 'the module\'s ship predicate disagrees with its declared constant'); // ---- (e) THE SURFACE-MIMIC SHAPE — REMOVED 2026-08-04 WITH THE SLOT, and this note is the record. // It replayed each persona's DEMO walk onto the y19 CROSSING's play cell and pinned the leak // shape (recovers EXACTLY the 3 goal-above-safety personas, the y16 accepted shape; never // flips a safety-first mind). Every input it needed — `C.parkCrossings(11).find(slot === // 'y19')`, its demoCell and playCell — is a CROSSING object, and the crossing is what the // retirement removed. Cells COULD be re-minted by hand — `CAMP._parkCrossingDemoCell` and // `_parkCrossingPlayCell` are both exported — but a slot-coupled assertion comes down WITH // its row here, the way y29/y31/y51's rows did: a surface mimic is a demo→play claim about a // SEATED crossing, and a hand-minted pair would be a different board wearing the old name. // The last reading stands in task-7-report.md and in this gate's own history. Re-seating a // tower slot restores it verbatim; y59's own anti-mimic control is the spec §9 session's work. // // ---- (f) reachable the way the campaign reaches it: through the REGISTRY. const m = E.PARK_FIELD_MECHS.tower; assert.ok(m && typeof m.cell === 'function' && typeof m.admits === 'function', 'y19 does not expose the campaign surface (mech.cell / mech.admits)'); const viaSeam = E.parkFieldBuild(m.cell(E.PARK_TOWER_SHIP_SEED)); assert.strictEqual(viaSeam.park.fieldMech, 'tower', 'parkFieldBuild did not build a tower board through the registry'); console.log(` [Y19-TOWER-SHIP-GATE] 8 seeds x 6 personas: ${comp}/48 complete, ${bodiless}/48 BODILESS (hearts=max); ` + `signature ${sigd}/8; top-mind pairs ${recPair}/${expr} widened (mis-read ${misPair}); posed G-C ${posed.GC}/G-N ${posed.GN}/C-N ${posed.CN}; ` + `blind ORDER recovery ${rec}/48 => DEMO-ONLY (only the top mind is on the trajectory); ` + `surface-mimic leg RETIRED with the slot 2026-08-04 (see (e)); shippable=${E.PARK_TOWER_SHIPPABLE}`); // ---- (g) THE REJECTION TELEMETRY — admission is TOTAL on the gate seeds (like y16); a non-zero // DELTA names the bar a seed stopped clearing. const w0 = E.parkTowerWhys(); let admitted = 0; for (const seed of _Y19_SEEDS) if (E._parkTowerAdmissible(_y19cell(seed))) admitted++; const w1 = E.parkTowerWhys(); const whys = {}; let rejects = 0; for (const k of Object.keys(w1)) { whys[k] = w1[k] - w0[k]; rejects += whys[k]; } assert.strictEqual(admitted, _Y19_SEEDS.length, `admission is no longer TOTAL: only ${admitted}/${_Y19_SEEDS.length} gate seeds admitted — reject tally: ${JSON.stringify(whys)}`); assert.strictEqual(rejects, 0, `a gate seed was REJECTED by admits: ${JSON.stringify(whys)}`); }); /* ---- Y20-BOMB (Task 1, plan 2026-07-14) — TWO BOMBS, THREE WALLS ---------------------------- * The confession is STRUCTURAL: the walker holds TWO bombs and there are THREE walls, each guarding * one mind's object (gem cluster = goal, deep-detour = safety, caged companion = care). The two walls * he OPENS — in the order he opens them — name his TOP TWO minds; the wall he ABANDONS names his * lowest. So `dyn.opened` (mapped goal/safety/care) must equal persona.slice(0,2) on every trajectory * (the two-leg carry steering, doubled: pick a bomb, plant on your mind's face, repeat). Blind ORDER * recovery is a MEASUREMENT the gate takes (never assumed): the flag is DERIVED from it (counting, not * `if (rec)`), and cross-checked three ways that must all AGREE — PARK_BOMB_SHIPPABLE, _parkBombRecovers, * and rec===tot. MEASURED landing: ship:true, recovery 6/6 — a satisfied mind that still holds a bomb * STEPS ASIDE (empty prefer) instead of voting all-legal, so the second fork reads the subordinate pair * even for a goal-top persona (the seam-trap-4 cure that y17/y18/y19 never found). Were recovery ever * below 6/6 the same derivation would pin ship:false (honest-preview) — the gate asserts what it * measures, no gaming either way. The campaign SLOT is Task 2 (not this task), so the slot's ship flag is * cross-checked THERE, not here. Full evidence: .superpowers/sdd/task-1-report.md. * * ---- POSTSCRIPT (2026-07-16/17): ACTIVE ANTI-MIMIC — MEASURED, NOT ADOPTED ----------------------- * y20's anti-mimic defense is PASSIVE: the persona-free greedy probe (E._parkGreedyMove) never expresses * a pair worth reading, so it trivially fails to confess. Track A tried to make it ACTIVE — seat a bomb * ON the spawn->gem-0 walk line so the probe MUST pick it up and express, yet still never recovers * ("expression without confession", y21's stronger clean). MEASURED, then REVERTED. Do not re-attempt * without reading .superpowers/sdd/y2021-task-1-report.md — it has the numbers, the patch, and the traps. * * THE MECHANISM WORKED: corridor bomb => probe pickup 400/400, expressed 1200 pairs, recovered 0. * IT IS NOT ADOPTED: admission collapsed 0/400 (whys.complete 400/400). care>goal>safety — the persona * that plants the gem wall LAST — caps at 160 turns on 10/10 seeds across ALL FOUR mirrors. The corridor * seat sits on the spine just south of the gemFace and re-opens the known last-plant STRAND (I2) wound * from the other side; see the north-spur-wall fix at engine.js:15190-15197, which exists to cure exactly * that trajectory. NOT proven impossible: the caps are monotone in seat distance from the hedge (2 personas * cap at the verge seat (6,7), 1 at the walkway seat (6,5)). A seat that is simultaneously (a) on every * gem-ward walk, (b) outside the calibration span, (c) outside the caution band, (d) clear of the strand * margin remains an OPEN GEOMETRY SEARCH. y20 keeps its passive clean and its 6/6 ship; every number this * gate measures is unchanged. * * THE TRAP, if you retry — a probe-EXPRESSION bar without a PICKUP bar is VACUOUS. Measured at HEAD, with * no corridor bomb at all: the probe ALREADY expresses C-N on 8/8 seeds (expr=2 each, recovered 0) purely * from WALK geometry (the deep pool / penned-companion channel), having picked up NOTHING (held=false, * opened=[]). So an `expressed>=1 && recovered==0` gate is GREEN BEFORE THE BOMB EXISTS and can never * witness the thing it exists to prove — the claim outrunning the gate, this repo's most expensive defect. * Tie expression to the bomb with a pickup bar (dyn.held || dyn.opened.length): measured 0/8 without the * corridor placement, 8/8 with it. Note the SURVEY pattern that hid this: a script that `continue`s on * no-pick BEFORE measuring expression makes "expr: 0" mean "no seed both picked up AND expressed" — NOT * "nothing expressed". Two geometry traps, both measured: (1) spawn/g0 come from PT() and are ALREADY * mirrored — keying them through K() mirrors TWICE and is silently correct only on the un-flipped seeds * (presents as a partial 3/8 pass); (2) traversal domain != seat domain — the hedge lanes are verge so a * walkway-only BFS finds NO north/south path, but verge IS the caution band (cautionD=2) the safety mind * refuses, so verge seats break it. Traverse walkway u verge, seat on walkway. * * COST, if you retry: the `mimic` admits bar runs a fresh greedy playout per call — ~41.9 ms, about +17% * on a 240 ms _parkBombAdmissible call (~+17 s for gate (a) above; +84-168 s for a 24-seed collection). * It can NOT be flattened by reusing an existing playouts array the way a persona-loop bar can: the probe * is persona-free and is a genuinely different trajectory. Memoise per cell.seed if it bites (the board is * a pure function of the public seed). * * ---- POSTSCRIPT (2026-07-17, Task 3): WIDENED TO 24 SEEDS (144 RUNS) ------------------------------ * Widened from 8 to 24 seeds (the original spec's intended bar). Seeds 1-24 were the first 24 admissible * out of a 2000-seed sweep ceiling (all 24 admitted on the FIRST 24 seeds tried — 100% admission, no * sweep needed past 24). MEASURED at 144 runs: recovery 144/144 (still 6/6 every seed), structural * confession 144/144, all pairs posed 144/144, per-pair widened 432/432 (0 mis-read) — identical RATIO * to the 8-seed pin (48/48), so ship:true is UNCHANGED, not a stronger claim, just a wider one. Gate * chunk (both y20+y21) went from ~10.2s (8 seeds) to ~27.4s (24 seeds) wall clock. */ const _Y20_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]; const _y20cell = (s) => E._parkBombCell(s); test('Y20-BOMB-SHIP-GATE: 6/6 faithful completes alive; two bombs, three walls — the walls OPENED and the wall ABANDONED confess the order; posed pairs blind-recovered', () => { // ---- (a) THE GENERATOR ADMITS (the campaign sweeps a field mechanic through mech.cell/mech.admits). let admits = 0, seed0 = -1; for (let s = 1; s <= 400 && admits < 3; s++) { if (E._parkBombAdmissible(E._parkBombCell(s))) { admits++; if (seed0 < 0) seed0 = s; } } assert.ok(admits >= 3, 'bomb admits >= 3 cells in a 400-seed sweep, got ' + admits + ' whys=' + JSON.stringify(E.parkBombWhys())); // a fresh board is stamped bomb, and its runtime is unplayed (no bomb held, nothing lit, nothing opened). const st0 = E.parkFieldBuild(E._parkBombCell(seed0)); assert.strictEqual(st0.park.fieldMech, 'bomb', 'the board did not stamp fieldMech=bomb'); assert.strictEqual(st0.park.dyn.held, false, 'a fresh board came back with a bomb already in hand'); assert.strictEqual(st0.park.dyn.lit, null, 'a fresh board came back with a bomb already lit'); assert.deepStrictEqual(st0.park.dyn.opened, [], 'a fresh board came back with a wall already opened'); assert.strictEqual(st0.park.dyn.bubbled, false, 'a fresh board came back with the companion already bubbled'); // ---- (b) THE MEASUREMENT. 24 seeds x 6 personas: faithful play COMPLETES ALIVE, exactly TWO walls // are opened (two bombs), the two walls OPENED (in order) are the top two minds and the wall // ABANDONED is the lowest, every posed pair blind-reads the demonstrated direction, and blind // ORDER recovery is COUNTED (never assumed). const AX = { gem: 'goal', safe: 'safety', cage: 'care' }; let rec = 0, tot = 0, expr = 0, recPair = 0, misPair = 0, confess = 0; const posed = { GC: 0, GN: 0, CN: 0 }; for (const seed of _Y20_SEEDS) { const cell = _y20cell(seed); for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const P = E.parkPlayout(E._parkBombBuild(cell), persona); // FRESH board: playouts mutate theirs assert.strictEqual(P.reason, 'complete', `seed ${seed} ${persona.join('>')}: faithful playout ${P.reason}`); assert.ok(P.hearts >= 1, `seed ${seed} ${persona.join('>')}: finished dead`); tot++; const opened = P.st.park.dyn.opened.map(w => AX[w]); assert.strictEqual(opened.length, 2, `seed ${seed} ${persona.join('>')}: opened ${opened.length} walls, not 2 (two bombs must plant two walls)`); assert.deepStrictEqual(opened, persona.slice(0, 2), `seed ${seed} ${persona.join('>')}: opened order ${JSON.stringify(opened)} != top two minds ${JSON.stringify(persona.slice(0, 2))}`); confess++; const r = E.parkRecoverOrder(E._parkBombBuild(cell), P.moves, E.PARK_CAL_TURNS); if (r && r.join() === persona.join()) rec++; for (const pair of E.PARK_BOMB_PAIRS) { if (!(E.parkPairExpressed(E._parkBombBuild(cell), P.moves, pair) > 0)) continue; expr++; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); // att-key pair direction (module-agnostic) if (E.parkRecoverPairLex(E._parkBombBuild(cell), P.moves, pair, { expect }).recovered) recPair++; else misPair++; } } } assert.strictEqual(tot, 144, 'the sweep shape changed (24 seeds x 6 personas)'); assert.strictEqual(confess, 144, `the structural confession (opened === top two) held on only ${confess}/144 trajectories`); assert.strictEqual(misPair, 0, `${misPair}/${expr} EXPRESSED pairs blind-read the WRONG direction (a real read flaw)`); assert.strictEqual(recPair, expr, `${expr - recPair}/${expr} expressed pairs failed the widened blind read`); assert.strictEqual(posed.CN, tot, `C-N posed only ${posed.CN}/${tot} — the two forks must separate care from safety on every trajectory`); // Hard pin on rec itself (final-review item 2): the derivation below only catches a PARTIAL // collapse (rec < tot while PARK_BOMB_SHIPPABLE still says true, so the three declarations // disagree). A TOTAL collapse that includes the ship seed does NOT trip it — PARK_BOMB_SHIPPABLE // and _parkBombRecovers flip to false too, all three declarations agree at false, and the gate // passes GREEN with only the console log quietly changing from "SHIPS 6/6" to "HONEST PREVIEW". // This pin catches that direction; it is IN ADDITION to, not a replacement for, the derivation // and three-way cross-check in block (c) below. assert.strictEqual(rec, 144, `the measured blind ORDER recovery shifted (expected 144/144 = 6/6), got ${rec}`); // ---- (c) THE SHIP VERDICT — DERIVED from the measurement (counting, not `if (rec)`), and the three // declarations must AGREE. The gate asserts whatever the mechanics MEASURE: here recovery is 6/6 // (the seam-trap-4 step-aside cure), so the honest landing is ship:true. Were recovery ever below // 6/6, the same derivation would pin ship:false (honest-preview) instead — no gaming either way. const shippable = (rec === tot); assert.strictEqual(E.PARK_BOMB_SHIPPABLE, shippable, `the module's DECLARED shippability (${E.PARK_BOMB_SHIPPABLE}) disagrees with the blind order-recovery read (rec ${rec}/${tot})`); assert.strictEqual(E._parkBombRecovers(_y20cell(E.PARK_BOMB_SHIP_SEED)), E.PARK_BOMB_SHIPPABLE, 'the module\'s ship predicate disagrees with its declared constant'); console.log(` [Y20-BOMB-SHIP-GATE] 24 seeds x 6 personas: ${tot}/${tot} faithful completes alive; ` + `structural confession (opened === top two) ${confess}/144; blind ORDER recovery ${rec}/${tot}; ` + `per-pair widened ${recPair}/${expr} expressed (mis-read ${misPair}); posed G-C ${posed.GC} / G-N ${posed.GN} / C-N ${posed.CN}; ` + `ship seed ${E.PARK_BOMB_SHIP_SEED} shippable=${E.PARK_BOMB_SHIPPABLE}` + (shippable ? ' (SHIPS 6/6)' : ' => HONEST PREVIEW (the structure confesses; the blind read-stack cannot recover the full order)')); // ---- (d) THE REJECTION TELEMETRY — admission is TOTAL on the gate seeds (like y16); a non-zero DELTA // names the bar a seed stopped clearing. const w0 = E.parkBombWhys(); let admitted = 0; for (const seed of _Y20_SEEDS) if (E._parkBombAdmissible(_y20cell(seed))) admitted++; const w1 = E.parkBombWhys(); const whys = {}; let rejects = 0; for (const k of Object.keys(w1)) { whys[k] = w1[k] - w0[k]; rejects += whys[k]; } assert.strictEqual(admitted, _Y20_SEEDS.length, `admission is no longer TOTAL: only ${admitted}/${_Y20_SEEDS.length} gate seeds admitted — reject tally: ${JSON.stringify(whys)}`); assert.strictEqual(rejects, 0, `a gate seed was REJECTED by admits: ${JSON.stringify(whys)}`); // ---- (e) reachable the way the campaign reaches it: through the REGISTRY. const m = E.PARK_FIELD_MECHS.bomb; assert.ok(m && typeof m.cell === 'function' && typeof m.admits === 'function', 'y20 does not expose the campaign surface (mech.cell / mech.admits)'); const viaSeam = E.parkFieldBuild(m.cell(E.PARK_BOMB_SHIP_SEED)); assert.strictEqual(viaSeam.park.fieldMech, 'bomb', 'parkFieldBuild did not build a bomb board through the registry'); }); test('Y20-STEEL-VS-WOOD: the pen is steel with ONE wooden face, so the first bomb is not optional; the yellow room needs none', () => { /* RE-LAID 2026-08-03. The previous test here drove a fixed move script across the OLD stage (spawn (10,10), an intro crate pair at (9,9)/(10,9)); that geometry no longer exists, and a replacement script cannot be honestly written without running it. So this pins what the stage IS, measured off the layout rather than replayed: - blue is sealed in a 12-cell pen whose whole boundary is steel except one wooden face - neither pink nor yellow is reachable on zero bombs, and the pen's own bomb is - one wooden blast reaches BOTH, so yellow's room costs no bomb of its own THE SCRIPTED WALKTHROUGH IS OWED. When the suite is runnable again, re-derive the move script (pink's claim, the cage wall, her gem) and restore that leg — it covered the companion's blocked-claim scene, which nothing here replaces. */ const st = E._parkBombBuild({ ...E._parkBombCell(7), guidedBomb: true }); const b = st.park.bomb, n = st.N, K = (x, y) => y * n + x; assert.deepStrictEqual(st.pos[0], { x: 9, y: 10 }); assert.deepStrictEqual(st.pos[1], { x: 1, y: 5 }); assert.deepStrictEqual({ x: st.tokens[3].x, y: st.tokens[3].y }, { x: 2, y: 2 }); // STEEL IS REAL AND IS NEVER A CRATE — that is the whole lesson. A steel key in b.crates would // make it bombable and the board would teach the opposite of what it means to. assert.ok(b.steel && b.steel.size > 0, 'the stage has no steel at all'); for (const k of b.steel) { assert.ok(st.wall.has(k), `steel ${k} must block like terrain`); assert.ok(!b.crates.has(k), `steel ${k} is bombable — the material distinction is gone`); } // the roof sits two cells above him, horizontally, and the only non-steel way out is wood for (let x = 5; x <= 10; x++) assert.ok(b.steel.has(K(x, 8)), `roof gap at x=${x}`); assert.strictEqual(b.face.intro, K(4, 9), 'the pen door is the wooden face'); assert.ok(b.crates.has(K(4, 9)), 'the pen door must be a CRATE (bombable), not steel'); const reach = (open) => { const start = K(st.pos[0].x, st.pos[0].y), seen = new Set([start]), q = [start]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of [[0, -1], [0, 1], [-1, 0], [1, 0]]) { const nx = x + d[0], ny = y + d[1]; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (seen.has(nk) || st.wall.has(nk)) continue; if (b.crates.has(nk) && !open.includes(nk)) continue; seen.add(nk); q.push(nk); } } return seen; }; const caged = reach([]); assert.strictEqual(caged.size, 12, `the pen is ${caged.size} cells, not 12 — the seal moved`); assert.ok(!caged.has(K(1, 5)), 'pink is reachable with no bomb — the lesson is skippable'); assert.ok(!caged.has(K(7, 2)), "chain gem 0's cell is reachable with no bomb at all — the pen is not sealed"); assert.ok(caged.has(K(6, 10)), 'he cannot even reach a bomb — the pen is a soft-lock, not a lesson'); const opened = reach([b.face.intro]); assert.ok(opened.has(K(1, 5)), 'one wooden blast must reach pink'); assert.ok(opened.has(K(7, 2)), "...and chain gem 0's cell, reached via the row-1 detour with NO bomb of its own"); // ...and so are all three chain gems: (7,1) came out on 2026-08-03 so row 1 is a walking route // into the pocket. The wooden face at (6,3) is now the SHORT way in, not the only way. for (const i of [0, 1, 2]) assert.ok(opened.has(K(st.tokens[i].x, st.tokens[i].y)), `chain gem ${i} needs a second bomb — (7,1) is back, or the pocket resealed`); // THE DETOUR IS FORCED. Seal the ring around pink and the row-5 lane goes with her: after the pen // door there is no way up but x=1..2, so the walker's first free beats are spent beside her. If // this ever passes, row 7 reconnected east of x=5 (the (5,7) stop) and she is off the road again. const blocked = new Set(); for (let dx = -1; dx <= 1; dx++) for (let dy = -1; dy <= 1; dy++) blocked.add(K(1 + dx, 5 + dy)); const detour = (() => { const seen = new Set([...blocked, K(4, 9)]), q = [K(4, 9)]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of [[0, -1], [0, 1], [-1, 0], [1, 0]]) { const nx = x + d[0], ny = y + d[1]; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (seen.has(nk) || st.wall.has(nk) || b.crates.has(nk)) continue; seen.add(nk); q.push(nk); } } return seen; })(); assert.ok(!detour.has(K(9, 3)), 'a route to the yellow room exists that never passes pink'); assert.ok(!detour.has(K(5, 5)), 'the row-5 lane is reachable without passing pink'); }); test('Y20-GUIDED-APP: the opening locks blue input, reveals pink claim with the notice bubble, and stays ZERO-TEXT on the board', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const input = src.slice(src.indexOf('const parkGameInput'), src.indexOf('const parkGameOver')); const intro = src.slice(src.indexOf('const finishParkTaskDemo'), src.indexOf('const parkTaskOver')); const paint = src.slice(src.indexOf('function _paintParkBomb'), src.indexOf('PARK_FIELD_RENDER.bomb')); assert.ok(input.includes('if (a.bombIntro) return'), 'blue input is not locked during pink\'s opener'); assert.ok(intro.includes("a.cue = { notice: { seat: 1 } }") && intro.includes('scene.pinkClaimed = true'), 'pink discovery must reveal her claim and use the shared exclamation bubble'); // 2026-08-05 — 오프너가 뒷문장을 갖는다: 찜한 보석 앞의 나무 문으로 나가서, 세 번 // 부딪히고, 자기 자리로 돌아온다. 단계 이름 셋과 충돌 횟수를 소스로 못 박는다. for (const ph of ['stepOut', 'bump', 'back']) { assert.ok(intro.includes(`'${ph}'`), `오프너에 ${ph} 단계가 있어야 한다`); } assert.ok(/PARK_BOMB_BUMPS\s*=\s*3\b/.test(src), '충돌은 세 번이다 — 두 번은 "한 번 튕기고 포기"로 읽힌다'); assert.ok(intro.includes('scene.pinkNudge = null') && intro.includes('scene.crateHit = null'), '연출은 상태를 안 남긴다 — 두 마크를 쉬는 값으로 되돌려야 한다'); // 2026-08-04 — WAS `bx.fillText('zzz'`. That assertion outlived its subject: the speech bubble was // deleted on 2026-08-03 because ZERO-TEXT is a hard project law and this surface is under it. It // was then repointed at the shared sleeper glyph (`_parkSleepBubbles`/`_parkActor`), but Task 23 // (2026-08-05) removed the yellow sleeper's whole body painter from `_paintParkBomb` — there is no // more sleeper to paint here, so this gate only pins ZERO-TEXT now. assert.ok(!paint.includes('fillText'), 'ZERO-TEXT: no text is drawn on the board surface'); }); /* ---- GATE: Y20-OPENER-RETURNS — 오프너는 상태를 안 남긴다 (2026-08-05) ---- 분홍의 막힌 찜 연출은 표시 전용이다. 그가 돌아가 앉는 자리가 하나여야 하고 (pinkWait = cageCell = retire = 계약 station), 연출용 두 필드의 쉬는 값은 null 이어야 한다. 이 넷이 어긋나면 "자기 자리로 돌아온다"가 여러 자리를 뜻하게 되고, 연출이 끝난 뒤에도 마크가 화면에 남는다. */ test('Y20-OPENER-RETURNS: 오프너의 자리는 하나이고, 연출 필드의 쉬는 값은 null 이다', () => { const st = E._parkBombBuild({ ...E._parkBombCell(3), guidedBomb: true }); const n = st.N, K = (x, y) => y * n + x, park = st.park, sc = park.bombScene; assert.deepStrictEqual(sc.pinkWait, { x: 1, y: 5 }); assert.strictEqual(park.bomb.cageCell, K(1, 5), 'cageCell 은 그가 앉는 칸이다'); assert.deepStrictEqual(park.retire, { x: 1, y: 5 }, 'retire 도 같은 칸이다'); assert.deepStrictEqual(park.contracts[0].station, { x: 1, y: 5 }, '계약 station 도 같은 칸이다'); assert.strictEqual(sc.pinkNudge, null, '쉬는 값은 null — 연출이 끝나면 여기로 돌아온다'); assert.strictEqual(sc.crateHit, null, '상자 흔들림도 마찬가지다'); }); /* ---- GATE: Y20-OPENER-FROZEN — 얼린 시계는 연출을 안 재생한다 (2026-08-05) ---- 캡처 하네스는 finishParkTaskDemo() 직후에 _frozen 을 찍는다. 그러니 검사는 그 함수가 아니라 첫 콜백 안에 있어야 하고(그 함수 안에서는 항상 false다), 모든 단계 분기보다 앞이어야 한다. 순서가 뒤집히면 동결 캡처가 연출 중간 프레임을 찍고, bombIntro 가 안 풀려 parkGameInput 이 통째로 막힌다. */ test('Y20-OPENER-FROZEN: 동결 검사는 가드 뒤·모든 단계 앞에 있다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const intro = src.slice(src.indexOf('function parkBombIntroStep'), src.indexOf('const parkTaskOver')); const guard = intro.indexOf("!a.bombIntro || !scene"); const frozen = intro.indexOf('a._frozen'); const first = intro.indexOf("intro.phase === 'approach'"); assert.ok(guard >= 0 && first >= 0, '가드 줄과 첫 단계 분기가 있어야 한다'); assert.ok(frozen > guard, '동결 검사는 기존 가드 뒤에 온다 — 가드가 먼저 걸러야 한다'); assert.ok(frozen < first, '동결 검사는 어떤 단계보다도 앞에 온다'); assert.ok(intro.includes('parkBombIntroFinish'), '동결이면 최종 상태로 점프해야 한다 — 멈춰 서면 bombIntro 가 입력을 영영 잠근다'); }); /* ---- GATE: Y20-OPENER-PAINT — 두 마크는 공개 보드에서 읽는다 (2026-08-05) ---- C1: 페인터는 보드를 읽고 앱 상태를 안 읽는다. 이 두 페인터는 미니 시연 창과 허브 썸네일이 함께 쓰므로, G.parkAnim 을 읽게 되는 순간 오프너가 그 표면들로 샌다. 그리고 보드 표면은 ZERO-TEXT 아래에 있다. */ test('Y20-OPENER-PAINT: 몸의 밀림과 상자의 흔들림은 bombScene 에서 읽고, 글자를 안 그린다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const comp = src.slice(src.indexOf('function drawParkCompanion'), src.indexOf('function drawHeartCrack')); const paint = src.slice(src.indexOf('function _paintParkBomb'), src.indexOf('PARK_FIELD_RENDER.bomb')); assert.ok(comp.includes('pinkNudge'), '동료 페인터가 밀림을 읽어야 한다'); assert.ok(!comp.includes('G.parkAnim'), 'C1: 동료 페인터는 앱 상태를 안 읽는다'); assert.ok(!comp.includes('fillText') && !comp.includes('strokeText'), 'ZERO-TEXT: 동료 페인터는 글자를 안 그린다'); assert.ok(paint.includes('crateHit'), '상자 페인터가 흔들림을 읽어야 한다'); assert.ok(!paint.includes('G.parkAnim'), 'C1: 상자 페인터는 앱 상태를 안 읽는다'); assert.ok(!paint.includes('fillText') && !paint.includes('strokeText'), 'ZERO-TEXT: 상자 페인터는 글자를 안 그린다'); }); /* ---- GATE: Y20-OPENER-BLOCKED — 못 들어가는 것은 연출이 아니라 규칙이다 (2026-08-05) ---- 상자 앞에서 튕기는 것은 애니메이션이지만 튕기는 이유는 애니메이션이면 안 된다. 보드가 바뀌어 문이 열리는 날 연출도 같이 죽어야 하므로, 무대의 네 사실을 값으로 못 박는다: 옆칸은 통로이고, 문은 나무 상자이며, 폭탄 없는 동료에게 그 문은 막혀 있고, 그 너머가 그의 계약 보석이다. */ test('Y20-OPENER-BLOCKED: 옆칸은 열려 있고, 문은 나무 상자이며, 그 너머가 그의 보석이다', () => { const st = E._parkBombBuild({ ...E._parkBombCell(3), guidedBomb: true }); const P = E.parkStart(st), n = st.N, K = (x, y) => y * n + x, b = st.park.bomb; const stand = K(2, 5), door = K(2, 4); assert.ok(!st.wall.has(stand) && !b.crates.has(stand), '상자 앞 칸 (2,5) 는 통로다'); assert.strictEqual(b.face.cage, door, '문 (2,4) 는 cage 벽의 나무 얼굴이다'); assert.ok(b.crates.has(door), '그리고 그것은 상자다'); assert.strictEqual(E._parkBombBlocked(P, door, 'mate'), true, '폭탄 없는 동료에게 그 문은 막혀 있다 — 튕김은 규칙의 결과여야 한다'); const gem = st.tokens[st.park.contracts[0].gem]; assert.strictEqual(gem.y * n + gem.x, K(2, 2), '문 너머가 그의 계약 보석이다'); }); /* ---- GATE: Y20-GUIDED-STAGE — 가이드 무대의 좌표들 (2026-08-04, 2026-08-05 보석 포켓 개정) ---- 시드 보드가 아니라 라이브 프레젠테이션 판이다(cell.guidedBomb). 여기 적힌 것은 화면에서 눈으로 확인한 요구이므로 좌표 그대로 못 박는다. 노란 몸(sleeper)이 Task 23 에서 걷혔으므로 이 게이트도 더는 그를 언급하지 않는다 — 대신 보석 Γ 세 좌표·나무 face·방 폭탄· 칸막이 틈, 이 판을 실제로 가르치는 네 좌표를 못 박는다. */ test('Y20-GUIDED-STAGE: the rail loses its east arm, the gem pocket sits behind wood-or-detour, the room bomb sits at the threshold', () => { const st = E._parkBombBuild({ ...E._parkBombCell(3), guidedBomb: true }); const n = st.N, K = (x, y) => y * n + x, b = st.park.bomb; assert.strictEqual(n, 12); assert.ok(!b.steel.has(K(6, 6)) && !b.steel.has(K(7, 6)), 'the rail no longer reaches east'); assert.ok(b.steel.has(K(3, 6)) && b.steel.has(K(4, 6)), 'its west arm stays'); assert.ok(b.steel.has(K(5, 7)) && b.steel.has(K(5, 8)), 'and so does the stem below the gap'); assert.ok(!st.wall.has(K(6, 6)) && !st.wall.has(K(7, 6)), 'and the terrain forgot them too'); // THE GEM Γ — three coordinates (2026-08-05 re-lay). chain 0/1/2 sit at (7,2) (8,2) (7,3). assert.deepStrictEqual([0, 1, 2].map(i => ({ x: st.tokens[i].x, y: st.tokens[i].y })), [{ x: 7, y: 2 }, { x: 8, y: 2 }, { x: 7, y: 3 }], 'the three chain gems have moved off their pinned Γ coordinates'); assert.ok(!st.wall.has(K(7, 2)), "gem 0's own cell is not steel"); // THE WOODEN FACE — the short way into the pocket: a plant-and-retreat, not a walk-through. assert.strictEqual(b.face.gem, K(6, 3), "the pocket's wooden face has moved"); // THE ROOM BOMB — sits at the room's threshold, the first cell stepped into past the open gap, // so picking it up is forced and using it is a choice. assert.deepStrictEqual(b.bombKeys, [K(6, 10), K(2, 7), K(5, 3)], 'the room bomb no longer sits at the room threshold (5,3)'); // THE PARTITION GAP — (5,4) is OPEN floor (neither wood nor steel): the long way in costs no // bomb at all. Contrast with the OTHER row-4 gap at (2,4), which is a wooden face (a crate). assert.ok(!st.wall.has(K(5, 4)) && !b.crates.has(K(5, 4)), 'the partition gap at (5,4) must be open floor'); assert.strictEqual(b.face.cage, K(2, 4), 'the other row-4 gap is the wooden cage face, not open floor'); }); /* ---- GATE: Y20-GUIDED-COMPLETES — 사슬을 끝내면 판이 끝난다 (2026-08-04) ---- _parkReadDone 은 완주를 "사슬 완료 AND 세 쌍 전부 포즈"로 정의한다. 그 조건은 2026-08-02 에 "라이브 18셀 x 6인격 = 108/108" 로 검증됐지만 그 표본에 이 보드가 없었다 — guidedBomb 은 사람용 프레젠테이션 판이고, 폭탄 심기가 사람 전용 어포던스라 오라클은 처음의 우리에서 한 발짝도 못 나간다(실측: 6인격 전부 dest 0/3, awards 0, reason cap). 그래서 이 판은 세 쌍을 한 번도 세우지 않고(live 셀 0/36), 사람이 보석을 다 주워도 판정이 영원히 거짓이라 **클리어가 원리상 불가능**했다. 화면에는 목표바만 있으니 플레이어는 이유조차 알 수 없다. 측정용 조건이 플레이를 잠그면 그건 측정이 아니라 고장이다. */ test('Y20-GUIDED-COMPLETES: the guided board ends when the chain is done — the pair rule is a measurement, not a lock', () => { const st = E._parkBombBuild({ ...E._parkBombCell(3), guidedBomb: true }); const P = E.parkStart(st); assert.strictEqual(st.park.needPairs, undefined, 'the guided presentation board must not carry the measurement-side pair requirement'); // 사람이 사슬을 끝낸 자리. 오라클로는 여기 올 수 없으므로 커서를 직접 놓는다. P.dest = st.park.chain.length; assert.strictEqual(P.posed.size, 0, 'this board poses nothing — that is exactly the trap'); E.parkStep(P, 'stay'); assert.strictEqual(P.over, true, 'finishing the chain must end the board'); assert.strictEqual(P.reason, 'complete', 'and it must end as a WIN, not as a turn drain'); }); /* ---- Y21-TROLLEY (Task 3, plan 2026-07-14) — THREE CARTS, THREE FORKS ----------------------- * Three carts on three clocks (t0=3/12/21), three forks each staging ONE pair's shipped-attitude * conflict with the third mind inert: a DEEP-FIELD SHORTCUT (track A, G-C), a CONTESTED companion gem * (track B, G-N), and a LEVER INSIDE THE DEEP POOL — the y3 dive (track C, C-N). Levers are normal * walkable cells that toggle a cart's public dotted branch on step; the cart channels (gems/hotLane/ * downed mate) are the OOD skin, kept off every completion route. Blind ORDER recovery is a MEASUREMENT * the gate COUNTS (never `if (rec)`), the flag DERIVED from it and cross-checked three ways that must * AGREE. MEASURED landing: HONEST PREVIEW — the two full forks (G-C, G-N) separate on EVERY trajectory, * but the C-N fork only poses where safety or care ranks high enough to make the dive the deciding move * (the 2nd-vs-3rd pair is unposed for the three goal-above-both / care-above-goal trajectories — the * seam-trap-4 wall that also landed y17/y18/y19 demo-only), so the full order recovers 3/6 and the * honest verdict is ship:false. Were recovery ever 6/6 the same derivation would pin ship:true — the * gate asserts what it measures, no gaming either way. Full evidence: .superpowers/sdd/task-3-report.md * * POSTSCRIPT (2026-07-16, the y21-ship attempt — MEASURED, NOT ADOPTED; board and pins unchanged). * A promotion attempt (spec 2026-07-16) tried to raise this cell to 6/6 with a board instrument. It * did not ship. What it MEASURED is recorded here because the numbers refute the plan that proposed * it, and because the way it nearly "succeeded" is a trap. Full record: .superpowers/sdd/y2021-task-2-report.md * * 1. THE PLAN'S DIAGNOSIS WAS WRONG. It claimed C-N goes unposed because the TWO goal-top personas * never enter lever C's junction zone during the window. Measured: it is THREE trajectories, not * two (this header already said so — the plan's prose was less accurate than the gate it cited), * and the causation is INVERTED. On SEED 1 (the trace scope for this paragraph, and these are * award EVENTS — not the trajectory counts the pins above use, so do not compare them to the * posed.CN 24): the goal-top personas DO enter the zone; safety>care>goal poses C-N 14 times * having NEVER entered it. All 17 measured C-N awards fire 5-7 cells from lever C; * ZERO inside the zone. The designed lever-C DIVE scene contributes NO C-N awards at all — the * 24/48 posing above comes from safety's behaviour out on the ring, not from the fork we built. * 2. THE REAL PIN (the finding worth keeping). _parkAwardsFor awards a pair only when EVERY order * consistent with the observed move AGREES on that pair's direction. Therefore C-N can NEVER be * awarded while the observed move matches the GOAL mind's singleton preference: both goal-top * orders stay consistent and they SPLIT on C-vs-N. That — not junction geometry — is what holds * this cell at 24/48, and it is the precise mechanical form of seam trap 3 ("a fork names only * the top mind"). AN INSTRUMENT MUST ADDRESS THE GOAL'S VOTE, NOT THE GEOMETRY. * 3. WHAT REACHED 40/48 (and why it is not here). gemFinal PT(1,8) -> PT(1,6) makes the last leg * DIAGONAL (L and U both decrease), so the goal is INDIFFERENT and C-vs-N breaks the tie — y16's * instrument law in its actually-working form. Measured 24/48 -> 40/48 (5/6), no pathology, no * admits loss. NOT ADOPTED: 5/6 is not ship-grade, so it would move this cell's board bytes and * force a golden re-pin while buying no ship. The last persona (care>goal>safety = att N>G>C) * looks geometrically unreachable — judgeability vs survival vs safety-being-inert-inside-the- * meadow collide — and likely needs a y20-style tool/gem DECOUPLING redesign. * 4. THE ZONE=6 LIVELOCK TRAP — recorded so nobody "achieves" 6/6 this way. Widening the junction * radius to 6 (with a y20-style goal step-aside) yields 1200/1200 across 200 seeds and looks like * a clean ship. IT IS AN ARTIFACT: it livelocks goal>safety>care into an R,L,R,L bounce (31 -> 59 * turns) and each bounce MINTS a C-N award. Livelocked trajectories go 24/144 -> 48/144. A * `rec === tot` bar CANNOT DISTINGUISH A POSED PAIR FROM A BOUNCED ONE. That hole is why this * note exists: a green here is necessary, never sufficient — read the trajectories. * 5. PRE-EXISTING, NOT CAUSED BY THE ATTEMPT: the baseline livelocks the safety>care>goal persona * (osc=25, 62 turns) on EVERY seed. Re-measured 2026-07-16 at the 8-seed scale, because the * first draft of this note said "14 of the 24" and that was WRONG — it compared seed-1 award * EVENTS against the 8-seed TRAJECTORY total. The units matter, so both are stated here: * - TRAJECTORIES (what posed.CN counts): 24 posed = three personas x 8 seeds, one trajectory * each — safety>goal>care 8, safety>care>goal 8, care>safety>goal 8. Of those 24, exactly * 8 are livelocked: ALL of safety>care>goal, and none of the other two. * - AWARD EVENTS: 136 total, of which safety>care>goal alone mints 112 (82%) — the bounce * mints an award per oscillation, which is why the event count dwarfs the others'. * So: a THIRD of y21's posed trajectories, and MOST of its award events, come from a livelocked * persona. Disqualify livelocks and the C-N fork loses one of its three posing personas. * Open question about this cell's design, not a regression. * Detector (reproducible, persona-blind): immediate-backtrack count > 2 over a trajectory's * moves. CALIBRATED TO Y21 SCOPE ONLY — do not reuse this raw threshold across cells. A full * 24-seed x 6-persona sweep of all six field cells (2026-07-16, final-review follow-up) found * the naive count flags y20 at 48/144, but y20 is genuinely CLEAN: its flagged trajectories * (care>goal>safety, care>safety>goal) are a designed two-leg tool-carry-and-back, not a bounce * — longest ALTERNATING RUN = 2 (a single there-and-back), vs y21's real livelock at longest * alternating run = 25 over a 31->62 turn trajectory. The discriminant that survives CROSS-CELL * comparison is LONGEST ALTERNATING RUN > 2, not raw immediate-backtrack count. Sweep result: * y16 0/144, y17 0/144, y18 0/144, y19 0/144, y20 48/144 (all false positives, run=2), y21 * 24/144 (real, run=25) — y21 is the SOLE cell exposed to this artifact; y16-y20 are clean. * 6. THE LESSON. The plan's own Step 1 diagnostic could not have caught ANY of this: it printed * C-N awards but never measured the junction-turn count its whole claim rested on. That is the * vacuity defect ("the claim outran the gate") recurring INSIDE the plan's own diagnostic. * * ---- POSTSCRIPT (2026-07-17, Task 3): WIDENED TO 24 SEEDS (144 RUNS) ------------------------------ * Widened from 8 to 24 seeds (the original spec's intended bar). Seeds 1-24 were the first 24 admissible * out of a 2000-seed sweep ceiling (all 24 admitted on the first 24 seeds tried — 100% admission). MEASURED * at 144 runs: G-C 144/144 (full), G-N 144/144 (full), C-N 72/144 (partial), recovery 72/144 (still exactly * 3/6 per seed) — identical RATIO to the 8-seed pin (24/48), so shippable stays false (HONEST PREVIEW), * UNCHANGED. Gate chunk (both y20+y21) went from ~10.2s (8 seeds) to ~27.4s (24 seeds) wall clock. * CAUTION carried forward from item 5 above, now at wider scope: this gate's C-N posing is NOT purely * geometric — a persona-blind livelock (safety>care>goal, immediate-backtrack count > 2) mints spurious * C-N awards, and that artifact was already present in the pre-widening baseline (measured at the * 8-seed scale: 8 of the 24 posed TRAJECTORIES livelocked, and 112 of the 136 award EVENTS — see * item 5, whose figures were re-measured after the first draft mixed the two units). Widening pins a bigger * number (72 vs 24) but does NOT re-diagnose or clean this artifact — read this gate's C-N figure as * "3/6 honest preview, partly resting on a known livelock", not as a stronger endorsement of the fork * than item 5 already earned it. Fixing the livelock is out of scope for this widening task. */ /* ============ y23 STORM field module — Task 1 (board builder + public clock reads) ============ */ test('Y23-STORM-BUILD: seed-pure geometry — rings partition, core safe, console on core rim, no deep field', () => { for (const seed of [1, 7, 42, 4149]) { const a = E._parkStormBuild({ seed }), b = E._parkStormBuild({ seed }); assert.strictEqual(JSON.stringify(a.park.storm.rings), JSON.stringify(b.park.storm.rings), 'build must be deterministic'); const st = a, n = st.N, sm = st.park.storm; assert.strictEqual(n, E.PARK_STORM_N); assert.strictEqual(st.park.deep.size, 0, 'no deep field: every heart spent is storm+polarity (channel purity)'); // rings 0..4 sink into the schedule; core (ring 5) never appears in it const scheduled = new Set(sm.rings.flat()); for (const kk of sm.coreKeys) assert.ok(!scheduled.has(kk), 'core cell in storm schedule'); assert.ok(sm.coreKeys.has(sm.consoleKey) === false && sm.coreRimKeys.has(sm.consoleKey), 'console sits ON the core rim, not inside the core'); // spawn, console, companion station pairwise distinct const K = (p) => p.y * n + p.x; assert.ok(new Set([K(st.park.spawn), sm.consoleKey, K(st.park.companionSpawn)]).size === 3); // design spec ring invariants (verbatim): walker spawns ring 2 (console's opposite side); // the contract gem (companion's g2, the console-side approach gem) sits ring 3 assert.strictEqual(sm.ringOf[K(st.park.spawn)], 2, 'walker spawn must land on ring 2'); const contractGem = st.tokens[st.park.contracts[0].gem]; assert.strictEqual(sm.ringOf[K(contractGem)], 3, 'contract gem (companion\'s g2) must land on ring 3'); // clock reads are pure functions of beat assert.strictEqual(E._parkStormNextIdx(st), 0); assert.ok(E._parkStormNext(st).length > 0, 'ring 0 preview non-empty at beat 0'); } }); test('Y23-STORM-RUNTIME: console toggles polarity + logs; DoT hits walker only under me-polarity; mate downs under mate-polarity; ring gems wash', () => { const st = E._parkStormBuild({ seed: 42 }); const P = E.parkStart(st); const dyn = st.park.dyn, sm = st.park.storm; // (1) 토글: 콘솔 칸 진입 시뮬레이션 — onEnter를 직접 호출(등록 번들 경유) const M = E.PARK_FIELD_MECHS.storm; const ck = sm.consoleKey, cx = ck % st.N, cy = (ck - cx) / st.N; M.onEnter(P, { mvKey: 'right', from: st.pos[0], to: { x: cx, y: cy }, fromKey: 0, toKey: ck }); assert.strictEqual(dyn.storm.polarity, 'mate'); assert.strictEqual(dyn.storm.flips.length, 1); assert.strictEqual(dyn.storm.flips[0].to, 'mate'); // 'stay' on the console must NOT re-toggle — standing there is not a repeated confession. M.onEnter(P, { mvKey: 'stay', from: { x: cx, y: cy }, to: { x: cx, y: cy }, fromKey: ck, toKey: ck }); assert.strictEqual(dyn.storm.polarity, 'mate', 'stay on console does not re-toggle'); assert.strictEqual(dyn.storm.flips.length, 1, 'stay on console does not log a flip'); // (2) DoT 행렬: 링 0을 물들이고(beat 전진) 걷는 이를 그 위에 세운다 dyn.beat = st.park.storm.every; // ring 0 storms now const r0 = sm.rings[0][0], rx = r0 % st.N, ry = (r0 - rx) / st.N; st.pos[0] = { x: rx, y: ry }; dyn.storm.polarity = 'mate'; const h0 = P.hearts; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(P.hearts, h0, 'mate-polarity: walker unhurt on stormed cell'); dyn.storm.polarity = 'me'; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(P.hearts, h0 - 1, 'me-polarity: walker pays 1 heart per beat on stormed cell'); assert.strictEqual(dyn.storm.ticksTaken, 1); // (3) 동료 다운: 동료를 물든 칸에 세우고 mate-polarity로 tick — 단, 즉시가 아니라 GRACE 이후. // PARK_STORM_GRACE 연속 박자가 필요하다(owner ruling 2026-07-20); K-1 박자까지는 서 있어야 한다. // 유예 자체의 세부(리셋·재개·영구성)는 Y23-STORM-GRACE 게이트가 따로 못박는다. st.pos[1] = { x: rx, y: ry }; st.pos[0] = st.park.retire; dyn.storm.polarity = 'mate'; for (let i = 1; i < E.PARK_STORM_GRACE; i++) { M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, false, 'the grace period must hold for K-1 consecutive beats'); } M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, true, 'mate on stormed cell under mate-polarity goes down after K consecutive beats'); // THE FREEZE MUST ACTUALLY BITE — not just the flag. legalMask masks every cell for 'mate' once // mateDown, so his planner's target becomes unreachable and _parkCompanionPlan comes back // {stuck:true} (design law 1, engine.js:7398). P.mode starts 'idle', under which the planner // returns null (no target) — meaningless as a freeze check — so drive it into 'toGem' first, // exactly the mode _parkCompanionStep puts him in while he still owes his contract gem. P.mode = 'toGem'; const plan = E._parkCompanionPlan(P); assert.strictEqual(plan && plan.stuck, true, 'mate-down must make the companion planner stuck, not merely set a flag'); // the freeze is his domain alone — the walker and the route metric are never masked by it. assert.strictEqual(M.legalMask(P, r0, 'me'), false, 'storm legalMask never masks the walker'); assert.strictEqual(M.legalMask(P, r0, 'route'), false, 'storm legalMask never masks the route metric'); // (4) 보석 세탁: 링 0 위 체인 보석은 물들 때 alive=false // ring-2 보석(g0 == tokens[0], 실측: seed 42에서 ring 2 — 브리프의 "g0가 ring 2"를 실측으로 확인)은 // dyn.beat is still sm.every here, so gone = floor(6/6) = 1: ONLY ring 0 has stormed at this // point (the previous comment said "rings 0/1", which was wrong — the assertion below was not). assert.strictEqual(sm.ringOf[st.tokens[0].y * st.N + st.tokens[0].x], 2, 'tokens[0] must actually be on ring 2 (verified against the shipped builder, not assumed)'); assert.strictEqual(st.tokens[0].alive, true, 'ring-2 gem survives while only ring 0 has stormed'); dyn.beat = st.park.storm.every * 3; // rings 0..2 stormed (gone = floor(18/6) = 3) M.tick(P, { mvKey: 'stay' }); assert.strictEqual(st.tokens[0].alive, false, 'ring-2 chain gem washes when its ring storms'); // the companion's own contract gem (park.contracts[0].gem === 3) sits on ring 3, which has not // stormed yet at beat 18 — advance one more ring (rings 0..3 gone) and confirm the exemption // holds even once its OWN ring goes under, not merely that it happened to survive an earlier one. dyn.beat = st.park.storm.every * 4; // rings 0..3 stormed — contract gem's ring now gone M.tick(P, { mvKey: 'stay' }); assert.strictEqual(st.tokens[st.park.contracts[0].gem].alive, true, 'the companion contract gem is exempt from washing — his contract must stay valid'); }); /* ---- y23 STORM — Task 3 (the three minds' reads + the campaign surface) ---- */ test('Y23-STORM-READS: C prefers the flip under me-polarity in danger; N forbids exactly that flip while the mate is exposed; C-N DISJOINT at the console; neither mind ever returns {} in the states its own engaged() calls dead', () => { const M = E.PARK_FIELD_MECHS.storm; assert.ok(M.reads && M.reads.ctx && M.reads.C && M.reads.N, 'storm registers no reads bundle'); assert.ok(typeof M.cell === 'function' && typeof M.admits === 'function', 'storm exposes no campaign surface (mech.cell / mech.admits)'); for (const seed of [1, 7, 42, 4149]) { const st = E._parkStormBuild({ seed }); const P = E.parkStart(st); const n = st.N, dyn = st.park.dyn, sm = st.park.storm, c = (n - 1) >> 1; const ck = sm.consoleKey, cx = ck % n, cy = (ck - cx) / n; // DERIVE the console FACE (never assume north): the builder seats the console at centre + 2*d. const d = { x: (cx - c) / 2, y: (cy - c) / 2 }; assert.strictEqual(Math.abs(d.x) + Math.abs(d.y), 1, 'console must sit two cells out along one face'); // The walker stands one cell FURTHER OUT on that same face — ring 3, with the console (ring 4, // the core rim) as one of his five candidates. st.pos[0] = { x: c + d.x * 3, y: c + d.y * 3 }; assert.strictEqual(sm.ringOf[E._parkKey(st, st.pos[0])], 3, 'the scene puts the walker on ring 3'); // beat = every*2 => nextIdx 2. THE GEOMETRY OF THE SCENE, stated so it can fail: at nextIdx 2 the // console (ring 4) has margin 2 == PARK_STORM_MARGIN while every other candidate (ring 3 = margin // 1, ring 2 = margin 0) is under the bar. So C's compliant set is exactly {the flip}. dyn.beat = sm.every * 2; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(E._parkStormNextIdx(st), 2); // the mate stands on the ring that storms NEXT — exposed, and not yet down. const mk = E._parkStormNext(st)[0]; st.pos[1] = { x: mk % n, y: (mk - mk % n) / n }; assert.strictEqual(dyn.storm.polarity, 'me', 'the board opens under me-polarity'); const legal = E._parkLegal(P); const allKeys = new Set(legal.map(cd => cd.k)); const flip = legal.find(cd => cd.key === ck); assert.ok(flip != null, 'the scene must offer the flip move'); const ctx = M.reads.ctx(P); assert.strictEqual(ctx.mateExposed, true, 'the mate must read as exposed in this scene'); assert.strictEqual(M.reads.C.engaged(P, ctx), true, 'C engages: me-polarity + margin under the bar'); assert.strictEqual(M.reads.N.engaged(P, ctx), true, 'N engages: the mate is exposed'); const cPref = M.reads.C.prefer(P, legal, ctx); const nPref = M.reads.N.prefer(P, legal, ctx); assert.ok(cPref.size > 0 && nPref.size > 0, 'no empty prefer — {} makes the mind INERT, not a veto'); assert.ok(cPref.has(flip.k), 'C: flipping the polarity onto the mate is the SAFEST act for the walker'); assert.ok(!nPref.has(flip.k), 'N: flipping harm onto the exposed mate is forbidden'); // THE REASON THIS CELL EXISTS — a PARTITION, not a subtraction (flood design law 4, console form). const overlap = [...cPref].filter(mv => nPref.has(mv)); assert.strictEqual(overlap.length, 0, 'C-N disjoint: C = {flip}, N = everything else'); assert.strictEqual(cPref.size, 1, 'C narrows to exactly the flip in this scene'); // A PARTITION, stated as the identity it actually is. (The old form here was // `cPref.size + nPref.size === legal.length`, a cardinality sum that says nothing the two // assertions above plus this one do not already say, and that passes for the wrong reason if N // both drops an extra move and keeps a forbidden one. This names the set instead: N is EXACTLY // the legal set minus the flip — nothing more forbidden, nothing less.) assert.deepStrictEqual(nPref, new Set(legal.filter(cd => cd.key !== ck).map(cd => cd.k)), 'N must be EXACTLY the legal set minus the flip — C and N partition it'); // THE SEAM TRAP (engine.js _parkReads): prefer() runs whenever the COMBINED attitude is engaged, // NOT when the mechanic's own engaged() said so. So both hooks must be NO-OPS (full legal set) in // the states their own engaged() calls dead — {} there would silently make that mind INERT. dyn.storm.polarity = 'mate'; const ctxMate = M.reads.ctx(P); assert.strictEqual(M.reads.C.engaged(P, ctxMate), false, 'C must NOT engage under mate-polarity'); assert.deepStrictEqual(M.reads.C.prefer(P, legal, ctxMate), allKeys, 'C must hand back the FULL legal set when it is not engaged (a no-op intersection)'); // N under mate-polarity is engaged the other way round: bring the harm BACK, console-ward only. const nBack = M.reads.N.prefer(P, legal, ctxMate); assert.ok(nBack.size > 0 && nBack.has(flip.k), 'N under mate-polarity steers toward the console'); assert.ok(!nBack.has('stay'), 'N under mate-polarity does not stand still while he burns'); // and N's own dead state: the mate safe deep in the CORE. dyn.storm.polarity = 'me'; st.pos[1] = { x: c, y: c + 1 }; const ctxSafe = M.reads.ctx(P); assert.strictEqual(ctxSafe.mateExposed, false, 'a mate in the CORE is never exposed'); assert.strictEqual(M.reads.N.engaged(P, ctxSafe), false, 'N must NOT engage with the mate safe'); assert.deepStrictEqual(M.reads.N.prefer(P, legal, ctxSafe), allKeys, 'N must hand back the FULL legal set when it is not engaged (a no-op intersection)'); // a DOWNED mate is not "exposed" either — care cannot be owed to a plan already frozen. st.pos[1] = { x: mk % n, y: (mk - mk % n) / n }; dyn.storm.mateDown = true; assert.strictEqual(M.reads.ctx(P).mateExposed, false, 'a downed mate no longer reads as exposed'); assert.deepStrictEqual(M.reads.N.prefer(P, legal, M.reads.ctx(P)), allKeys, 'N is a no-op once the mate is down'); } }); /* ---- y23 STORM — THE GRACE PERIOD (owner ruling, 2026-07-20): the flip is RETRACTABLE ---- */ test('Y23-STORM-GRACE: the mate survives K-1 consecutive harm beats and drops on the K-th; flipping back to me RESETS the count; stepping off the stormed zone resets it too', () => { const M = E.PARK_FIELD_MECHS.storm; const K = E.PARK_STORM_GRACE; assert.ok(Number.isInteger(K) && K >= 2, 'PARK_STORM_GRACE must be an integer >= 2 — at K=1 there is no grace and the flip is a one-way door again'); // A fresh scene builder: ring 0 stormed, walker parked in the CORE (he is never the subject here), // mate standing on a stormed cell, polarity already pointed at him. const scene = () => { const st = E._parkStormBuild({ seed: 42 }); const P = E.parkStart(st); const sm = st.park.storm, dyn = st.park.dyn, n = st.N; dyn.beat = sm.every; // gone = floor(6/6) = 1 => ring 0 stormed const r0 = sm.rings[0][0]; const stormedCell = { x: r0 % n, y: (r0 - r0 % n) / n }; st.pos[0] = { x: st.park.retire.x, y: st.park.retire.y }; st.pos[1] = { x: stormedCell.x, y: stormedCell.y }; dyn.storm.polarity = 'mate'; return { st, P, dyn, sm, stormedCell }; }; // (1) K-1 beats of harm are SURVIVABLE; the K-th is not. { const { P, dyn } = scene(); for (let i = 1; i < K; i++) { M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, false, 'the mate must still be standing after ' + i + ' of ' + K + ' consecutive harm beats'); assert.strictEqual(dyn.storm.mateHeat, i, 'mateHeat must count consecutive harm beats (unit: beats)'); } M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateHeat, K, 'mateHeat reaches K on the K-th consecutive harm beat'); assert.strictEqual(dyn.storm.mateDown, true, 'the mate goes down on the K-th consecutive harm beat'); } // (2) FLIPPING BACK TO 'me' RESETS — the confession is retractable. Burn K-1 beats, retract, and // the counter must return to 0; re-flipping then buys the mate a FULL fresh grace window. { const { P, dyn } = scene(); for (let i = 1; i < K; i++) M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateHeat, K - 1); dyn.storm.polarity = 'me'; // the walker takes the harm back M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateHeat, 0, 'flipping back to me RESETS the consecutive count'); assert.strictEqual(dyn.storm.mateDown, false, 'retracting before the K-th beat saves the mate'); dyn.storm.polarity = 'mate'; // confess again: a FULL window, not a remainder for (let i = 1; i < K; i++) { M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, false, 'after a retraction the grace window restarts from zero, not from where it left off'); } M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, true, 'the second, uninterrupted window still ends in a mate-down'); } // (3) STEPPING OFF THE STORMED ZONE RESETS TOO — the counter measures beats of HARM ACTUALLY LANDING, // not beats since the flip. A mate who walked into the CORE is taking nothing and must not be // billed for it (see the module comment for why the reset is one predicate, not two). { const { st, P, dyn, stormedCell } = scene(); for (let i = 1; i < K; i++) M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateHeat, K - 1); st.pos[1] = { x: st.park.retire.x, y: st.park.retire.y }; // into the CORE: never stormed M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateHeat, 0, 'leaving the stormed zone RESETS the consecutive count'); assert.strictEqual(dyn.storm.mateDown, false, 'a mate off the zone takes no harm at all'); st.pos[1] = { x: stormedCell.x, y: stormedCell.y }; // back into it for (let i = 1; i < K; i++) { M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, false, 'the window restarts from zero after stepping back in'); } M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, true); } // (4) Once DOWN, he stays down and the counter stops mattering — the freeze is permanent for the run. { const { P, dyn } = scene(); for (let i = 0; i < K; i++) M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, true); dyn.storm.polarity = 'me'; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.storm.mateDown, true, 'mate-down is permanent — retracting AFTER the fact does not heal him'); } }); /* ---- y23 STORM — C.prefer's SECOND dead state, and the geometry that makes it harmless ---- */ test('Y23-STORM-C-NOOP: 4-adjacent cells differ by at most one ring, and THAT is why C.prefer is a no-op in the me-polarity/high-margin state its guard does not cover', () => { const M = E.PARK_FIELD_MECHS.storm; // C.engaged is a CONJUNCTION (me-polarity AND meMargin <= MARGIN) but C.prefer's early return // guards only the polarity half, so `polarity==='me' && meMargin > MARGIN` is a state where prefer // runs while its own engaged() calls it dead. The seam intersects it anyway (_parkReads), so if it // narrowed there it would silently make the safety mind partial. It does not — by ring geometry, // not by the guard. Both halves of that argument are pinned here, because a future change to // PARK_STORM_MARGIN or to _parkStormRing would otherwise break it without a single bar going red. for (const seed of [1, 7, 42, 4149]) { const st = E._parkStormBuild({ seed }); const P = E.parkStart(st); const n = st.N, sm = st.park.storm, dyn = st.park.dyn; // (A) THE LEMMA, stated directly on the board: any two 4-adjacent playable cells are at most one // ring apart. This is what bounds every candidate's margin below by (walker's margin - 1). for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { const a = sm.ringOf[y * n + x]; if (a < 0) continue; for (const d of [{ x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 }]) { const bx = x + d.x, by = y + d.y; if (bx < 1 || by < 1 || bx > n - 2 || by > n - 2) continue; const b = sm.ringOf[by * n + bx]; if (b < 0) continue; assert.ok(Math.abs(a - b) <= 1, 'ring adjacency lemma broken at (' + x + ',' + y + ')->(' + bx + ',' + by + '): rings ' + a + ' vs ' + b); } } // (B) THE CONSEQUENCE, by enumeration over every playable walker cell x every clock position. let visited = 0; for (let idx = 0; idx <= sm.rings.length; idx++) { dyn.beat = sm.every * idx; for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 1; x++) { if (sm.ringOf[y * n + x] < 0) continue; st.pos[0] = { x, y }; const ctx = M.reads.ctx(P); assert.strictEqual(ctx.polarity, 'me', 'the board opens under me-polarity'); if (!(ctx.meMargin > E.PARK_STORM_MARGIN)) continue; // not the state under test assert.strictEqual(M.reads.C.engaged(P, ctx), false, 'C must read itself dead above the bar'); const legal = E._parkLegal(P); assert.deepStrictEqual(M.reads.C.prefer(P, legal, ctx), new Set(legal.map(cd => cd.k)), 'C.prefer must be a NO-OP above the margin bar (seed ' + seed + ', cell ' + x + ',' + y + ', nextIdx ' + idx + ')'); visited++; } } // the enumeration must actually REACH the state — an empty sweep would make all of (B) vacuous. assert.ok(visited > 50, 'the me-polarity/high-margin state must be reached many times, got ' + visited); } }); /* ---- y23 STORM — THE FILTER LAYER itself (previously asserted only as `typeof admits === function`) ---- */ test('Y23-STORM-FILTER: the pair table, both signature clauses, playout purity, and the whys tally — including that `cn` counts only C-N posed in a run that ACTUALLY TOOK THE FLIP', () => { // (1) THE PAIR TABLE. The `cn` counter used to key off `pair[0] === 'C'` alone, which was correct // only by the accident that exactly one pair leads with C. Pin the table's shape so a future pair // cannot silently change what `nocn` means. const pairs = E.PARK_STORM_PAIRS; assert.ok(Array.isArray(pairs) && pairs.length === 3, 'storm poses exactly three attitude pairs'); for (const p of pairs) { assert.ok(Array.isArray(p) && p.length === 2 && p[0] !== p[1], 'each pair is two DISTINCT attitudes'); for (const a of p) assert.ok(['G', 'C', 'N'].includes(a), 'unknown attitude key in PARK_STORM_PAIRS: ' + a); } assert.ok(pairs.some(p => p[0] === 'C' && p[1] === 'N'), 'the C-N pair is the reason this module exists'); // (2) THE SIGNATURE, both clauses, on synthetic playouts aligned with PARK_PERSONAS. Driving it // with hand-built states is the only way to exercise the FALSE branches: measured on real boards // this filter currently rejects nothing (0/40 seeds), so a real-board-only test would be vacuous. const top = E.PARK_PERSONAS.map(p => p[0]); const mk = (score, ticks) => ({ st: { score: { 0: score }, park: { dyn: { storm: { ticksTaken: ticks } } } } }); const build = (goalScore, safetyScore, safetyTicks) => top.map(t => t === 'goal' ? mk(goalScore, 0) : t === 'safety' ? mk(safetyScore, safetyTicks) : mk(0, 0)); assert.strictEqual(E._parkStormSignature(build(10, 4, 0)), true, 'signature holds: goal-top out-banks safety-top and safety-top took no storm tick'); assert.strictEqual(E._parkStormSignature(build(10, 4, 1)), false, 'clause 2 must reject: safety-top standing in the field even once (unit: ticks)'); assert.strictEqual(E._parkStormSignature(build(4, 4, 0)), false, 'clause 1 is STRICT: an equal bank is not a separation'); assert.strictEqual(E._parkStormSignature(build(3, 4, 0)), false, 'clause 1 must reject: safety-top out-banking goal-top'); // (3) THE PLAYOUT is faithful, terminating, and built on a FRESH board every call (every consumer // rebuilds because parkStep mutates what it is handed). const persona = E.PARK_PERSONAS[0]; const a = E._parkStormPlay(E._parkStormCell(9), persona); const b = E._parkStormPlay(E._parkStormCell(9), persona); assert.strictEqual(a.over, true, 'a playout runs to termination'); assert.ok(Array.isArray(a.moves) && a.moves.length > 0, 'a playout records its move log'); assert.notStrictEqual(a.st, b.st, 'each playout gets its own board — no shared mutable state'); assert.deepStrictEqual(a.moves, b.moves, 'same cell + same persona => same faithful walk'); // (4) THE TALLY. Every branch of _parkStormAdmissible early-returns, so ONE key moves per call — // that is exactly why the header calls it a FIRST-REASON histogram and not a cause histogram. const delta = (seed) => { const before = E.parkStormWhys(); const ok = E._parkStormAdmissible(E._parkStormCell(seed)); const after = E.parkStormWhys(); const moved = Object.keys(after).filter(k => after[k] !== before[k]); assert.strictEqual(moved.length, 1, 'seed ' + seed + ': exactly one whys key must move per call, got ' + JSON.stringify(moved)); assert.strictEqual(after[moved[0]] - before[moved[0]], 1, 'the tally moves by exactly one'); return { ok, why: moved[0] }; }; assert.notStrictEqual(E.parkStormWhys(), E._PARK_STORM_WHYS, 'parkStormWhys() must hand back a COPY'); // 4a. an ADMITTED cell (measured: seeds 1..40, raw cells, every=6 — seed 9 passes) const good = delta(9); assert.strictEqual(good.ok, true, 'seed 9 must be admitted'); assert.strictEqual(good.why, 'complete', 'an admitted cell is tallied under `complete`'); // 4b. a cell whose faithful walk DIES to its own field — a generator bug, not a hard board. const dead = delta(1); assert.strictEqual(dead.ok, false); assert.strictEqual(dead.why, 'dead', 'seed 1 has a non-complete faithful playout and must tally as `dead`'); // 4c. THE FINDING-2 SEMANTICS, and the sharpest assertion in this gate. Seed 2 poses C-N in one of // its six faithful runs but takes ZERO polarity flips in all six. A C-N divergence that no flip // caused is NOT the console scene, so it must NOT satisfy the `cn` bar — the cell must be rejected // under `nocn`. If `cn` were ever un-gated back to "C-N from any cause", seed 2 would be admitted // and this assertion is what would catch it. let flips2 = 0, cnRuns2 = 0; for (const p of E.PARK_PERSONAS) { const P = E._parkStormPlay(E._parkStormCell(2), p); flips2 += P.st.park.dyn.storm.flips.length; assert.strictEqual(P.reason, 'complete', 'seed 2 must get past the `dead` bar to reach the cn bar'); if (E.parkPairExpressed(E._parkStormBuild(E._parkStormCell(2)), P.moves, ['C', 'N']) > 0) cnRuns2++; } assert.strictEqual(flips2, 0, 'the premise: seed 2 takes no polarity flip in any of its six faithful runs'); assert.ok(cnRuns2 > 0, 'the premise: seed 2 DOES pose C-N — from some cause other than the flip'); const nocn = delta(2); assert.strictEqual(nocn.ok, false, 'C-N without a flip is not the console scene: seed 2 must be REJECTED'); assert.strictEqual(nocn.why, 'nocn', 'and rejected specifically at the cn bar'); }); /* ---- Y23-SHIP-GATE (Task 6, plan 2026-07-20) — the test-side form of derive-never-assert: the * slot's ship flag must always EQUAL the measured pairing (CAMP.PARK_Y23_SHIPPABLE), never diverge * from it in either direction. Unlike Y22-SHIP-MEASURED (which pins the value to `true` because y22 * is a shipped promotion), this gate pins nothing about the VALUE — y23 is still a preview * (ship:false, open:true) as of this task, and PARK_Y23_SHIPPABLE is whatever the pairing sweep * says. What the gate enforces is that the slot literal and the measurement never drift apart — * the same discipline y22's Y22-SHIP-MEASURED enforces, but without hand-asserting a truth value. * NOTE for whoever writes the eventual promotion commit (Minor, noted in review 2026-07-20): this * gate does NOT pin `open`, unlike Y22-SHIP-MEASURED which asserts `!y22.open` above (a measured/ * shipped crossing must not also carry a preview mark). A future promotion of y23 could land * `ship: true, open: true` and this gate would stay green. Follow the y22 pattern: the promotion * commit should add its own `!y23.open` assertion (or fold one into this gate) at the same time it * flips ship:true — do not just flip the slot literal and leave `open` dangling. Not changed here; * this gate matches its current spec (y23 is still a preview) and should not be touched ahead of a * real promotion measurement. */ test('Y23-SHIP-GATE: slot.ship agrees with the measured pairing (derive-never-assert)', () => { const y23 = CAMP.PARK_CROSSINGS.find(c => c.id === 'y23'); assert.ok(y23, 'y23 slot must exist in PARK_CROSSINGS'); assert.strictEqual(y23.ship, CAMP.PARK_Y23_SHIPPABLE, `y23.ship=${y23.ship} disagrees with PARK_Y23_SHIPPABLE=${CAMP.PARK_Y23_SHIPPABLE} — the picker flag ` + 'and the pairing measurement drifted apart. Flip the flag only WITH the measurement, never ahead of it.'); }); /* ---- Y23-STORM-EXHAUSTED (whole-branch review 2026-07-20, Important): THE CLOCK THAT HAS RUN OUT. * `_parkStormNextIdx` CLAMPS to `rings.length` (5), and `_parkStormNext` correctly returns [] there — * the schedule is over, rings 0..4 have all gone, and the CORE (ringOf 5..6) is what it was always * advertised to be: ground that can never storm. But `_parkStormMargin` used to keep computing * `ringOf - next` against that CLAMPED index, so a walker standing in the core read a margin of 1 or * 0 against a bite that does not exist. C.engaged went TRUE and mateExposed went TRUE on a board with * nothing left to fear. * WHY THIS IS A GATE AND NOT A NOTE. It contradicts two design laws this module already wrote down: * C.engaged's own comment ("a safety mind that keeps its distance from a field that cannot touch it * is a superstition, not a caution") and mateExposed's `!mateDown` clause, which exists precisely so * that N is never pinned open for the rest of a run — the clamped margin re-created that failure by * another route. * LATENT, NOT LIVE, and said as a number so the next owner does not over-read this gate: across seeds * 1..40 x 6 personas = 240 faithful playouts (raw cells, every=6, GRACE=2) the MAX observed nextIdx is * 4 — no faithful run reaches beat 30, so this state occurs in 0 of 240 runs and no recorded * measurement on this branch is affected. It is a gate because PARK_STORM_EVERY is labelled in its own * declaration as this module's first tuning lever, and LOWERING it is exactly what brings nextIdx 5 * into reach. This pins the semantics BEFORE that edit, not after it. */ test('Y23-STORM-EXHAUSTED: once the ring schedule has run out, the core is not dangerous — margin is +Infinity, C does not engage, the mate does not read as exposed', () => { const M = E.PARK_FIELD_MECHS.storm; for (const seed of [9, 1, 42]) { const st = E._parkStormBuild({ seed }); const P = E.parkStart(st); const n = st.N, c = (n - 1) >> 1, sm = st.park.storm, dyn = st.park.dyn; // PAST THE END OF THE SCHEDULE: every one of rings 0..4 has gone and then some. dyn.beat = sm.every * sm.rings.length + 3; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(E._parkStormNextIdx(st), sm.rings.length, 'the premise: the clock is exhausted'); assert.strictEqual(E._parkStormNext(st).length, 0, 'the premise: there is no next ring to preview'); // Walker on the core centre (ring 5+), companion on a core cell beside him. Neither is in the zone. st.pos[0] = { x: c, y: c }; st.pos[1] = { x: c, y: c + 1 }; const meKey = E._parkKey(st, st.pos[0]), mateKey = E._parkKey(st, st.pos[1]); assert.ok(sm.coreKeys.has(meKey) && sm.coreKeys.has(mateKey), 'the premise: both stand on CORE'); assert.strictEqual(dyn.storm.zone.has(meKey), false, 'the premise: the core never storms'); assert.strictEqual(dyn.storm.zone.has(mateKey), false, 'the premise: the core never storms'); assert.strictEqual(dyn.storm.mateDown, false, 'the premise: the companion is still standing'); // THE CLAIM. No next bite exists, so there is no daylight to measure and every cell is clear. assert.strictEqual(E._parkStormMargin(st, meKey, sm.rings.length), Infinity, 'an exhausted schedule must yield +Infinity margin, not a distance to a bite that cannot come'); const ctx = M.reads.ctx(P); assert.strictEqual(ctx.polarity, 'me', 'the premise: the board opens under me-polarity'); assert.strictEqual(ctx.meMargin, Infinity, 'ctx must carry the exhausted margin through'); assert.strictEqual(M.reads.C.engaged(P, ctx), false, 'C must NOT engage against a field that cannot touch it — that is superstition, not caution'); assert.strictEqual(ctx.mateExposed, false, 'a companion on core ground under an exhausted schedule is not exposed'); assert.strictEqual(M.reads.N.engaged(P, ctx), false, 'N must NOT engage: nobody is exposed'); // and the seam trap still holds: both minds hand back the FULL legal set here, never {}. const legal = E._parkLegal(P); const allKeys = new Set(legal.map(cd => cd.k)); assert.deepStrictEqual(M.reads.C.prefer(P, legal, ctx), allKeys, 'C must be a no-op intersection once the schedule is exhausted'); assert.deepStrictEqual(M.reads.N.prefer(P, legal, ctx), allKeys, 'N must be a no-op intersection once the schedule is exhausted'); // A WALL IS STILL NOT GROUND — the exhausted case must not promote off-board keys to safe. assert.strictEqual(E._parkStormMargin(st, 0, sm.rings.length), -Infinity, 'the wall clause must win over the exhausted clause'); } }); /* ---- GATE: BOMB2-PEN-ROUTES — the y22 fork's pen geometry, asserted as MEASURED path facts ---- * The pen's contract gem stands on DRY ground with exactly two player approaches: a grass route * (>= 1 deep entry, no bomb) and a blast route (0 deep entries after the cage wall opens). The * companion's own domain must NEVER reach the stand — before or after the blast — so his plan * stays `stuck` (the HOLD that keeps steering reads clean). Units: deep entries per path; seeds * 1..8 (the sweep bar itself is PARK_BOMB2_SHIPPABLE / PARK_Y22_SHIPPABLE, not this gate). */ /* ==== Y25-GATES-BEGIN ==== */ // Y25-BULL-READS: THE CRUX. During a mate-targeted telegraph, the two facets are DISJOINT by // construction — C (caution) forbids the whole charge lane, N (care) prescribes stepping ONTO the // interpose segment between the bull and the companion so the walker takes the hit instead. Both // self-gate: with no aim alive, each prefer() is the WHOLE legal set (an inert intersection), never // {}. Mirrors Y8-LOG-READS's facet-level call shape (M.reads.C.prefer(P, legal, ctx)). // Y25-BULL-CHARGE-CLEAR (Task-2 coverage, confirmed by hand at review, now gated): with an aim set // but NO body on the stored lane, the charge runs the whole lane and stuns against the wall/fence at // the far end — no heart lost, no companion downed. // Y25-BULL-CRAWL (Task-2 coverage, confirmed by hand at review, now gated): once the companion is // down, the transplanted y3 crawl fires on its own cadence (PARK_BULL_CRAWL_EVERY) — it advances one // cell only on a cadence beat, is inert off-cadence, and self-rescues (`rescued` flips) at the // PARK_BULL_CRAWL_CAP ceiling or a wall/edge, whichever comes first. Driven by direct tick() calls // with dyn.beat advanced in explicit cadence steps so the crawl is measured in isolation (parkStep // would also run the companion planner). The bull is parked (B.stunned high; crawlTick runs BEFORE // the stun check) and the walker sits clear of the eastward crawl corridor. // Y25-BULL-PATH-SIG: THE PATH-TESTIMONY SEPARATION (Option B, brief task-3c). The module's own read // law — "cost is NOT read; the mind is testified by the PATH, not the price" — forbids the old signature's // demand for a COMPLETED body-block. The redefined _parkBullSignature reads the walker's CLOSEST APPROACH // to the interpose segment over the mate-aim scene (_bullMinSegDist), and SEPARATES the three minds on // that path: CARE (both) approaches strictly nearer than BOTH the safety baseline (max over safety) AND // goal's own closest approach; GOAL does NOT approach nearer than safety; SAFETY stays off the lane. The // approach bound is DATA-DRIVEN (the other minds' own paths), so a persona that merely wanders can never // satisfy it — proven below by the FLEE / TIE / GOAL-APPROACHES negatives. Constructed playout stubs // (aligned to PARK_PERSONAS) so the separation is exact regardless of any single board's geometry. /* ---- Y25-SHIP-GATE (Task 6, plan 2026-07-20) — derive-never-assert in test form: the slot literal * and the measured PAIRING (CAMP.PARK_Y25_SHIPPABLE) must never drift apart, in either direction. * Like Y23-SHIP-GATE and unlike Y22-SHIP-MEASURED, it pins no truth VALUE — y25 is a preview and the * constant is whatever the sweep says. * MEASURED 2026-07-21 (24 base seeds x 6 personas, slide demo -> bull play, calibrated at * PARK_CAL_TURNS): pairing 0/24. Faithful order recovery 48/144 persona-runs full, 96 undetermined, * 0 wrong — the walk simply does not determine the whole order on this board, which is precisely * what an honest preview looks like (the readout says "읽을 수 없음" rather than inventing one). * The mimic control was measured SEPARATELY, because this predicate short-circuits on the faithful * clause and therefore never reaches its own mimic loop — reading "0 leaks" off a run that failed * earlier would be vacuous. Independently: the ice-demo surface learner EXPRESSES the G-C pair on * 144/144 probes and RECOVERS it on 59 (41%). Controls run the same way: y23 (peer preview) leaks * 72/144 (50%); y22 (shipped) expresses 0/144, which is what a defended crossing looks like. So * y25's leak is unremarkable for its stage and slightly better than the peer that was accepted — * the narrowed anti-mimic axis (home fixed to the west wall, see the slot header) does NOT make this * crossing worse than its cohort. That is evidence against a builder rebuild, not proof the bearing * is irrelevant; the promotion measurement is where it would have to be settled. * FOR THE PROMOTION COMMIT: this gate does not pin `open` (the same gap y23's gate notes). Whoever * flips ship:true must add `!y25.open` at the same time — a measured crossing must not also carry a * preview mark — rather than leaving `open` dangling. */ /* ==== Y25-GATES-END ==== */ /* ==== Y26-GATES-BEGIN ==== */ /* ---- Y26-LEDGE-BUILD (Task 1, plan 2026-07-20-y26-ledge-commit.md) — the board's GEOMETRY. * The module's whole identity is a one-way drop, and a one-way drop is a GEOMETRY claim before it is * a rules claim: two terraces, a skirt band between them that no traveller may route through, and a * ramp gap that is always open (the soft-lock law — an irreversible move must never be the only way * back). This gate pins seed-purity and that geometry; the one-way behaviour itself is Y26-LEDGE-MOVES. */ test('LEDGE-BUILD: seed-pure; upper/lower/skirt partition the interior with a ramp gap; chain gems lower, contract gem upper, companion lower; crate upper', () => { for (const seed of [1, 7, 42, 4149]) { const a = E._parkLedgeBuild({ seed }), b = E._parkLedgeBuild({ seed }); assert.strictEqual(JSON.stringify([...a.park.ledge.skirt].sort((p, q) => p - q)), JSON.stringify([...b.park.ledge.skirt].sort((p, q) => p - q)), 'seed-pure skirt'); const st = a, L = st.park.ledge, nn = st.N, K = (p) => p.y * nn + p.x; for (const kk of L.skirt) assert.ok(!L.upper.has(kk) && !L.lower.has(kk), 'partition: skirt is neither terrace'); assert.ok(L.rampKeys.size >= 2, 'ramp gap exists — the always-open alternative route (soft-lock law)'); for (const kk of L.rampKeys) assert.ok(!L.skirt.has(kk), 'the ramp is a GAP in the skirt, not a skirt cell'); assert.ok(L.upper.has(K(st.park.spawn)), 'walker spawns upper'); assert.ok(L.lower.has(K(st.park.companionSpawn)), 'companion starts lower'); for (const kk of L.skirt) assert.ok(L.rimOf.has(kk), 'every skirt cell knows its rim'); for (const [sk, rim] of L.rimOf) assert.ok(L.upper.has(rim) && L.skirt.has(sk), 'rimOf maps skirt -> upper rim'); assert.ok(L.upper.has(st.park.ledge.crateSpawn), 'crate starts upper'); // THE CHAIN IS A ROUND TRIP, and that is the whole economy of the crate. park.chain is ORDERED // (P.dest indexes it, engine.js:7238), so the first two gems BELOW and the last one ABOVE force // the walker down the cliff and then back up it. Down is free (the drop); up is not (the long // ramp, or a stair he built himself) — which is what puts his own use of the one crate in // competition with the companion's, and that competition IS the cell. const chainRows = st.park.chain.map(ci => L.lower.has(K(st.park.clusters[ci]))); assert.deepStrictEqual(chainRows, [true, true, false], 'chain order: lower, lower, then UPPER (the return trip)'); assert.ok(L.upper.has(K(st.park.clusters[st.park.chain[2]])), 'the last chain gem is upper'); for (const c of st.park.contracts) assert.ok(L.upper.has(K(st.park.clusters[c.gem])), 'contract gem upper'); assert.ok(L.lower.has(K(st.park.retire)), 'companion retires lower'); // THE SKIRT IS DEEP (the G/C separator — see the module header): fast prices it 1, safe 24. for (const kk of L.skirt) assert.ok(st.park.deep.has(kk), 'skirt cells are deep — the shipped G/C separator'); // the walker does not start on the brink: a drop inside the first two turns would be a coin-flip, // not a decision (the calibration guard). assert.ok(!L.rimOf.has(K(st.park.spawn)) || true, 'spawn guard'); let onRim = false; for (const [, rim] of L.rimOf) if (rim === K(st.park.spawn)) onRim = true; assert.ok(!onRim, 'the walker does not spawn ON a rim cell'); } }); /* ---- Y26-LEDGE-MOVES (Task 2) — the ONE-WAY EDGE, and the crate that unmakes it. * Four claims, each one a thing that was easy to get wrong: * (1) the drop opens from the rim and ONLY from the rim — that is the whole cell; * (2) the 'route' domain sees exactly the one cell the walker could actually drop into and no * other, because a route-masked cell can never be chosen (engine.js:7941-7947) and an * un-masked band would let the metric plan a climb that is not legal; * (3) the drop is a REAL move that costs a heart, and the heart comes from the shipped deep-entry * charge rather than from a second one this module adds; * (4) pushing the crate over the rim is an ACTION — the crate falls, the cell becomes a stair open * to BOTH travellers, and the walker is put back where he pushed from. */ test('LEDGE-MOVES: drop opens only from the rim and only in the walker domain; route sees just that one cell; the drop is a real move that spends the deep charge; the crate becomes a two-way stair', () => { const st = E._parkLedgeBuild({ seed: 42 }); const P = E.parkStart(st); const M = E.PARK_FIELD_MECHS.ledge, L = st.park.ledge, nn = st.N; const dyn = st.park.dyn; const at = (k) => ({ x: k % nn, y: (k / nn) | 0 }); const sk = [...L.skirt][3], rim = L.rimOf.get(sk); // (1) FROM THE RIM: the cell under his feet opens. st.pos[0] = at(rim); assert.strictEqual(M.legalAdd(P, sk, 'me'), true, 'the drop opens from the rim'); assert.strictEqual(M.legalAdd(P, sk, 'mate'), false, 'the companion never drops'); assert.strictEqual(M.legalMask(P, sk, 'mate'), true, 'the band is a wall to the companion'); // (2) the route domain sees THIS cell (so the drop can win an argmin) and no other skirt cell. assert.strictEqual(M.legalMask(P, sk, 'route'), false, 'the reachable drop carries route'); let seen = 0; for (const kk of L.skirt) if (!M.legalMask(P, kk, 'route')) seen++; assert.strictEqual(seen, 1, 'exactly one skirt cell is route-visible — the one under his feet'); // FROM BELOW: nothing opens. This is the one-way edge. const below = sk + nn; assert.ok(L.lower.has(below), 'the cell below the skirt is the lower terrace'); st.pos[0] = at(below); assert.strictEqual(M.legalAdd(P, sk, 'me'), false, 'no climb from below'); assert.strictEqual(M.legalMask(P, sk, 'me'), true, 'and the band is masked from below'); for (const kk of L.skirt) assert.strictEqual(M.legalMask(P, kk, 'route'), true, 'no route through the band from below'); // (3) THE DROP IS A REAL MOVE. Drive it through parkStep so the engine's own charge fires. const st2 = E._parkLedgeBuild({ seed: 42 }), P2 = E.parkStart(st2); const L2 = st2.park.ledge, sk2 = [...L2.skirt][3], rim2 = L2.rimOf.get(sk2); st2.pos[0] = at(rim2); const h0 = P2.hearts, de0 = P2.deepEntries; E.parkStep(P2, 'D'); assert.strictEqual(E._parkKey(st2, st2.pos[0]), sk2, 'he really stands on the skirt (not an action-move)'); assert.strictEqual(P2.hearts, h0 - 1, 'the drop costs a heart'); assert.strictEqual(P2.deepEntries, de0 + 1, 'and it is the SHIPPED deep charge, not a second one'); assert.strictEqual(st2.park.dyn.ledge.hops.length, 1, 'the drop is confessed'); assert.strictEqual(st2.park.dyn.ledge.hopHurts, 1, 'hopHurts observes the charge'); assert.strictEqual(st2.park.dyn.ledge.hops[0].key, sk2, 'the confession names the cell'); // (4) THE CRATE. Stand north of it and push south twice: rim row, then over. const st3 = E._parkLedgeBuild({ seed: 42 }), P3 = E.parkStart(st3); const L3 = st3.park.ledge, d3 = st3.park.dyn; const crate0 = d3.ledge.crate, cx = crate0 % nn, cy = (crate0 / nn) | 0; st3.pos[0] = { x: cx, y: cy - 1 }; const M3 = E.PARK_FIELD_MECHS.ledge; assert.strictEqual(M3.legalMask(P3, crate0, 'me'), true, 'you cannot stand on a crate'); assert.strictEqual(M3.legalMask(P3, crate0, 'route'), false, 'but the crate carries route (else the push can never be chosen)'); assert.strictEqual(M3.legalAdd(P3, crate0, 'me'), true, 'walking into the crate is the push'); E.parkStep(P3, 'D'); assert.strictEqual(E._parkKey(st3, st3.pos[0]), crate0 - nn, 'ACTION, not a step: he is put back'); assert.strictEqual(d3.ledge.crate, crate0 + nn, 'the crate moved one cell'); // second push: over the rim. st3.pos[0] = { x: cx, y: cy }; const fellTo = crate0 + 2 * nn; assert.ok(L3.skirt.has(fellTo), 'the crate is now on the rim, facing the band'); assert.strictEqual(M3.legalAdd(P3, crate0 + nn, 'me'), true, 'pushing it over the rim is legal'); E.parkStep(P3, 'D'); assert.strictEqual(d3.ledge.crate, null, 'the crate is spent'); assert.ok(d3.ledge.stairs.has(fellTo), 'and it became a stair'); assert.strictEqual(E._parkKey(st3, st3.pos[0]), crate0, 'he is put back again'); // THE STAIR IS TWO-WAY, for both bodies, in every domain. assert.strictEqual(M3.legalMask(P3, fellTo, 'me'), false, 'a stair is walkable'); assert.strictEqual(M3.legalMask(P3, fellTo, 'mate'), false, 'by the companion too'); assert.strictEqual(M3.legalMask(P3, fellTo, 'route'), false, 'and it carries route permanently'); assert.strictEqual(M3.legalAdd(P3, fellTo, 'mate'), true, 'legalAdd overrides his deep taboo (engine.js:7608)'); st3.pos[0] = at(fellTo + nn); assert.strictEqual(M3.legalMask(P3, fellTo, 'me'), false, 'and he may CLIMB it from below — the crate bought reversibility'); }); /* ---- Y26-LEDGE-READS (Task 3) — the ONE facet, and the two ways a facet gets written wrong. * There is deliberately no G facet and no C facet on this module: the band is park.deep, so the * shipped fast/safe metrics already disagree about the cliff (1 against 24) and a module that taught * G to drop would be re-deriving the engine. What IS here is the care read — and it must self-gate * (cold => the WHOLE legal set, never {}, engine.js:7369-7377) and must only ever NARROW. */ test('LEDGE-READS: the care facet self-gates, returns the whole legal set when cold, never {}, and only narrows', () => { const st = E._parkLedgeBuild({ seed: 1 }); const P = E.parkStart(st); const M = E.PARK_FIELD_MECHS.ledge, L = st.park.ledge, D = st.park.dyn.ledge; assert.ok(!M.reads.G && !M.reads.C, 'no G or C facet — the deep marking already separates them'); // LIVE: his gem stands, his column has no stair, the crate is still a crate. let ctx = M.reads.ctx(P); assert.strictEqual(ctx.live, true, 'the care read is live at the start'); assert.ok(ctx.plan && ctx.plan.stand != null, 'and it has a next push to plan for'); assert.strictEqual(M.reads.N.engaged(P, ctx), true, 'engaged while live'); const legal = E._parkLegal(P); const pref = M.reads.N.prefer(P, legal, ctx); assert.ok(pref.size > 0, 'NEVER {} — an empty prefer deletes the mind, it does not veto'); const legalKeys = new Set(legal.map(c => c.k)); for (const k of pref) assert.ok(legalKeys.has(k), 'prefer only ever NARROWS the legal set'); assert.ok(pref.size < legalKeys.size, 'and here it really does narrow (it is not inert)'); // COLD, three ways — each of the three live-clauses, killed one at a time. const coldCheck = (why) => { const c = M.reads.ctx(P); assert.strictEqual(c.live, false, why + ': goes cold'); assert.strictEqual(M.reads.N.engaged(P, c), false, why + ': disengaged'); const all = M.reads.N.prefer(P, legal, c); assert.strictEqual(all.size, legalKeys.size, why + ': cold returns the WHOLE legal set (inert intersection)'); }; D.stairs.add(L.mateStair); coldCheck('his stair exists'); D.stairs.delete(L.mateStair); const keep = D.crate; D.crate = null; coldCheck('the crate is spent'); D.crate = keep; st.tokens[st.park.contracts[0].gem].alive = false; coldCheck('he has already banked his gem'); }); /* ---- Y26-LEDGE-SIG (Task 3) — the field signature, and PROOF IT CAN FAIL. Every mind is read off a * DIFFERENT observable here, because on this board they do not differ in score at all: all six * personas bank the same chain (measured — see the admission note below). A signature leaning on * score would be reading noise, so: safety by abstention (no drop, no heart, no errand), goal by the * drop plus a strictly faster time than safety's own worst, care by BOTH care personas building the * companion's stair. The eight mutations below are the whole point of this gate — a bar nothing can * trip is not a bar. */ test('LEDGE-SIG: safety abstains, goal drops and is faster, BOTH care personas build his stair — non-vacuously', () => { const top = E.PARK_PERSONAS.map(p => p[0]); assert.deepStrictEqual(top, ['goal', 'goal', 'safety', 'safety', 'care', 'care'], 'persona tops as assumed'); const stub = (hops, hurts, stair, turns) => ({ _ledgeHops: hops, _ledgeHurts: hurts, _ledgeMateStair: stair, turns }); const base = () => [ stub(1, 1, false, 47), stub(1, 1, false, 40), // goal-top: takes the cliff, home fast, no errand stub(0, 0, false, 58), stub(0, 0, false, 63), // safety-top: abstains entirely (baseline worst 63) stub(0, 2, true, 37), stub(0, 0, true, 55), // care-top: BOTH build his stair ]; assert.ok(E._parkLedgeSignature(base()), 'the separating scene must PASS'); const bites = (name, mut) => { const p = base(); mut(p); assert.ok(!E._parkLedgeSignature(p), name); }; bites('safety that drops must FAIL', p => { p[2]._ledgeHops = 1; }); bites('safety that loses a heart to the band must FAIL', p => { p[3]._ledgeHurts = 1; }); bites('safety that runs the brink errand must FAIL', p => { p[2]._ledgeMateStair = true; }); bites('goal that refuses the cliff must FAIL', p => { p[0]._ledgeHops = 0; }); bites('goal that builds his stair must FAIL — goal does not run errands', p => { p[1]._ledgeMateStair = true; }); bites('goal no faster than safety must FAIL — the cliff must really be the short way', p => { p[0].turns = 999; }); bites('only ONE care persona building must FAIL — the read is required of BOTH', p => { p[4]._ledgeMateStair = false; }); bites('NO care persona building must FAIL', p => { p[4]._ledgeMateStair = false; p[5]._ledgeMateStair = false; }); // and the non-vacuity guard itself: with no safety playout there is no baseline to be faster than. bites('a persona set with no safety reference must FAIL (undefined baseline)', p => { p.length = 2; }); }); /* ---- Y26-LEDGE-ADMITS (Task 3) — the module's own playability bar, run over a SHORT seed range * because the suite is already ~28 minutes and a full sweep is ~0.5s a seed. * MEASURED 2026-07-22 on this branch, re-derived rather than quoted: seeds 1..40 admit 40/40 (6 * personas each = 240 faithful playouts), every counter in _PARK_LEDGE_WHYS at zero. Widened as a * vacuity probe to seeds 1..200: 200/200, still zero, 105s. * READ THAT NUMBER THE WAY BULL'S IS READ, NOT THE WAY STORM'S IS. 100% here is the BUILDER's * invariant showing through — the board CONSTRUCTS its three separators on every seed (the band and * its ramp, the ordered chain's forced return trip, the crate parked between the two columns that * want it) rather than waiting for a seed to supply them. Storm's 6/40 measures the opposite thing: * how often an unforced geometry happens to pose its scene. Do not rank the modules by these. The * bar is not vacuous — Y26-LEDGE-SIG trips the signature eight distinct ways. */ test('LEDGE-ADMITS: every swept cell is playable by all six personas, with the reject tally silent', () => { const before = JSON.parse(JSON.stringify(E._PARK_LEDGE_WHYS)); let ok = 0; for (let s = 1; s <= 6; s++) if (E._parkLedgeAdmissible(E._parkLedgeCell(s))) ok++; assert.strictEqual(ok, 6, 'seeds 1..6 must all admit'); for (const k of Object.keys(E._PARK_LEDGE_WHYS)) { assert.strictEqual(E._PARK_LEDGE_WHYS[k], before[k], `_PARK_LEDGE_WHYS.${k} moved during a sweep that admitted everything — the tally and the verdict disagree`); } }); /* ---- Y26-SHIP-GATE (promotion commit, 2026-07-22) — the y22 shape now that y26 is SHIPPED. * * This gate replaced a deliberately different one. Until the promotion it pinned "the measurement is * true AND the flip is outstanding", because y26 was the first slot whose pairing bar passed while it * was still a preview, and the y23/y25 equality shape is only writable when BOTH its sides are false. * The promotion makes the equality the right assertion, so it is written here — and unlike y23's and * y25's gates, it also pins `!open`, which is the gap both of those comments flag and neither closes. * * TWO BARS, MEASURED SEPARATELY, BOTH TRUE — and they are not the same quantity: * MODULE PARK_LEDGE_SHIPPABLE = _parkLedgeRecovers(_parkLedgeCell(PARK_LEDGE_SHIP_SEED)) — a SOLO * blind order-recovery on the ledge board alone, the _parkBombRecovers shape. Swept as a * vacuity check: 24/24 base seeds, 6 personas each = 144 runs, every one completing alive * and recovering its own full order at PARK_CAL_TURNS. CAMP-CROSS-SWEEP's F4 reads this. * PAIRING CAMP.PARK_Y26_SHIPPABLE = _parkY26Recovers() — the push demo -> ledge play crossing. * 24/24 base seeds. Anti-mimic measured on its own (_parkY26MimicLeaks, because the * predicate short-circuits past its own mimic loop): 144 probes, 0 EXPRESSED, 0 leaked. * Controls on the same probes: y25 0/24 & 41% leak, y23 0/24 & 50% leak, y22 (shipped) * 24/24 & 0/144 expressed. y26 measures like y22 and unlike both peers it was built with. * * WHY IT MEASURES THAT WAY — physical divergence. A replayed push trajectory on a ledge board meets a * masked skirt band and a crate that is not where the demo left it, so it parses as input NOISE * rather than as evidence. Same lever the walk and slide crossings measure, landing harder here. */ /* ==== Y26-GATES-END ==== */ /* ==== Y27-GATES-BEGIN ==== */ /* ---- Y27-TRAIL-BUILD (Task 1, plan 2026-07-20-y27-trail-wall.md) — the board's GEOMETRY. * The trail module's identity is "the ground you left is shut behind you", and the scene it needs is * a DIVIDER the walker must cross with more than one way through it. This gate pins seed-purity and * that geometry: one wall row across the interior with exactly THREE openings, priced differently by * the SHIPPED safety frame (deep 24 / verge 8 / walkway 1) rather than by anything this module says; * the walker's three chain gems in one cluster on the far side; the companion's errand crossing the * divider through the walkway opening. The freezing itself is Y27-TRAIL-HOOKS. */ /* ---- Y27-TRAIL-HOOKS (Task 2) — the freeze, the melt, and the four things that were easy to get * wrong about them: * (1) a real step freezes the cell just VACATED, in all three domains at once (there is no traveller * for whom old ice is passable, and no legalAdd anywhere in this module to argue otherwise); * (2) 'stay' freezes NOTHING — onLeave fires on it too (engine.js:7325-7329) and a module that * inherited that would seal the walker under his own feet the first time he paused; * (3) the melt is a SCHEDULE, not a countdown: expiry is stamped once as beat + TTL and tick only * removes what is due, so a search fork that rewinds the beat sees the same ice; * (4) a walker sealed in on every side still has 'stay' (the seam's own guarantee) — being boxed in * by your own trail is a spent beat, never a stuck engine and never a terminal. */ /* ---- Y27-TRAIL-READS (Task 3) — the ONE facet, and the three ways a facet gets written wrong. * N keeps the ice off the companion's published route. It must SELF-GATE (cold => the WHOLE legal * set, never {} — an empty prefer deletes the mind rather than vetoing, engine.js:7369-7377), it must * only ever NARROW, and it must be reachable from a real trajectory rather than only from a * hand-placed state. * * There is no C facet, and the first half of this gate is what pays for that absence. A self-boxing * C facet was built here and measured dead: 0 of 4814 states over seeds 1..40 x 6 personas ever * offered the oracle a move that would leave the walker with no open neighbour. Rather than keep a * read for a situation that does not arise, this gate PINS THE MEASUREMENT — it re-derives the count * from live trajectories and goes red the moment the board or the TTL changes enough to make * self-boxing reachable, which is exactly when the facet becomes worth building again. Pinning the * measurement instead of asserting the facet is the y26 discipline. */ /* ---- Y27-TRAIL-SIG (Task 3) — the field signature, and PROOF IT CAN FAIL. Three openings, three * minds, and each mind is read off a DIFFERENT observable, because on this board they do not differ * in score at all (every persona banks the same three-gem cluster) and a signature leaning on score * would be reading noise. The eleven mutations below are the point of this gate: a bar nothing can * trip is not a bar. */ /* ---- Y27-TRAIL-ADMITS (Task 3) — the module's own playability bar, run over a SHORT seed range * because the suite is already ~28 minutes and a full sweep costs ~0.24s a seed. * MEASURED 2026-07-22 on this branch, re-derived rather than quoted: seeds 1..40 admit 40/40 (6 * personas each = 240 faithful playouts, 9.4s), every counter in _PARK_TRAIL_WHYS at zero. Widened * as a vacuity probe to seeds 1..200: see the module note in engine.js. * READ 100% AS THE BUILDER'S INVARIANT, not as a lucky draw: this board CONSTRUCTS its separators on * every seed (a wall divider whose three openings are priced 24 / 8 / 1 by the SHIPPED safety frame, * the companion's only crossing being the cheapest of the three, the gem cluster beyond it). Storm's * 6/40 measures the opposite quantity — how often an unforced geometry happens to pose its scene. * THE BAR IS NOT VACUOUS, AND THAT WAS CHECKED RATHER THAN ASSUMED: Y27-TRAIL-SIG trips the * signature eleven distinct ways, and the sweep itself was WITNESSED at 0/40 with WHYS {sig:40} by * moving the companion's station one row closer to the divider — he then clears his own door before * any caution-led walker reaches it, nothing of his route is ever frozen, and the harm the cell * exists to pose stops existing. */ /* ---- Y27-TRAIL-STAGING (Task 3) — THE Y26 LAW, pinned as a gate rather than trusted to a comment. * y26 measured 0/40 for one reason: the cell its facet steered the walker onto was a token cell, so * a care-led walker ATE his companion's gem on the way to doing him a favour and the read went cold * one step before the act it had spent eight turns preparing. This gate asserts the corresponding * emptiness for y27 over every seed the module's own sweep covers: no cell the care errand routes * the walker through is ever a token cell, on either side of the mirror. */ /* ==== Y27-GATES-END ==== */ /* ==== Y24-GATES-BEGIN ==== */ /* y24 LANTERN — the night board whose night is NOT in the engine. * * The whole cell rests on one architectural decision, and these gates are where it is pinned: the * DARK is a fact of the app's painter alone. The oracle, the three minds and the blind readout all * see the whole board. What the engine models is the LANTERN's public physics — the companion may * not walk outside the light, the light is carried until it is hung on one of three hooks, and * hanging it is an ordered, recorded act. So the read stack is the standard one and these gates can * be the standard gates; nothing here needs a seeing/unseen state and nothing here may grow one. * * (If a later owner is tempted to give `dyn` a visibility set: that would NARROW nothing — it would * REPLACE the shipped minds with masked copies of themselves, which the registry contract forbids * outright, engine.js:7362-7366. "He did not go because he could not see" and "he did not go because * he did not care" would become the same move, and the cell stops being readable at all.) */ const _Y24_SEEDS = [1, 7, 42, 4149]; test('Y24-LANT-BUILD: seed-pure; three distinct walkable hooks off every token cell; the junction hook is the deep-adjacent throat between two lobes; the lantern starts carried', () => { for (const seed of _Y24_SEEDS) { const a = E._parkLantBuild({ seed }), b = E._parkLantBuild({ seed }); const L = a.park.lantern; assert.strictEqual(JSON.stringify(L.hooks), JSON.stringify(b.park.lantern.hooks), 'hooks are not seed-pure'); assert.strictEqual(a.N, E.PARK_LANT_N); assert.strictEqual(new Set(L.hooks).size, 3, 'three distinct hooks'); for (const hk of L.hooks) assert.ok(a.park.walkway.has(hk) || a.park.verge.has(hk), 'hooks are walkable'); // THE y26 LESSON, PINNED: a cell the care facet parks the walker on must never be a token cell. // y26 swept 0/40 because the staging square WAS the companion's contract gem: the care-led walker // ate the gem on his way to the favour, and the care read is gated on that gem being alive. const tokKeys = new Set(a.tokens.map(t => t.y * a.N + t.x)); for (const hk of L.hooks) assert.ok(!tokKeys.has(hk), 'a hook sits on a token cell (y26 trap)'); assert.ok(!tokKeys.has(E._parkKey(a, a.park.spawn)), 'spawn is a token cell'); // the corridor: the ONLY companion-passable seam between the two deep lobes, and its junction // hook is deep-adjacent (verge) so the shipped caution mind refuses the errand without this // module saying one word about safety. assert.ok(L.corridor.size >= 2, 'escort corridor exists'); assert.strictEqual(L.hooks[1], L.junction, 'hooks[1] is the corridor junction'); assert.ok(a.park.verge.has(L.junction), 'the junction hook must be verge (deep-adjacent)'); assert.ok(a.park.walkway.has(L.lane), 'the crossing lane must be plain walkway'); // the lantern begins in his hand: the light centre is the walker himself. assert.strictEqual(a.park.dyn.lantern.key, null, 'lantern starts carried'); assert.strictEqual(E._parkLantCenter(a), E._parkKey(a, a.pos[0]), 'carried light centres on the walker'); assert.strictEqual(E._parkLantLit(a, E._parkKey(a, a.pos[0])), true, 'the walker stands in his own light'); } }); test('Y24-LANT-HOOKS: hook entry hangs the lantern (logged) and re-entry takes it back; the dark is masked for the COMPANION ONLY; stay is a no-op; a dark-stranded companion is counted', () => { const st = E._parkLantBuild({ seed: 42 }); const P = E.parkStart(st); const M = E.PARK_FIELD_MECHS.lantern, dyn = st.park.dyn, n = st.N; const hk = st.park.lantern.hooks[1]; const cellOf = (k) => ({ x: k % n, y: (k - k % n) / n }); // (1) hang / take back M.onEnter(P, { mvKey: 'right', from: st.pos[0], to: cellOf(hk), fromKey: E._parkKey(st, st.pos[0]), toKey: hk }); assert.strictEqual(dyn.lantern.key, hk, 'entering a hook while carrying hangs the lantern'); assert.strictEqual(dyn.lantern.drops.length, 1, 'the hang is logged'); assert.strictEqual(E._parkLantCenter(st), hk, 'a hung lantern is the light centre'); M.onEnter(P, { mvKey: 'stay', from: cellOf(hk), to: cellOf(hk), fromKey: hk, toKey: hk }); assert.strictEqual(dyn.lantern.key, hk, "'stay' is not an entry — the hook seam ignores it"); M.onEnter(P, { mvKey: 'left', from: st.pos[0], to: cellOf(hk), fromKey: E._parkKey(st, st.pos[0]), toKey: hk }); assert.strictEqual(dyn.lantern.key, null, 're-entering the lantern cell picks it back up'); // (2) MATE-ONLY mask. me/route are untouched: no cell is closed to the walker and none to the // route metric, so this module can never hit the masked-cell argmin trap (engine.js:7941-7947). let far = null; for (let k = 0; k < n * n; k++) if ((st.park.walkway.has(k) || st.park.verge.has(k)) && !E._parkLantLit(st, k)) { far = k; break; } assert.ok(far != null, 'the board must have a dark walkable cell'); assert.strictEqual(M.legalMask(P, far, 'mate'), true, 'the dark is a wall to the companion'); assert.strictEqual(!!M.legalMask(P, far, 'me'), false, 'the walker walks into his own dark'); assert.strictEqual(!!M.legalMask(P, far, 'route'), false, 'the route metric is never narrowed'); for (let k = 0; k < n * n; k++) { assert.strictEqual(!!M.legalMask(P, k, 'me'), false, 'no cell may be closed to the walker'); assert.strictEqual(!!M.legalMask(P, k, 'route'), false, 'no cell may be closed to the route metric'); } assert.strictEqual(M.legalAdd, undefined, 'lantern opens no cell: legalAdd would need a route answer it has not got'); // (3) the dark-stranded companion is COUNTED (public meter; the signature and the render read it). // STRANDED means no LIT STEP, so the probe cell must be dark with every neighbour dark too — a // cell just outside the ball still has a lit step and is not stranded at all. let deepDark = null; for (let k = 0; k < n * n; k++) { if (!(st.park.walkway.has(k) || st.park.verge.has(k)) || E._parkLantLit(st, k)) continue; let anyLit = false; for (const d of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = (k % n) + d[0], ny = ((k / n) | 0) + d[1]; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; if (E._parkLantLit(st, ny * n + nx)) anyLit = true; } if (!anyLit) { deepDark = k; break; } } assert.ok(deepDark != null, 'the board must have a cell with no lit step at all'); st.pos[1] = { x: deepDark % n, y: (deepDark - deepDark % n) / n }; const f0 = dyn.lantern.frozeBeats; M.tick(P, { mvKey: 'stay' }); assert.ok(dyn.lantern.frozeBeats > f0, 'a companion with no lit step is a frozen beat'); }); test('Y24-LANT-FREEZE: a companion the light has left is STUCK, not FINISHED — he holds his contract and resumes when the light returns', () => { const st = E._parkLantBuild({ seed: 42 }); const P = E.parkStart(st); const park = st.park, n = st.N; const gemIdx = park.contracts[0].gem; // put him on his own lane, out in the dark, and engage his errand P.mode = 'toGem'; st.pos[1] = { x: park.companionSpawn.x, y: park.companionSpawn.y }; park.dyn.lantern.key = null; st.pos[0] = { x: park.spawn.x, y: park.spawn.y }; const planDark = E._parkCompanionPlan(P); assert.ok(planDark && planDark.stuck === true, 'the dark must make his target unreachable'); assert.ok(!planDark.arrived, 'STUCK is not ARRIVED (engine.js:7398-7402)'); assert.strictEqual(P.contract, 0, 'a stuck companion keeps his contract'); assert.ok(st.tokens[gemIdx].alive, 'and his gem'); // hang the lantern on the junction hook: his whole errand lights up and the plan resumes park.dyn.lantern.key = park.lantern.junction; const planLit = E._parkCompanionPlan(P); assert.ok(planLit && !planLit.stuck && planLit.next, 'the hung lantern re-opens his lane'); }); test('Y24-LANT-READS: the escort facet is self-gated, never empty, and its errand cell is one the shipped caution mind refuses (C-N disjoint by geometry)', () => { const st = E._parkLantBuild({ seed: 42 }); const P = E.parkStart(st); const park = st.park, M = E.PARK_FIELD_MECHS.lantern, R = M.reads; // RANGED, like the shipped care mind: at the spawn stub, far from the hook, this facet has no // escort move to offer and says so in engaged() rather than pacing (the well it would otherwise // dig is documented on PARK_LANT_REACH). const n = st.N, J = park.lantern.junction; assert.ok(E._parkLantApproach(P, J)[E._parkKey(st, st.pos[0])] > E.PARK_LANT_REACH, 'the spawn must lie outside the escort reach, or the cold assertion below is vacuous'); assert.strictEqual(R.ctx(P).live, false, 'out of reach the facet is cold'); // HOT: stand him one step from the junction hook and read both minds at that state. st.pos[0] = { x: J % n, y: ((J / n) | 0) - 1 }; P._fields = null; const ctx = R.ctx(P); assert.strictEqual(ctx.live, true, 'on the doorstep the escort facet must be live'); assert.strictEqual(R.N.engaged(P, ctx), true); const reads = E._parkReads(P); const legal = reads.legal; const nSet = R.N.prefer(P, legal, ctx); assert.ok(nSet.size > 0, 'an empty prefer() does not veto — it deletes the mind (engine.js:7369-7377)'); const step = legal.find(c => c.key === J); assert.ok(step, 'the junction hook must be a legal step from its own doorstep'); assert.ok(nSet.has(step.k), 'the escort facet must want the hook'); assert.strictEqual(park.distDeep[J] , 1, 'the junction hook is deep-adjacent'); assert.ok(reads.atts.C.engaged, 'caution is engaged on the doorstep of the throat'); assert.ok(!reads.atts.C.pref.has(step.k), 'and caution refuses the hook — the C-N disjunction'); // COLD: hang it, and the facet must go inert by returning EVERY legal move, not {}. park.dyn.lantern.key = J; const cold = R.ctx(P); assert.strictEqual(cold.live, false, 'the facet is self-gated on its own errand'); const coldSet = R.N.prefer(P, legal, cold); assert.strictEqual(coldSet.size, legal.length, 'cold means say nothing, i.e. return the whole legal set'); }); test('Y24-LANT-SIG: the field signature is falsifiable — eight degenerate playout shapes are each rejected', () => { const base = () => ([ { turns: 20, deepEntries: 1, _lantJunction: false, _lantMateHome: false }, // goal, safety, care ... { turns: 20, deepEntries: 1, _lantJunction: false, _lantMateHome: false }, { turns: 34, deepEntries: 0, _lantJunction: false, _lantMateHome: false }, { turns: 34, deepEntries: 0, _lantJunction: false, _lantMateHome: false }, { turns: 40, deepEntries: 0, _lantJunction: true, _lantMateHome: true }, { turns: 40, deepEntries: 0, _lantJunction: true, _lantMateHome: true }, ]); assert.strictEqual(E._parkLantSignature(base()), true, 'the canonical shape must pass, or the rejections below are vacuous'); const shake = (i, patch) => { const p = base(); Object.assign(p[i], patch); return p; }; const bad = [ shake(0, { deepEntries: 0 }), // goal declined the shortcut shake(0, { turns: 60 }), // goal is not faster than caution shake(2, { deepEntries: 1 }), // caution paid the body shake(2, { _lantJunction: true }), // caution ran the verge errand shake(4, { _lantJunction: false }), // care did not hang the lantern shake(4, { _lantMateHome: false }), // care hung it and he still never got home shake(1, { _lantMateHome: true }), // goal got him home anyway (no separation) shake(3, { _lantJunction: true }), // caution hung it too ]; for (let i = 0; i < bad.length; i++) assert.strictEqual(E._parkLantSignature(bad[i]), false, `degenerate shape ${i} was accepted`); }); test('Y24-LANT-STAGING: no token sits on a hook or anywhere in the throat the escort errand runs through (seeds 1..40)', () => { // THE y26 POST-MORTEM AS A STANDING GATE. That cell swept 0/40 because the square the care facet // parked the walker on was the companion's own contract gem: he ate it on the way to the favour, // and the care read is gated on that gem being alive, so the read went cold one step before the act // it had spent eight turns setting up. The rule that falls out of it is cheap to check and easy to // break by accident, so it is checked here rather than remembered. for (let seed = 1; seed <= 40; seed++) { const st = E._parkLantBuild(E._parkLantCell(seed)); const L = st.park.lantern; const staged = new Set([...L.hooks, ...L.corridor]); for (const t of st.tokens) { assert.ok(!staged.has(t.y * st.N + t.x), `seed ${seed}: a token stands on a staging cell (${t.x},${t.y}) — the y26 trap`); } } }); test('Y24-LANT-ADMITS: the module admits its own cells over seeds 1..40, and every reject reason is tallied out loud', () => { let ok = 0; const before = E.parkLantWhys(); for (let seed = 1; seed <= 40; seed++) if (E._parkLantAdmissible(E._parkLantCell(seed))) ok++; const w = E.parkLantWhys(); const delta = {}; for (const k in w) delta[k] = w[k] - before[k]; console.log(' [Y24-LANT-ADMITS] ' + ok + '/40 cells admitted (seeds 1..40, 6 personas each) whys=' + JSON.stringify(delta)); assert.strictEqual(E.PARK_LANT_SHIPPABLE, false, 'y24 is a PREVIEW: its module pin is a false literal until a measurement session derives it'); // 40/40 IS THE BUILDER'S INVARIANT SHOWING THROUGH, exactly as y26's is — every separator this // board needs (the barrier, the three-wide throat, the two verge flanks, the ordered throat-ward // chain, the companion's whole errand inside one hook's ball) is CONSTRUCTED on every seed rather // than waited for. It is NOT a vacuous bar, and that was checked rather than assumed: two one-lever // shakes of the board each drive it to 0/40 with WHYS {sig:40} — moving the junction hook onto the // throat's plain-walkway middle lane (caution stops refusing it, so every mind hangs the lantern), // and moving the companion's contract gem onto the junction hook (the y26 trap, which puts the care // read's own gate under the care walker's feet). Widened as a further probe to seeds 1..200: // 200/200, every counter still zero. assert.strictEqual(ok, 40, `y24 admitted ${ok}/40 cells over seeds 1..40 — whys ${JSON.stringify(delta)}`); }); /* ==== Y24-GATES-END ==== */ /* ---- HUB-BADGE-FIT (2026-07-22) — the hub tile badge band must fit inside its own tile. * app.js needs the DOM, so this reads it as SOURCE, the C1-drawToken idiom every app.js gate here * uses. Two independent things are checked, and the first is the one with teeth: * * (1) THE DECLARED SPAN IS HONEST. drawHubBadges lays its glyphs on a cursor of advances that are * all multiples of one metric `m`, and it sizes `m` from BADGE_SPAN — the row's width in units * of m. If someone adds a glyph or bumps a coefficient without re-deriving BADGE_SPAN, the row * silently outgrows the budget it is being fitted to. So the gate re-sums the coefficients out * of the source and fails if they exceed the declared span. * (2) IT ACTUALLY FITS, at every tile count the picker can reach. Tile side comes from * pickerTileRects' own constants, not from a copy of them. * * WHY THIS EXISTS. The band used to scale off `bh`, which is floored at 17px, so below ~68px tiles * its content froze at a constant 82.7px while the tile kept shrinking. Measured overhang past the * NEIGHBOURING tile's left edge: 18 tiles -1.3px (fit, by luck), 19-21 tiles +10.7px, 22-24 +19.7px. * The 2026-07-21 hub capture shows exactly that. Note the vacuity trap this gate is written around: * "does it spill onto the neighbour" was GREEN at 18 tiles for no designed reason, so the gate asserts * the row fits its OWN tile with a margin instead. */ test('HUB-BADGE-FIT: the tile badge row fits inside its tile from 19 up to the current slot count, and its declared span matches the glyphs it actually draws', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const num = (re, what) => { const mm = re.exec(src); assert.ok(mm, `HUB-BADGE-FIT cannot read ${what} out of app.js — its source regex has drifted, ` + 'which means this gate stopped measuring the thing it claims to measure'); return parseFloat(mm[1]); }; // ---- (1) the declared span vs the advances actually drawn (worst row: kind m4, 3 pulls, 1 heart) const SPAN = num(/const BADGE_SPAN = ([\d.]+);/, 'BADGE_SPAN'); const MARGIN = num(/const BADGE_MARGIN = ([\d.]+);/, 'BADGE_MARGIN'); const lead = num(/let x = r\.x \+ m \* ([\d.]+);/, 'the lead-in inset'); const arch = num(/x \+= m \* ([\d.]+);/, 'the arch advance'); const swatch = num(/const s = m \* ([\d.]+);/, 'the hazard swatch size'); const hPad = num(/_heartPath\(bx, x \+ s \/ 2 \+ m \* ([\d.]+) \+ h \* m \* [\d.]+, y, m \* [\d.]+\)/, 'the heart pad'); const hStep = num(/_heartPath\(bx, x \+ s \/ 2 \+ m \* [\d.]+ \+ h \* m \* ([\d.]+), y, m \* [\d.]+\)/, 'the heart step'); const pullR = num(/drawKindPulls\(bx, x, y, tile\.kind, m \* ([\d.]+)\)/, 'the pull-dot radius'); const pullG = num(/ctx\.arc\(x \+ r \+ i \* \(r \* 2 \+ r \* ([\d.]+)\), y, r, 0, 7\)/, 'the pull-dot gap'); const PULLS = 3; // m4 / park carry three conflict dots; m1 carries two // MIND THE UNITS — this is where the gate first caught its own author. app.js writes the pull gap as // a multiple of the dot RADIUS (r * 0.55), and r is itself m * pullR, so in metric units the gap is // pullR * pullG. Reading pullG as m-units inflates the sum to 5.72 and condemns a row that fits. const gapM = pullR * pullG; const drawn = lead + arch + swatch + hPad + hStep // cursor up to the pull dots + pullR + (PULLS - 1) * (2 * pullR + gapM) + pullR; assert.ok(drawn <= SPAN + 1e-9, `the badge row draws ${drawn.toFixed(3)} metric-units but BADGE_SPAN declares ${SPAN} — the row ` + 'outgrew the budget it is fitted to; re-derive BADGE_SPAN from the advances above'); assert.ok(drawn > SPAN * 0.9, `BADGE_SPAN=${SPAN} is ${(SPAN / drawn).toFixed(2)}x the ${drawn.toFixed(3)} actually drawn — a span ` + 'padded that far is not a measurement, and it shrinks the band for nothing'); // ---- (1b) THE SOURCE ACTUALLY CAPS THE METRIC. Part (2) below recomputes the metric from the // formula the fix is supposed to use, so on its own it would keep passing even if drawHubBadges went // back to sizing everything off bh — it would be grading the fix's design instead of the fix. (That // hole was live for one revision of this gate.) So pin the line itself. assert.ok(/const m = Math\.min\(bh, \(r\.w - BADGE_MARGIN\) \/ BADGE_SPAN\);/.test(src), 'drawHubBadges no longer caps its horizontal metric by the tile width — without that cap the row ' + 'reverts to a constant width inside a shrinking tile, which is the overhang this gate exists for'); // ---- (2) the fit itself, over every tile count the picker can reach const COLS = num(/const n = tiles\.length, cols = (\d+)/, 'the column count'); const GAP = num(/const gap = (\d+), m = \d+/, 'the tile gap'); const MARG = num(/const gap = \d+, m = (\d+)/, 'the grid margin'); const BHC = num(/const bh = Math\.min\((\d+), Math\.max\(\d+, r\.h \* [\d.]+\)\)/, 'the bh cap'); const BHF = num(/const bh = Math\.min\(\d+, Math\.max\((\d+), r\.h \* [\d.]+\)\)/, 'the bh floor'); const BHR = num(/const bh = Math\.min\(\d+, Math\.max\(\d+, r\.h \* ([\d.]+)\)\)/, 'the bh ratio'); const BOARD = 540; // index.html: canvas#board is 540x540 const rows = []; // THE UPPER BOUND IS DERIVED, NOT TYPED. A literal here goes stale the moment a crossing is seated: // the gate keeps passing on tile counts the hub no longer draws while the count it DOES draw goes // unmeasured. (Live for one seating — y33 took the hub to 27 while this loop still stopped at 26.) // Reading it off the roster makes the next seating extend the sweep by itself. const MAXSLOTS = CAMP.PARK_CROSSINGS.length; assert.ok(MAXSLOTS >= 19, `the roster holds ${MAXSLOTS} slots — below this sweep's 19-tile floor, so ` + 'the loop below would not execute and this gate would pass vacuously'); for (let n = 19; n <= MAXSLOTS; n++) { const rr = Math.ceil(n / COLS); const u = Math.min((BOARD - 2 * MARG - (COLS - 1) * GAP) / COLS, (BOARD - 2 * MARG - (rr - 1) * GAP) / rr); const bh = Math.min(BHC, Math.max(BHF, u * BHR)); const m = Math.min(bh, (u - MARGIN) / SPAN); const width = drawn * m; rows.push(`${n}:u=${u.toFixed(1)} row=${width.toFixed(1)}`); assert.ok(width <= u - MARGIN + 1e-9, `at ${n} slots the badge row is ${width.toFixed(1)}px inside a ${u.toFixed(1)}px tile — it needs to ` + `leave ${MARGIN}px clear, and past the tile edge it paints on the neighbour`); assert.ok(width > 0 && m > 2, `at ${n} slots the metric collapsed to ${m.toFixed(2)} — unreadable, not fitted`); } console.log(` [HUB-BADGE-FIT] span declared ${SPAN}, drawn ${drawn.toFixed(3)}; ${rows.join(' ')}`); }); /* ---- Y27-SHIP-GATE / Y24-SHIP-GATE (2026-07-22) — the two new crossings are PREVIEWS, and these * gates are what make that word mean something. `ship:false` asserted by hand is an opinion; here it * is compared against a pairing measurement that ran, and the anti-mimic control is read from its own * function so it cannot be a loop that never executed. * * THREE QUANTITIES, THREE NUMBERS, never collapsed into one (the lesson y26's promotion paid for): * MODULE PARK_TRAIL_SHIPPABLE / PARK_LANT_SHIPPABLE — what CAMP-CROSS-SWEEP's F4 compares against. * PAIRING PARK_Y27_SHIPPABLE / PARK_Y24_SHIPPABLE — the demo->play crossing, blind order recovery. * MIMIC _parkY27MimicLeaks / _parkY24MimicLeaks — measured SEPARATELY, reported WITH its probe * count, because the pairing predicate short-circuits in its faithful clause on every seed * today and therefore never reaches its own mimic loop. * * MEASURED 2026-07-22 over seeds 1..24 x 6 personas (denominator 144 = 6 x one posed pair x 24): * y22 SHIPPED 24/24 · 0 leaks / 0 expressed y26 SHIPPED 24/24 · 0 / 0 * y23 preview 0/24 · 72 leaks / 144 expressed y25 preview 0/24 · 59 / 144 * y27 preview 0/24 · 0 leaks / 0 expressed y24 preview 0/24 · 72 / 144 * Neither new cell meets the pairing bar, so neither ships. y27's zero leakage is an ANTI-MIMIC * result and not a ship result, and quoting it as though it were would be the exact error these * split functions exist to prevent. */ test('Y24-SHIP-GATE: y24 stays a preview until its pairing measurement says otherwise, and the anti-mimic control reports its probes', () => { const y24 = CAMP.PARK_CROSSINGS.find(c => c.id === 'y24'); assert.ok(y24, 'y24 slot must exist in PARK_CROSSINGS'); assert.strictEqual(y24.ship, CAMP.PARK_Y24_SHIPPABLE, `y24.ship=${y24.ship} disagrees with PARK_Y24_SHIPPABLE=${CAMP.PARK_Y24_SHIPPABLE} — the picker flag ` + 'and the pairing measurement drifted apart. Flip the flag only WITH the measurement, never ahead of it.'); assert.strictEqual(y24.open, true, 'a non-shipped slot must carry the preview mark, or it reads as inert'); const mim = CAMP._parkY24MimicLeaks(); assert.ok(mim.probes > 0, `the anti-mimic control reported ${mim.leaks} leaks over ${mim.probes} probes — zero probes means the ` + 'loop never ran, and "0 leaks" off a loop that never ran is not a measurement'); assert.ok(mim.leaks <= mim.expressed, 'leaks cannot exceed the pairs the mimic even poses'); console.log(` [Y24-SHIP-GATE] pairing=${CAMP.PARK_Y24_SHIPPABLE} (preview) · module=${E.PARK_LANT_SHIPPABLE} · ` + `mimic probes=${mim.probes} expressed=${mim.expressed} leaks=${mim.leaks} @seed ${CAMP.PARK_Y24_SHIP_SEED}`); }); /* ==== TRIAGE-GATES-BEGIN ==== */ /* ---- TRIAGE-PARTITION — the classifier must account for EVERY run. * parkRecoverOrder collapses a tied pair, a cyclic majority, and a plain disagreement into one * null, and the 2026-07-22 pairing measurement duly piled all three into a single "unrecovered" * bucket — which is why we know y27 is 0/24 but not what kind of zero. This gate makes that * mistake structurally impossible: the five buckets plus `complete` must SUM to the run count, * so a run can never be silently dropped or double-counted. */ test('TRIAGE-PARTITION: every run lands in exactly one bucket, and unfiltered seeds produce no runs', () => { const r = CAMP._parkPreviewTriage('y27', 4); assert.strictEqual(r.id, 'y27'); assert.strictEqual(r.seeds, 4); const sum = r.complete + r.undecided.total + r.cyclic + r.misread + r.incomplete.total; assert.strictEqual(sum, r.runs, `buckets sum to ${sum} but there were ${r.runs} runs — the classifier has a hole; ` + `complete=${r.complete} undecided=${r.undecided.total} cyclic=${r.cyclic} ` + `misread=${r.misread} incomplete=${r.incomplete.total}`); // a seed whose PLAY cell the incongruence filter never accepted contributes no runs at all. // "could not be measured" and "measured zero" are different claims and must not be merged. assert.strictEqual(r.runs, (r.seeds - r.unfiltered) * E.PARK_PERSONAS.length, `runs=${r.runs} does not match ${r.seeds - r.unfiltered} filtered seeds x ${E.PARK_PERSONAS.length} personas`); assert.ok(r.undecided.total === 0 || Object.values(r.undecided.byPair).reduce((a, b) => a + b, 0) > 0, 'an undecided run must name at least one undecided pair'); console.log(` [TRIAGE-PARTITION] y27 seeds1..4: runs=${r.runs} complete=${r.complete} ` + `undecided=${r.undecided.total}${JSON.stringify(r.undecided.byPair)} cyclic=${r.cyclic} ` + `misread=${r.misread} incomplete=${r.incomplete.total}${JSON.stringify(r.incomplete.byReason)} ` + `awards=${r.awards.total}/${r.awards.postSkip} postSkip unfiltered=${r.unfiltered}`); }); /* ---- TRIAGE-DRIFT — the duplicated tally is pinned to the original, in BOTH directions. * Task 1 re-walks the award tally that parkRecoverOrder walks. A copy can drift from its original, * and a drifted copy would make the whole ranking a fiction. So: wherever the shipped scorer returns * an order, the triage must agree with THAT ORDER; and wherever it returns null, the triage must land * in undecided or cyclic — never in complete or misread, which would mean the copy is reading a * different world. The engine's scorer is not modified; it is the reference. * THREE parties must agree here, not two. Comparing only the scorer against a tally re-derived * INSIDE this gate would leave _parkPreviewTriage — the copy the ranking is actually built from — * outside the comparison entirely, free to drift while this gate stayed green. So the gate also * pins its own run-by-run verdicts to that function's buckets. */ test('TRIAGE-DRIFT: the triage agrees with parkRecoverOrder wherever it speaks, and stays silent wherever it does not', () => { let checked = 0, spoke = 0, silent = 0, cells = 0; // y26 is SHIPPED and recovers all six orders at seed 1, so it is the only one of the three that // exercises the "the scorer SPOKE, so the copy must agree with THAT ORDER" direction; y27 and y12 // are previews whose every run comes back null and so exercise the silent direction. Both legs of // a bidirectional pin have to actually run, or half the gate is decoration. Reading a shipped // slot here promotes nothing and mutates nothing — it is a reference measurement. for (const id of ['y26', 'y27', 'y12']) { // one shipped, one new preview, one original-five const cx = CAMP.PARK_CROSSINGS.find(c => c.id === id); for (let s = 1; s <= 3; s++) { const dSeed = (s * 61 + 11 + cx.si * 197) >>> 0; const pSeed = (s * 89 + 23 + cx.si * 211) >>> 0; const demoCell = CAMP._parkCrossingDemoCell(cx.kind, cx.demoMech, dSeed, cx.demoHaz); const play = CAMP._parkCrossingPlayCell(cx.kind, demoCell, cx.playMech, pSeed, cx.playHaz, undefined, true); if (play.filtered !== true) continue; const bucket = { complete: 0, undecided: 0, cyclic: 0, misread: 0, incomplete: 0 }; for (const persona of E.PARK_PERSONAS) { const board = () => CAMP._parkCrossBoard(cx.kind, play.cell); const P = E.parkPlayout(board(), persona); if (P.reason !== 'complete') { bucket.incomplete++; continue; } // not the scorer's business const ref = E.parkRecoverOrder(board(), P.moves, CAMP.PARK_CAL_TURNS); const tally = {}; for (const [a, b] of E._PARK_PAIRS) tally[a + b] = { [a]: 0, [b]: 0 }; for (const w of P.awards) if (!(CAMP.PARK_CAL_TURNS > 0 && w.turn <= CAMP.PARK_CAL_TURNS)) tally[w.pair][w.winner]++; const wins = { G: 0, C: 0, N: 0 }; let tied = 0; for (const [a, b] of E._PARK_PAIRS) { const ta = tally[a + b][a], tb = tally[a + b][b]; if (ta === tb) { tied++; continue; } wins[ta > tb ? a : b]++; } const ord = ['G', 'C', 'N'].sort((x, y) => wins[y] - wins[x]); const cyclic = !tied && ord.map(k => wins[k]).join('') !== '210'; const mine = (tied || cyclic) ? null : ord.map(k => E.PARK_ATT_AXIS[k]); checked++; if (ref) { spoke++; assert.deepStrictEqual(mine, ref, `${id} seed ${s} ${persona.join('>')}: the scorer said ${JSON.stringify(ref)} but the ` + `triage's tally said ${JSON.stringify(mine)} — the copy has drifted from the original`); } else { silent++; assert.strictEqual(mine, null, `${id} seed ${s} ${persona.join('>')}: the scorer returned null but the triage produced ` + `${JSON.stringify(mine)} — the copy is reading a different world (tied=${tied} cyclic=${cyclic})`); } if (mine === null) { if (tied) bucket.undecided++; else bucket.cyclic++; } else if (mine.join() === persona.join()) bucket.complete++; else bucket.misread++; } // THE THIRD LEG: campaign.js's own copy — the one the ranking is built from — must land in the // same buckets this gate just derived run by run. `s` is the FIRST accepted seed for this id, // so _parkPreviewTriage(id, s) sweeps seeds 1..s of which only s can contribute runs; the runs // assertion is what makes that a checked claim rather than an assumed one. const t = CAMP._parkPreviewTriage(id, s, CAMP.PARK_CAL_TURNS); assert.strictEqual(t.runs, E.PARK_PERSONAS.length, `${id}: expected seeds 1..${s} to contribute exactly one accepted seed's runs, got ${t.runs} ` + `(unfiltered=${t.unfiltered}) — the gate and the triage are not looking at the same seeds`); assert.deepStrictEqual( { complete: t.complete, undecided: t.undecided.total, cyclic: t.cyclic, misread: t.misread, incomplete: t.incomplete.total }, bucket, `${id} seed ${s}: _parkPreviewTriage's buckets disagree with this gate's own run-by-run ` + 're-derivation — the copy that produces the ranking has drifted from the scorer it claims ' + 'to represent'); cells++; break; // one seed's worth of personas is enough per id } } assert.ok(checked > 0, 'the drift check never ran — no completed run was found to compare'); assert.strictEqual(cells, 3, `only ${cells} of the 3 cells produced an accepted seed in 1..3 — the gate silently shrank`); assert.ok(spoke > 0 && silent > 0, `the pin ran in only one direction (spoke=${spoke} silent=${silent}) — a bidirectional gate that ` + 'never sees the scorer speak, or never sees it fall silent, is asserting half of what it claims'); console.log(` [TRIAGE-DRIFT] compared ${checked} completed runs across ${cells} cells ` + `(scorer spoke ${spoke}, silent ${silent})`); }); /* ---- TRIAGE-DISCRIMINATES — a classifier that puts everything in one drawer has classified nothing. * Kept to TWO cells and FOUR seeds on purpose: the full 13-cell sweep is an on-demand script, not a * suite member. The suite is already ~30 minutes and this gate exists to prove the tool DISCRIMINATES, * which two contrasting cells settle just as well as thirteen. Cells picked for contrast, not luck: * y17 measured 28 awards on its first run and y27 measured 3, so if the award budget never separates * them the tool is not reading what it claims to read. */ test('TRIAGE-DISCRIMINATES: the tool separates cells rather than sorting them all into one bucket', () => { const a = CAMP._parkPreviewTriage('y17', 4); const b = CAMP._parkPreviewTriage('y27', 4); for (const r of [a, b]) { assert.ok(r.runs > 0, `${r.id} produced no runs at all (unfiltered=${r.unfiltered}) — nothing was measured`); const sum = r.complete + r.undecided.total + r.cyclic + r.misread + r.incomplete.total; assert.strictEqual(sum, r.runs, `${r.id}: partition broken`); } assert.notStrictEqual(a.awards.perRun, b.awards.perRun, `y17 and y27 report the same awards-per-run (${a.awards.perRun}) — the evidence budget is the ` + 'axis that tells "not enough conflict posed" apart from "conflict posed but not decided", and if ' + 'it cannot separate these two cells it is not measuring anything'); console.log(` [TRIAGE-DISCRIMINATES] y17 awards/run=${a.awards.perRun} undecidedPairs/run=${a.undecidedPairsPerRun} | ` + `y27 awards/run=${b.awards.perRun} undecidedPairs/run=${b.undecidedPairsPerRun}`); }); /* ==== TRIAGE-GATES-END ==== */ /* ==== Y29-GATES-BEGIN ==== */ /* y29 무궁화 꽃이 피었습니다 (statue) — the MODULE gates (core session: Task 1/2/3/5). * The seating gates (FIELD_SHIPPABLE / PIN / CROSS-DEMO-SEAM / fieldChecked) and the pairing * measurement belong to the integration session and are deliberately absent here. * NO TEST INDEX IS HARDCODED in this block: it is appended to the tail and the tail moves. */ test('Y29-STATUE-BUILD: seed-pure; doll on a seed-drawn side; empty deep (channel purity); clock reads are pure beat functions', () => { const sides = new Set(); for (const seed of [1, 7, 42, 4149]) { const a = E._parkStatueBuild({ seed }), b = E._parkStatueBuild({ seed }); assert.strictEqual(JSON.stringify(a.park.statue), JSON.stringify(b.park.statue), 'build must be deterministic'); const st = a; assert.strictEqual(st.N, E.PARK_STATUE_N); assert.strictEqual(st.park.deep.size, 0, 'every heart spent must be a gaze violation (channel purity)'); sides.add(st.park.statue.side); // ARRIVAL GOAL: the chain is ONE marked cell straight in front of the doll, one cell // inside its wall — the errand is the crossing itself, not a gem line. assert.strictEqual(st.park.chain.length, 1, 'the goal is one marked cell, not a gem line'); assert.strictEqual(st.park.statue.finishTi, 0); const f = st.tokens[0]; assert.strictEqual(st.park.statue.finishKey, f.y * st.N + f.x, 'token 0 sits ON the finish cell'); const dd = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }][st.park.statue.side]; assert.strictEqual(st.park.statue.finishKey, st.park.statue.dollKey - dd.y * st.N - dd.x, 'finish = the cell just inside the doll wall'); assert.strictEqual(st.park.contracts[0].gem, 1, 'his contract gem is token 1 now'); assert.strictEqual(E._parkStatueGazing(st), false, 'beat 0 is song'); assert.strictEqual(E._parkStatueTillGaze(st), E.PARK_STATUE_SING); st.park.dyn.beat = E.PARK_STATUE_SING; // the first gaze beat assert.strictEqual(E._parkStatueGazing(st), true); assert.strictEqual(E._parkStatueTillGaze(st), 0); st.park.dyn.beat = E.PARK_STATUE_SING + E.PARK_STATUE_GAZE; // song again assert.strictEqual(E._parkStatueGazing(st), false); } assert.ok(sides.size >= 2, 'anti-mimic axis: the doll side must vary across seeds'); }); test('Y29-STATUE-HOOKS: moving during gaze costs a heart, stay is always innocent; adjacency holds the mate; a caught mate is stunned then released', () => { const st = E._parkStatueBuild({ seed: 42 }); const P = E.parkStart(st); const M = E.PARK_FIELD_MECHS.statue, dyn = st.park.dyn, n = st.N; const from = st.pos[0], fk = from.y * n + from.x; // song beat: movement is free const h0 = P.hearts; M.onLeave(P, { mvKey: 'R', from, to: { x: from.x + 1, y: from.y }, fromKey: fk, toKey: fk + 1 }); assert.strictEqual(P.hearts, h0, 'song beat: free movement'); // gaze beat: a step is a heart plus a logged confession, a pause is neither dyn.beat = E.PARK_STATUE_SING; M.onLeave(P, { mvKey: 'R', from, to: { x: from.x + 1, y: from.y }, fromKey: fk, toKey: fk + 1 }); assert.strictEqual(P.hearts, h0 - 1); assert.strictEqual(dyn.statue.caught.length, 1); assert.strictEqual(dyn.statue.caught[0].beat, E.PARK_STATUE_SING, 'the confession carries the beat it happened on'); M.onLeave(P, { mvKey: 'stay', from, to: from, fromKey: fk, toKey: fk }); assert.strictEqual(P.hearts, h0 - 1, 'stay never pays'); assert.strictEqual(dyn.statue.caught.length, 1); // the hold: gaze + adjacency masks the mate's WHOLE domain, and nobody else's st.pos[1] = { x: from.x + 1, y: from.y }; for (const kk of [0, 5, fk]) assert.strictEqual(M.legalMask(P, kk, 'mate'), true, 'gaze+adjacent holds him'); assert.strictEqual(M.legalMask(P, fk, 'me'), false, 'me domain untouched'); assert.strictEqual(M.legalMask(P, fk, 'route'), false, 'route domain untouched (no Infinity argmin)'); const held = E._parkCompanionPlan(P); assert.ok(!held || held.stuck === true || held.arrived === true, 'a fully masked mate domain leaves his planner stuck, never mid-path'); // being caught: the previous beat was a gaze beat (gazePrev) and he moved off matePrev dyn.statue.gazePrev = true; dyn.statue.matePrev = { x: st.pos[1].x - 1, y: st.pos[1].y }; // he stood elsewhere last beat st.pos[0] = { x: 1, y: 1 }; // walker far away: no hold M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.statue.mateCaught, 1); assert.strictEqual(dyn.statue.mateStun, E.PARK_STATUE_STUN); assert.strictEqual(M.legalMask(P, 0, 'mate'), true, 'stunned mate is masked everywhere'); M.tick(P, { mvKey: 'stay' }); M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.statue.mateStun, 0, 'stun expires'); assert.strictEqual(dyn.statue.mateCaught, 1, 'a mate who never moved is never caught twice'); // the hold is CONFESSED: gaze beat, he did not move, and the walker was beside him st.pos[0] = { x: st.pos[1].x - 1, y: st.pos[1].y }; dyn.statue.gazePrev = true; dyn.statue.matePrev = { x: st.pos[1].x, y: st.pos[1].y }; const holds0 = dyn.statue.holds.length; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(dyn.statue.holds.length, holds0 + 1, 'standing beside him under the gaze is the care act'); }); test('Y29-STATUE-STEP: the clock offset survives a real step through parkStep — a gaze step bills the beat the walker decided on', () => { const st = E._parkStatueBuild({ seed: 42 }); const P = E.parkStart(st), dyn = st.park.dyn; let guard = 0; while (dyn.beat < E.PARK_STATUE_SING && guard++ < 20) { // walk the song out const legal = E._parkLegal(P).map(c => c.k).filter(k => k !== 'stay'); E.parkStep(P, legal[0]); } assert.strictEqual(dyn.beat, E.PARK_STATUE_SING); assert.strictEqual(P.hearts, 3, 'the song is free'); assert.strictEqual(dyn.statue.caught.length, 0); const legal = E._parkLegal(P).map(c => c.k).filter(k => k !== 'stay'); E.parkStep(P, legal[0]); assert.strictEqual(P.hearts, 2, 'a step taken while it looks costs a heart'); assert.strictEqual(dyn.statue.caught.length, 1); assert.strictEqual(dyn.statue.caught[0].beat, E.PARK_STATUE_SING, 'billed against the beat shown at decision time, not the beat the tick advanced to'); assert.strictEqual(dyn.statue.gazePrev, true, 'the tick left the snapshot the next tick judges by'); const h = P.hearts; E.parkStep(P, 'stay'); assert.strictEqual(P.hearts, h, 'holding still under the gaze is the free move'); // ARRIVAL IS THE TERMINAL: stepping onto the marked cell completes the run through the // shipped chain predicate — no goal grammar of its own. const st2 = E._parkStatueBuild({ seed: 7 }); const P2 = E.parkStart(st2), n2 = st2.N; const d2 = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }][st2.park.statue.side]; const fk2 = st2.park.statue.finishKey; st2.pos[0] = { x: (fk2 % n2) - d2.x, y: ((fk2 / n2) | 0) - d2.y }; // one cell shy of the finish st2.park.dyn.beat = 0; // song: the step is free E.parkStep(P2, d2.y < 0 ? 'U' : d2.y > 0 ? 'D' : d2.x < 0 ? 'L' : 'R'); assert.strictEqual(P2.over, true, 'arriving ends the run'); assert.strictEqual(P2.reason, 'complete'); }); test('Y29-STATUE-READS: under the gaze C is exactly {stay} and N is exactly the closing moves — disjoint by the clock; both say EVERYTHING when cold', () => { const st = E._parkStatueBuild({ seed: 42 }); const P = E.parkStart(st), dyn = st.park.dyn; const R = E.PARK_FIELD_MECHS.statue.reads; st.pos[0] = { x: 6, y: 6 }; st.pos[1] = { x: 6, y: 8 }; // two cells apart, open ground const keysOf = (s) => [...s].sort().join(','); // ---- COLD (song, and the gaze is still PARK_STATUE_LEAD+1 beats off): every mind says everything. dyn.beat = 0; assert.strictEqual(E._parkStatueTillGaze(st) > E.PARK_STATUE_LEAD, true, 'beat 0 is far from the gaze'); let ctx = R.ctx(P), legal = E._parkLegal(P); const all = keysOf(new Set(legal.map(c => c.k))); assert.strictEqual(R.C.engaged(P, ctx), false, 'no gaze, no caution facet'); assert.strictEqual(R.N.engaged(P, ctx), false, 'the escort has not been called yet'); assert.strictEqual(keysOf(R.C.prefer(P, legal, ctx)), all, 'a cold facet must return the WHOLE legal set — {} would delete the mind, not abstain (engine.js:7369-7377)'); assert.strictEqual(keysOf(R.N.prefer(P, legal, ctx)), all, 'same rule for care'); // ---- UNDER THE GAZE, companion a clear step away: the scene the cell is built for. dyn.beat = E.PARK_STATUE_SING; ctx = R.ctx(P); legal = E._parkLegal(P); assert.strictEqual(R.C.engaged(P, ctx), true); assert.strictEqual(R.N.engaged(P, ctx), true); const c = R.C.prefer(P, legal, ctx), nn = R.N.prefer(P, legal, ctx); assert.strictEqual(keysOf(c), 'stay', 'while it looks, the certainly-free move is the only compliant one'); assert.ok(nn.size > 0, 'an empty care set would be an inert mind, not a refusal'); assert.ok(!nn.has('stay'), 'closing on him is never standing still'); for (const k of nn) { const cand = legal.find(x => x.k === k); assert.ok(Math.max(Math.abs(cand.x - st.pos[1].x), Math.abs(cand.y - st.pos[1].y)) < Math.max(Math.abs(st.pos[0].x - st.pos[1].x), Math.abs(st.pos[0].y - st.pos[1].y)), 'every care move must actually shorten the distance to him'); } for (const k of c) assert.ok(!nn.has(k), 'C and N are DISJOINT here — that is the cell, not a coincidence'); for (const k of nn) { const cand = legal.find(x => x.k === k); assert.ok(Math.max(Math.abs(cand.x - st.pos[1].x), Math.abs(cand.y - st.pos[1].y)) <= 1, 'under the gaze a care move must ARRIVE at his shoulder — a heart spent short of him buys nothing'); } // ---- UNDER THE GAZE but out of reach: care refuses to burn a heart it cannot cash. st.pos[1] = { x: 6, y: 9 }; ctx = R.ctx(P); legal = E._parkLegal(P); assert.strictEqual(ctx.near, 3); assert.strictEqual(R.N.engaged(P, ctx), true); assert.strictEqual(keysOf(R.N.prefer(P, legal, ctx)), 'stay', 'too far to reach him this beat: spend nothing'); // ---- THE LEAD-IN is free, so there the read is the plain gradient (and it may be a long walk). dyn.beat = E.PARK_STATUE_SING - E.PARK_STATUE_LEAD; ctx = R.ctx(P); legal = E._parkLegal(P); assert.strictEqual(ctx.gaze, false); assert.strictEqual(E._parkStatueTillGaze(st), E.PARK_STATUE_LEAD); assert.strictEqual(R.N.engaged(P, ctx), true, 'the escort is called in the last lead beats of the song'); assert.strictEqual(keysOf(R.N.prefer(P, legal, ctx)), 'D', 'closing on him while it is still free'); dyn.beat = E.PARK_STATUE_SING; // ---- UNDER THE GAZE, already beside him: care holds the hand instead of walking away. st.pos[0] = { x: 6, y: 8 }; // his companion still stands at (6,9): a single step apart ctx = R.ctx(P); legal = E._parkLegal(P); assert.strictEqual(R.N.engaged(P, ctx), true); assert.strictEqual(keysOf(R.N.prefer(P, legal, ctx)), 'stay', 'beside him under the gaze: hold'); // ---- A STUNNED companion cannot be caught, so there is nothing left to escort him from. dyn.statue.mateStun = 1; ctx = R.ctx(P); legal = E._parkLegal(P); assert.strictEqual(R.N.engaged(P, ctx), false, 'no harm to prevent while he is already sitting it out'); assert.strictEqual(keysOf(R.N.prefer(P, legal, ctx)), keysOf(new Set(legal.map(x => x.k)))); }); test('Y29-STATUE-ADMITS: the admission sweep reports its tally, and the signature is not vacuous', () => { let ok = 0; const before = E.parkStatueWhys(); for (let s = 1; s <= 40; s++) if (E._parkStatueAdmissible(E._parkStatueCell(s))) ok++; const after = E.parkStatueWhys(), delta = {}; for (const k in after) delta[k] = after[k] - before[k]; console.log(` [Y29-STATUE-ADMITS] admitted ${ok}/40 cells (seeds 1..40) · rejects ${JSON.stringify(delta)}`); assert.ok(ok >= 1, `no seed in 1..40 produced a playable statue cell (${JSON.stringify(delta)}) — the module has no board, ` + 'not merely a weak one'); assert.strictEqual(E.PARK_STATUE_SHIPPABLE, false, 'y29 is a PREVIEW module: the pin is a literal false'); // NON-VACUITY: a signature that accepts a degenerate reading is not a signature. Feed it playout // stand-ins whose observables are wrong in one place at a time and require every one to be refused. const base = E.PARK_PERSONAS.map((p) => ({ turns: p[0] === 'goal' ? 8 : 12, hearts: 2, reason: 'complete', _statueCaught: p[0] === 'goal' ? 2 : 0, _statueMateCaught: p[0] === 'care' ? 0 : 1, _statueHolds: p[0] === 'care' ? 2 : 0, _statueMateHome: true, })); assert.strictEqual(E._parkStatueSignature(base), true, 'the intended reading must pass, or the shakes below prove nothing'); const shake = (mut) => { const c = base.map(o => ({ ...o })); mut(c); return E._parkStatueSignature(c); }; const top = E.PARK_PERSONAS.map(p => p[0]); assert.strictEqual(shake(c => c.forEach((o, i) => { if (top[i] === 'safety') o._statueCaught = 1; })), false, 'a caution-led walker who steps under the gaze is not caution-led'); assert.strictEqual(shake(c => c.forEach((o, i) => { if (top[i] === 'goal') o._statueCaught = 0; })), false, 'a goal-led walker who never risks a beat bought nothing with his hearts'); assert.strictEqual(shake(c => c.forEach((o, i) => { if (top[i] === 'care') o._statueMateCaught = 1; })), false, 'a care-led walker who lets him be sent back did not escort him'); assert.strictEqual(shake(c => c.forEach((o, i) => { if (top[i] === 'care') o._statueHolds = 0; })), false, 'care must be an ACT (a hold), not merely the absence of harm'); assert.strictEqual(shake(c => c.forEach((o, i) => { if (top[i] === 'goal') o.turns = 99; })), false, 'if hurrying is not faster, the hearts bought nothing'); assert.strictEqual(shake(c => c.forEach((o) => { o._statueMateCaught = 0; })), false, 'if NOBODY ever loses him to the doll, the hold is free and separates no one'); }); test('Y29-STATUE-SHADOW: behind the companion the gaze does not bill — and WITHOUT him the same step does', () => { const st = E._parkStatueBuild({ seed: 42 }); const P = E.parkStart(st), dyn = st.park.dyn, n = st.N, S = st.park.statue; const M = E.PARK_FIELD_MECHS.statue; const d = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }][S.side]; const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }, c = (n - 1) >> 1; const at = (fwd, side) => ({ x: c + d.x * fwd + lat.x * side, y: c + d.y * fwd + lat.y * side }); const key = (p) => p.y * n + p.x; // the companion stands on the centre file; the walker one cell behind him (farther from the doll) st.pos[1] = at(2, 0); const a = at(1, 0), b = at(0, 0); st.pos[0] = { ...a }; dyn.beat = E.PARK_STATUE_SING; // the doll is looking assert.strictEqual(E._parkStatueShadow(st, key(a)), true, 'behind his body is shadow'); assert.strictEqual(E._parkStatueShadow(st, key(at(3, 0))), false, 'the doll side of him is lit'); assert.strictEqual(E._parkStatueShadow(st, key(at(1, 1))), false, 'one file over is lit'); assert.strictEqual(E._parkStatueShadow(st, key(st.pos[1])), false, 'his own cell is not shadow'); const h0 = P.hearts; M.onLeave(P, { mvKey: 'x', from: a, to: b, fromKey: key(a), toKey: key(b) }); assert.strictEqual(P.hearts, h0, 'a step fully behind him is unseen'); assert.strictEqual(dyn.statue.shadowed.length, 1, 'the shelter is confessed, not silent'); assert.strictEqual(dyn.statue.caught.length, 0); M.onLeave(P, { mvKey: 'x', from: b, to: at(0, 1), fromKey: key(b), toKey: key(at(0, 1)) }); assert.strictEqual(P.hearts, h0 - 1, 'a landing outside the shadow is a seen step'); // NON-VACUITY WITNESS: the very same step with his body elsewhere bills normally. st.pos[1] = at(2, 2); M.onLeave(P, { mvKey: 'x', from: a, to: b, fromKey: key(a), toKey: key(b) }); assert.strictEqual(P.hearts, h0 - 2, 'no body, no shelter — the rule is the mate, not the file'); }); test('Y29-STATUE-SUMMON: the call arms only in reach, the mate answers a cell per beat, is spent at the post, and pays the gaze law on the way', () => { const st = E._parkStatueBuild({ seed: 42 }); const P = E.parkStart(st), dyn = st.park.dyn, n = st.N, S = st.park.statue, D = dyn.statue; const M = E.PARK_FIELD_MECHS.statue; const d = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }][S.side]; const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }, c = (n - 1) >> 1; const at = (fwd, side) => ({ x: c + d.x * fwd + lat.x * side, y: c + d.y * fwd + lat.y * side }); const cheb = () => Math.max(Math.abs(st.pos[0].x - st.pos[1].x), Math.abs(st.pos[0].y - st.pos[1].y)); // OUT OF REACH: the call is refused and arms nothing. st.pos[0] = at(0, 0); st.pos[1] = at(0, -3); assert.strictEqual(E.parkStatueSummon(P), false, 'four cells away is out of reach'); assert.strictEqual(D.summon, null); // IN REACH: the call arms the walker's forward cell. st.pos[1] = at(0, -2); assert.strictEqual(cheb() <= 2, true); assert.strictEqual(E.parkStatueSummon(P), true); const dest = E._parkStatueSummonDest(st); assert.strictEqual(D.summon, dest, 'armed at the forward cell'); assert.strictEqual(dest, (at(1, 0).y * n + at(1, 0).x), 'the post is one cell toward the doll'); // THE FREEZE: while a call is armed his own planner has no domain at all. assert.strictEqual(M.legalMask(P, 0, 'mate'), true); const held = E._parkCompanionPlan(P); assert.ok(!held || held.stuck === true || held.arrived === true, 'his own errand holds, not walks'); // THE ANSWER: one cell per beat through the module tick (song beats: free). dyn.beat = 0; D.gazePrev = false; D.matePrev = { ...st.pos[1] }; let dm0 = Math.abs(st.pos[1].x - (dest % n)) + Math.abs(st.pos[1].y - ((dest / n) | 0)); let guard = 0; while ((st.pos[1].y * n + st.pos[1].x) !== dest && guard++ < 10) { const before = Math.abs(st.pos[1].x - (dest % n)) + Math.abs(st.pos[1].y - ((dest / n) | 0)); D.gazePrev = false; D.matePrev = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); const after = Math.abs(st.pos[1].x - (dest % n)) + Math.abs(st.pos[1].y - ((dest / n) | 0)); assert.strictEqual(after, before - 1, 'he closes exactly one cell per beat'); } assert.ok(guard <= dm0, 'he arrived on the shortest walk'); // THE SPEND (2026-07-27, owner's call — the shield is ONE-USE, not a leash): arrived, the call // ends itself. Spending it moves nobody; what still pins him while the player is sheltering is // the SHOULDER rule (gazing && near<=1), so that is what this asserts now — the shelter lasts // exactly the looking beats the player spends behind his body, and not one beat longer. const post = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(D.summon, null, 'the post is spent on arrival'); assert.deepStrictEqual(st.pos[1], post, 'spending the call does not move him'); st.pos[0] = at(0, 0); dyn.beat = E.PARK_STATUE_SING; // the doll looks, the player is beside him assert.strictEqual(M.legalMask(P, 0, 'mate'), true, 'the shoulder still holds him through the stare'); dyn.beat = 0; st.pos[0] = at(-3, 0); // the player walked on, the song resumes assert.strictEqual(M.legalMask(P, 0, 'mate'), false, 'and after that he is his own again'); // THE GAZE LAW ON THE WAY: a call answered while it looks is a mate sent back. st.pos[0] = at(-1, 0); E.parkStatueSummon(P); // re-arm toward a farther post assert.notStrictEqual(D.summon, st.pos[1].y * n + st.pos[1].x); D.gazePrev = true; D.matePrev = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(D.mateCaught, 1, 'the doll does not care why he walked'); assert.strictEqual(D.mateStun, E.PARK_STATUE_STUN); const stuck = { ...st.pos[1] }; D.gazePrev = false; D.matePrev = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); // stun 2 -> 1: no walking assert.deepStrictEqual(st.pos[1], stuck, 'a stunned body does not answer'); }); /* ==== Y29-GATES-END ==== */ /* ==== Y31-GATES-BEGIN ==== */ /* y31 RELAY — the counter that splits the yard, and the two gems nobody can reach. * * THE ONE THING THESE GATES EXIST TO PIN is that "a gem you cannot pick" is GEOMETRY here and not a * rule. The counter is ordinary floor this module MASKS in all three domains; the contract gems sit * on it, so the walker's indiscriminate harvest (engine.js:8114) never meets them and the * companion's no-wall BFS (engine.js:7605-7613) never reaches them. Delivering a good to a PASS cell * is what opens the matching gem — for the COMPANION only, through `legalAdd(P, key, 'mate')`, the * same additive override y14's paid gate uses. * * THREE ENGINE FACTS THE FIRST DRAFT OF THIS CELL GOT WRONG, each one fatal on its own: * (1) the companion routes to `park.clusters[...]`, not to `st.tokens[...]` (engine.js:7582 vs * 7204 — two objects). Moving a token at runtime would leave him walking at a coordinate the * gem no longer occupies, forever. So NOTHING here moves a token or a cluster; the geometry is * fixed at build time and only the MASK changes. * (2) the walker's harvest fires BEFORE onEnter and is indiscriminate. So a delivery cell must * never be a token cell — pass cells and gem cells are DISJOINT counter cells, pinned below. * (3) straight out of parkStart the companion's plan is `null` (mode 'idle', engine.js:7213/7587), * not `{stuck:true}`. The hold gate therefore drives him to 'toGem' first. */ const _Y31_SEEDS = [1, 7, 42, 4149]; test('Y31-RELAY-BUILD: seed-pure; the counter is FLOOR (not wall) masked in every domain; both contract gems sit on it; pass/gem/stand/goods cells never coincide with a token; the stands are verge', () => { for (const seed of _Y31_SEEDS) { const a = E._parkRelayBuild({ seed }), b = E._parkRelayBuild({ seed }); const R = a.park.relay, n = a.N, K = (p) => p.y * n + p.x; assert.strictEqual(a.N, E.PARK_RELAY_N); assert.strictEqual(JSON.stringify(R.pass), JSON.stringify(b.park.relay.pass), 'pass cells are not seed-pure'); assert.strictEqual(JSON.stringify(R.goods), JSON.stringify(b.park.relay.goods), 'goods are not seed-pure'); assert.strictEqual(JSON.stringify([...R.counter]), JSON.stringify([...b.park.relay.counter]), 'counter is not seed-pure'); // (1) THE COUNTER IS NOT A WALL. legalAdd cannot open a wall (engine.js:7306-7308), so if the // counter were built out of walls the companion could never be let through and the whole cell // would be inexpressible. It is plain floor that this module masks. for (const k of R.counter) assert.ok(!a.wall.has(k), `counter cell ${k} is a wall — legalAdd could never open it`); // (2) the counter SEPARATES: with the mask on, no unmasked path joins the two zones. const M = E.PARK_FIELD_MECHS.relay, P0 = E.parkStart(a); const reach = new Set([E._parkKey(a, a.park.spawn)]); const q = [E._parkKey(a, a.park.spawn)]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of E.DIRS) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (a.wall.has(nk) || reach.has(nk)) continue; if (M.legalMask(P0, nk, 'me')) continue; reach.add(nk); q.push(nk); } } assert.ok(!reach.has(E._parkKey(a, a.pos[1])), 'the walker must not be able to walk into the mate zone'); for (const k of R.gems) assert.ok(!reach.has(k), 'a contract gem must be unreachable to the walker (geometry, not a rule)'); // (3) the contract gems really are ON the counter, and pass cells are OTHER counter cells for (const c of a.park.contracts) { const t = a.tokens[c.gem]; assert.ok(R.counter.has(K(t)), 'every contract gem starts on the counter'); } for (let i = 0; i < R.pass.length; i++) { assert.ok(R.counter.has(R.pass[i]), 'pass cells live on the counter'); assert.ok(!R.gems.includes(R.pass[i]), 'a pass cell must never be a gem cell (the walker would eat it, engine.js:8114)'); assert.ok(!a.park.deep.has(R.pass[i]), 'a pass cell must not be deep — the entry charge fires BEFORE onEnter (engine.js:8130)'); assert.strictEqual(a.park.relay.opens[i][0], R.gems[i], 'each pass opens its own gem'); } // (4) THE y26 LAW: no cell the facet parks the walker on may be a token cell. const tokKeys = new Set(a.tokens.filter(t => t.alive).map(K)); for (const k of [...R.stand, ...R.goods, ...R.pass]) { assert.ok(!tokKeys.has(k), `staging/goods/pass cell ${k} coincides with a live token (the y26 trap)`); } assert.ok(!tokKeys.has(E._parkKey(a, a.park.spawn)), 'spawn is a token cell'); // (5) the stands are VERGE — that is the whole C-N price, delivered by park.deep and no facet. for (const k of R.stand) assert.strictEqual(a.park.distDeep[k], 1, 'a delivery stand must be verge (deep-adjacent)'); assert.ok(a.park.deep.size > 0, 'the rim geometry needs a deep field'); assert.strictEqual(a.park.dyn.relay.carrying, null, 'the walker starts empty-handed'); assert.strictEqual(a.park.dyn.relay.delivered.length, 0, 'nothing is delivered yet'); } }); test('Y31-RELAY-HOOKS: pickup flips carrying; a pass cell opens for the walker (me AND route) only while he is armed; delivery is an action-move that restores position and logs the beat; the mate opening is per-pass', () => { const st = E._parkRelayBuild({ seed: 42 }); const P = E.parkStart(st); const M = E.PARK_FIELD_MECHS.relay, R = st.park.relay, D = st.park.dyn.relay, n = st.N; const cellOf = (k) => ({ x: k % n, y: (k / n) | 0 }); // EMPTY-HANDED: every counter cell is shut in all three domains. for (const k of R.counter) { assert.strictEqual(!!M.legalMask(P, k, 'me'), true, 'empty-handed the counter is shut to the walker'); assert.strictEqual(!!M.legalMask(P, k, 'route'), true, 'and invisible to his route metric'); assert.strictEqual(!!M.legalMask(P, k, 'mate'), true, 'and shut to the companion'); assert.strictEqual(!!M.legalAdd(P, k, 'me'), false, 'nothing is force-opened for an empty-handed walker'); } // PICKUP — an ordinary step. const g0 = R.goods[0]; M.onEnter(P, { mvKey: 'L', from: cellOf(g0 + 1), to: cellOf(g0), fromKey: g0 + 1, toKey: g0 }); assert.strictEqual(D.carrying, 0, 'entering a goods cell picks the good up'); assert.strictEqual(D.left.has(0), false, 'and takes it off the yard'); M.onEnter(P, { mvKey: 'stay', from: cellOf(g0), to: cellOf(g0), fromKey: g0, toKey: g0 }); assert.strictEqual(D.carrying, 0, "'stay' is not an entry (engine.js:7324, lantern 18445)"); // ARMED: the pass cells open for 'me' AND for 'route'. Opening one without the other is the // Infinity trap (engine.js:7941-7947) — a cell legalAdd offers and legalMask('route') hides can // never win the oracle's argmin, so the move is dead. for (let i = 0; i < R.pass.length; i++) { assert.strictEqual(M.legalAdd(P, R.pass[i], 'me'), true, 'carrying: the pass cell opens'); assert.strictEqual(!!M.legalMask(P, R.pass[i], 'me'), false, 'and its own mask lets go of it'); assert.strictEqual(!!M.legalMask(P, R.pass[i], 'route'), false, 'armed, the route metric must SEE the pass cell (y16 design law 1) — otherwise the move is dead'); } for (const k of R.gems) { assert.strictEqual(!!M.legalAdd(P, k, 'me'), false, 'a gem cell is never opened for the walker'); assert.strictEqual(!!M.legalMask(P, k, 'route'), true, 'and never enters his route metric'); } // DELIVERY — an action, not a step (y3 idiom, engine.js:15509). const p0 = R.pass[0], stand0 = R.stand[0]; const before = cellOf(stand0); st.pos[0] = { x: before.x, y: before.y }; M.onEnter(P, { mvKey: R.passMv[0], from: before, to: cellOf(p0), fromKey: stand0, toKey: p0 }); assert.deepStrictEqual(st.pos[0], before, 'an action, not a step — he leans over the counter, he does not stand on it'); assert.strictEqual(D.carrying, null, 'the good left his hands'); assert.strictEqual(D.delivered.length, 1, 'the delivery is logged'); assert.strictEqual(D.delivered[0].pass, 0, 'and it names which shelf'); assert.strictEqual(typeof D.delivered[0].beat, 'number', 'with the beat it happened on'); // THE OPENING IS PER-PASS. gem 0 is now the companion's; gem 1 is still nobody's. assert.strictEqual(M.legalAdd(P, R.gems[0], 'mate'), true, 'the served gem opens for the companion'); assert.strictEqual(!!M.legalAdd(P, R.gems[1], 'mate'), false, 'the unserved one stays shut'); assert.strictEqual(!!M.legalAdd(P, R.gems[0], 'me'), false, 'and it never opens for the walker'); // a served shelf does not take a second good assert.strictEqual(!!M.legalAdd(P, p0, 'me'), false, 'a served pass cell closes again'); // NOTHING MOVED. The token and the cluster still agree, which is the whole point of the design. for (let i = 0; i < st.tokens.length; i++) { assert.strictEqual(st.tokens[i].x, st.park.clusters[i].x, 'token/cluster x drifted apart'); assert.strictEqual(st.tokens[i].y, st.park.clusters[i].y, 'token/cluster y drifted apart'); } }); test('Y31-RELAY-HOLD: before the delivery the companion is STUCK (not finished); after it he walks in and actually banks the gem', () => { const st = E._parkRelayBuild({ seed: 42 }); const P = E.parkStart(st); const M = E.PARK_FIELD_MECHS.relay, R = st.park.relay, D = st.park.dyn.relay, n = st.N; const gemIdx = st.park.contracts[0].gem; // parkStart leaves him IDLE, and an idle companion has no target at all — his plan is null, not // stuck (engine.js:7213/7587). Drive him to his errand first, or this gate asserts nothing. assert.strictEqual(E._parkCompanionPlan(P), null, 'a fresh runtime has an idle companion with no plan'); P.mode = 'toGem'; const plan0 = E._parkCompanionPlan(P); assert.ok(plan0 && plan0.stuck === true, 'the counter must make his gem unreachable'); assert.ok(!plan0.arrived, 'STUCK is not ARRIVED (engine.js:7398-7402) — conflating them retires him for good'); assert.strictEqual(P.contract, 0, 'a stuck companion keeps his contract'); assert.ok(st.tokens[gemIdx].alive, 'and his gem'); // deliver to pass 0 by hand, then let him walk. He must really BANK it: an opened cell is not a // harvest, and "legalAdd returned true" is not evidence that anybody ate anything. D.carrying = 0; D.left.delete(0); const p0 = R.pass[0], stand0 = R.stand[0]; const cellOf = (k) => ({ x: k % n, y: (k / n) | 0 }); st.pos[0] = cellOf(stand0); M.onEnter(P, { mvKey: R.passMv[0], from: cellOf(stand0), to: cellOf(p0), fromKey: stand0, toKey: p0 }); const plan1 = E._parkCompanionPlan(P); assert.ok(plan1 && !plan1.stuck && plan1.next, 'his very next plan resumes (guarantee 1)'); const before = st.score[1]; for (let i = 0; i < 60 && st.tokens[gemIdx].alive; i++) E._parkCompanionStep(P); assert.strictEqual(st.tokens[gemIdx].alive, false, 'he must actually reach and take it, not merely be allowed to'); assert.ok(st.score[1] > before, 'and bank its value'); }); test('Y31-RELAY-READS: one care facet, ranged and self-gated, never empty, and it wants the delivery from the stand; no G facet and no C facet (park.deep prices the errand)', () => { const st = E._parkRelayBuild({ seed: 42 }); const P = E.parkStart(st); const M = E.PARK_FIELD_MECHS.relay, R = M.reads, RL = st.park.relay, n = st.N; assert.ok(R && R.N, 'the module folds its facet into the shipped CARE mind'); assert.strictEqual(R.G, undefined, 'no G facet — the fast metric already prices the shortcut'); assert.strictEqual(R.C, undefined, 'no C facet — park.deep already makes caution refuse the rim (y26 finding)'); // HOT: armed, one step short of a stand. const dyn = st.park.dyn.relay; dyn.carrying = 0; dyn.left.delete(0); st.pos[0] = { x: RL.stand[0] % n, y: (RL.stand[0] / n) | 0 }; P._fields = null; const ctx = R.ctx(P); assert.strictEqual(ctx.live, true, 'armed with an unserved shelf left, the facet is live'); assert.strictEqual(R.N.engaged(P, ctx), true); const reads = E._parkReads(P); const step = reads.legal.find(c => c.key === RL.pass[0]); assert.ok(step && step.add, 'the pass cell must be a force-opened candidate from its own stand'); const nSet = R.N.prefer(P, reads.legal, ctx); assert.ok(nSet.size > 0, 'an empty prefer() does not veto — it deletes the mind (engine.js:7369-7377)'); assert.ok(nSet.has(step.k), 'the facet must want the delivery'); // COLD: serve both shelves and the facet must go inert by returning the WHOLE legal set. dyn.delivered.push({ beat: 0, pass: 0, good: 0 }); dyn.delivered.push({ beat: 0, pass: 1, good: 1 }); dyn.carrying = null; const cold = R.ctx(P); assert.strictEqual(cold.live, false, 'the facet is self-gated on its own errand'); const coldSet = R.N.prefer(P, reads.legal, cold); assert.strictEqual(coldSet.size, reads.legal.length, 'cold means say nothing, i.e. return every legal move'); // RANGED, like the shipped care mind and for the harder reason recorded on PARK_RELAY_REACH: an // unranged facet pulls a caution-first walker at a verge square he may never enter and the pull is // what closed the escape from the band, turning an abstention into a period-4 orbit at the turn cap. // A crate sits one step past its own stand, so the bound must be at least 2 and must not be large. assert.ok(E.PARK_RELAY_REACH >= 2 && E.PARK_RELAY_REACH <= 4, `PARK_RELAY_REACH=${E.PARK_RELAY_REACH}: below 2 the last leg of the relay is out of reach, well above it the facet is effectively unranged again (measured 0/40, whys {complete:40})`); const far = E._parkRelayBuild({ seed: 42 }); const farP = E.parkStart(far); far.park.dyn.relay.carrying = 0; far.park.dyn.relay.left.delete(0); const dd = E._parkRelayApproach(farP, far.park.relay.stand[1]); const here = E._parkKey(far, far.pos[0]); assert.ok(dd[here] > E.PARK_RELAY_REACH, 'the spawn must lie outside the reach of the far stand, or the ranging assertion is vacuous'); }); test('Y31-RELAY-RIM: on every seed 1..40 the delivery stands lie INSIDE the caution band while the whole chain lies OUTSIDE it — caution ABSTAINS from the favour, it is never merely unable to play', () => { // THE MEASUREMENT-FIXED GATE THAT STANDS IN FOR A C FACET. y26 taught that the honest place for the // safety/care split is park.deep's asymmetric price (fast 1 / verge 8 / safe 24), not a module- // authored mind. So rather than asserting "caution refuses", this walks the CAUTION-CLEAN sub-graph // — distDeep >= cautionD, the shipped static caution preference verbatim (engine.js:7757) — and // measures four things off the board. Each one goes red on a different mistake: // (1) a stand outside the band -> the delivery becomes free and the C-N separation collapses. // (2) a chain gem not clean-reachable -> the caution-led run CAPS instead of abstaining (y24's // 20-of-40 bug), and a signature read off a capped run means nothing. // (3) the crates not clean-reachable -> "he had it in his hands and walked past" stops being // true of the caution-led walker, and delivered=0 stops being a refusal. // (4) a clean lane through the band -> the three-wide throat law. Zero clean lanes and the run // caps; a clean lane on the flank beside the walker's lane and the detour stops being one. for (let seed = 1; seed <= 40; seed++) { const st = E._parkRelayBuild(E._parkRelayCell(seed)); const n = st.N, park = st.park, R = park.relay, d = park.cautionD || 2; const src = E._parkKey(st, park.spawn); assert.ok(park.distDeep[src] >= d, `seed ${seed}: the spawn itself must be caution-clean`); const seen = new Set([src]), q = [src]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const dd of E.DIRS) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (seen.has(nk) || st.wall.has(nk) || R.counter.has(nk)) continue; if (park.distDeep[nk] < d) continue; // the caution band: C never steps here seen.add(nk); q.push(nk); } } for (const k of R.stand) { assert.ok(park.distDeep[k] < d, `seed ${seed}: stand ${k} is outside the caution band — the delivery is free`); assert.ok(!seen.has(k), `seed ${seed}: stand ${k} is caution-reachable — C-N collapses`); } for (const i of park.chain) assert.ok(seen.has(E._parkKey(st, st.tokens[i])), `seed ${seed}: chain gem ${i} is not caution-reachable — the safety-led run would CAP, not abstain`); for (const k of R.goods) assert.ok(seen.has(k), `seed ${seed}: crate ${k} is not caution-reachable — "he carried it past the counter" stops being true of him`); // THE THROAT LAW (y24's, restated): a three-wide gap is what makes the caution-clean detour EXIST // — the middle column sits distDeep 2 and stays walkway while the flanks are verge. A two-wide // gap makes every gap cell verge and the caution-led run caps instead of abstaining. The count is // >= 1 rather than == 1 on purpose: when the seed draws the throat hard against the board edge the // OUTER flank has no band cell beyond it and comes out clean too (seed 5 and its family). That is // a wider door, not a missing one, and what matters is that a door exists and that it is never the // flank beside the walker's own lane. const clean = R.throat.filter(k => seen.has(k)); assert.ok(clean.length >= 1, `seed ${seed}: the band has NO caution-clean crossing — the safety-led run would cap, not abstain`); assert.ok(clean.indexOf(R.throat[1]) >= 0, `seed ${seed}: the MIDDLE of the throat must be a clean lane`); assert.ok(clean.indexOf(R.throat[0]) < 0, `seed ${seed}: the throat flank nearest the walker's lane is clean — the detour stops being a detour`); } }); test('Y31-RELAY-SIG: the field signature is falsifiable — eight degenerate playout shapes are each rejected', () => { const base = () => ([ { turns: 20, deepEntries: 1, _relayDelivered: 0, _relayMateHome: false }, // goal, goal, safety, safety, care, care { turns: 20, deepEntries: 1, _relayDelivered: 0, _relayMateHome: false }, { turns: 30, deepEntries: 0, _relayDelivered: 0, _relayMateHome: false }, { turns: 30, deepEntries: 0, _relayDelivered: 0, _relayMateHome: false }, { turns: 40, deepEntries: 0, _relayDelivered: 2, _relayMateHome: true }, { turns: 40, deepEntries: 0, _relayDelivered: 2, _relayMateHome: true }, ]); assert.strictEqual(E._parkRelaySignature(base()), true, 'the canonical shape must pass, or every rejection below is vacuous'); const shake = (i, patch) => { const p = base(); Object.assign(p[i], patch); return p; }; const bad = [ shake(0, { deepEntries: 0 }), // goal declined the shortcut shake(0, { turns: 60 }), // goal is no faster than caution shake(0, { _relayDelivered: 1 }), // goal ran the errand shake(2, { deepEntries: 1 }), // caution paid the body shake(2, { _relayDelivered: 1 }), // caution ran the rim errand shake(4, { _relayDelivered: 1 }), // care left a shelf empty shake(4, { _relayMateHome: false }), // care delivered and he still never got home shake(1, { _relayMateHome: true }), // goal got him home anyway (no separation) ]; for (let i = 0; i < bad.length; i++) assert.strictEqual(E._parkRelaySignature(bad[i]), false, `degenerate shape ${i} was accepted`); }); test('Y31-RELAY-ADMITS: the module sweeps its own cells over seeds 1..40, and every reject reason is tallied out loud', () => { let ok = 0; const before = E.parkRelayWhys(); for (let seed = 1; seed <= 40; seed++) if (E._parkRelayAdmissible(E._parkRelayCell(seed))) ok++; const w = E.parkRelayWhys(); const delta = {}; for (const k in w) delta[k] = w[k] - before[k]; console.log(' [Y31-RELAY-ADMITS] ' + ok + '/40 cells admitted (seeds 1..40, 6 personas each = 240 faithful playouts) whys=' + JSON.stringify(delta)); assert.strictEqual(E.PARK_RELAY_SHIPPABLE, false, 'y31 is a PREVIEW: its module pin is a false LITERAL until a measurement session derives it'); // 40/40 IS THE BUILDER'S INVARIANT SHOWING THROUGH, as y26's and y24's are: every separator this // board needs (the counter, the band, the three-wide throat, the ordered chain that crosses it, a // crate one step short of each stand) is CONSTRUCTED on every seed rather than waited for. Storm's // 6/40 measures the opposite quantity — how often an unforced geometry happens to pose its scene — // and the two figures do not rank the modules. // // AND IT IS NOT A VACUOUS BAR, which was CHECKED rather than assumed. Three one-lever shakes of the // board or the facet, each measured over the same seeds 1..40: // throat drawn from W(1) (so the band no longer covers the stands' column, and the two stands stop // being verge) -> 0/40, whys {sig:40} // the facet unranged (PARK_RELAY_REACH 2 -> 99) -> 0/40, whys {complete:40} — a caution-led // walker orbits four cells to the turn cap; that orbit was the first thing this cell measured. // the last-step exception removed from the facet's just-vacated-square rule // -> 0/40, whys {matelost:40} // Widened as a further probe to seeds 1..200: 200/200, every counter still zero, and all 18 layout // draws (2 sides x 3 band rows x 3 throat columns) occur in that range. assert.strictEqual(ok, 40, `y31 admitted ${ok}/40 cells over seeds 1..40 — whys ${JSON.stringify(delta)}`); }); /* ==== Y31-GATES-END ==== */ /* ==== Y32-GATES-BEGIN ==== */ /* y32 WARP — the first park cell that edits the board's TOPOLOGY rather than its terrain. * * Two pylons, six sockets, and once both are planted the two socket cells are ONE place: whoever * steps on either end comes out at the other. Nothing is blocked, nothing is opened; only the * adjacency graph everybody already walks gets one extra edge in it. That single sentence is what * makes the seam choices below forced rather than chosen, and each gate pins one of them: * * THE TRAVERSAL CANNOT BE A legalAdd. The oracle enumerates candidates by walking the four * direction vectors off the walker's cell (engine.js:7540-7541) and parkStep recomputes the * destination from the vector again (8091-8092), so a non-adjacent cell is not expressible as a * candidate at all. The only seam that can move a man to a far cell is onEnter's st.pos[0] * relocation (contract 7315-7318), and P.path records where he ACTUALLY stands (8162). * * THE ORACLE STILL HAS TO WANT IT. A relocation the route metric cannot see is a free lunch * nobody orders, so the exit's price is handed to the entry through oracleCost — a DISCOUNT and * never a penalty, because a candidate priced Infinity is a dead move (7941-7947). * * THE COMPANION CANNOT PLAN THROUGH IT EITHER (his BFS is adjacency too, 7601-7616), so he is * carried by tick, on a cooldown, exactly as y3's crawl carries a downed man. */ const _Y32_SEEDS = [1, 7, 42, 4149]; /* ==== Y32-GATES-END ==== */ /* ==== FIELD-REGISTRY-SURFACE (2026-07-23) — every seated mechanic must expose the CAMPAIGN * surface, and it must be alive. * * WHY THIS EXISTS. y32 warp shipped its module gates green with `_parkWarpAdmissible` at 40/40 over * seeds 1..40 — and the crossing layer could not measure the cell at all, because the registry entry * never wired `admits`. `_parkFieldCellAdmits` opens with `!!(m && m.admits)`, so a missing key is * not a crash: every candidate is silently skipped, the sweep exhausts, `filtered` comes back false, * and the anti-mimic control reports 0 probes. "0 leaks" off a loop that never ran. * * NEITHER EXISTING GATE COULD SEE IT. The module's own gates call `_parkWarpAdmissible` DIRECTLY, * never through the registry, so they prove the predicate and not the wiring. CAMP-CROSS-SWEEP only * sweeps SHIPPED slots — a preview is counted as "non-shipped (unswept)" — so the whole preview * shelf was outside its reach. This gate walks the registry the way campaign.js walks it. * * Two claims, and the second is the one with teeth: the surface EXISTS, and it ACCEPTS something. * An `admits` wired to a predicate that is false everywhere fails exactly like a missing one. */ test('FIELD-REGISTRY-SURFACE: every field mechanic a crossing slot names exposes cell+admits, and the registry path actually accepts cells', () => { const named = [...new Set(CAMP.PARK_CROSSINGS .filter(c => c.playMech && c.playMech.fieldMech).map(c => c.playMech.fieldMech))]; assert.ok(named.length >= 21, `only ${named.length} field mechanics are named by slots — the roster shrank`); const rows = []; for (const fv of named) { const m = E.PARK_FIELD_MECHS[fv]; assert.ok(m, `slot names field mechanic '${fv}' but nothing is registered under it`); assert.strictEqual(typeof m.cell, 'function', `${fv} has no cell() — the campaign cannot mint its play cell`); assert.strictEqual(typeof m.admits, 'function', `${fv} has no admits() — _parkFieldCellAdmits opens with !!(m && m.admits), so EVERY candidate ` + 'is skipped, the play-cell sweep exhausts, and the pairing measurement reports zero probes ' + 'instead of failing loudly. This is the y32 defect; it was invisible to the module gates ' + 'because they call the predicate directly rather than through this registry.'); // ALIVE, not merely present: a predicate false everywhere breaks the sweep the same way. // The range is seeds 1..40 because that is the sweep every module's own STOP gate reports on, // and some cells are legitimately SPARSE — y23 storm admits 6 of 40 and none of the first // eight, so a tighter bound would condemn a board that works. Short-circuits on the first // acceptance, so a healthy mechanic costs one seed. let ok = 0, at = -1; for (let s = 1; s <= 40 && ok === 0; s++) if (m.admits(m.cell(s))) { ok++; at = s; } assert.ok(ok > 0, `${fv}.admits accepted none of its own cells over seeds 1..40 — the registry ` + 'surface is wired but dead, which the crossing sweep cannot tell apart from missing'); rows.push(at > 1 ? `${fv}@${at}` : fv); } console.log(` [FIELD-REGISTRY-SURFACE] ${rows.length} mechanics carry a live cell+admits surface`); }); /* ==== Y29/Y31/Y32-SHIP-GATE (2026-07-23) — the three new crossings are PREVIEWS, and these gates * are what make that word mean something. `ship:false` asserted by hand is an opinion; here the flag * is pinned to the MEASUREMENT, so the day one of these boards starts recovering its pairing the * gate goes red and forces the flip to be deliberate. * * The anti-mimic control is reported SEPARATELY from the pairing verdict on purpose. * `_parkYNNRecovers` short-circuits on the first persona that fails, so its mimic leg may never run * — a slot could report "no leaks" having probed nothing. `_parkYNNMimicLeaks` runs the mimic leg * unconditionally and reports `probes` next to `leaks`, and the gate refuses a zero-probe report. * That is not hypothetical: y32 reported exactly zero probes until 5452ef6, because its registry * entry was missing `admits` and the play-cell sweep had been exhausting in silence. */ /* y29 and y31 left this table with their crossing SLOTS on 2026-07-29 (m1 x CN 0.56 and 0.67 * against a 0.80 bar, redundant on GC/GN, and skip-independent — separation map 2026-07-29). * Their rows could not stay: CAMP.PARK_Y{29,31}_SHIPPABLE and CAMP._parkY{29,31}MimicLeaks are * gone from campaign.js's exports, and `assert.ok(cx, ...)` below wants a row in PARK_CROSSINGS. * The MODULE gates Y29-STATUE-* (7) and Y31-RELAY-* (7) are untouched and still run — that is the * separation, and the shipped y46 siege board depends on the statue module at runtime. * NOTE this block is DUPLICATED verbatim further down the file (merge damage, 2026-07-29 finding): * both copies execute, so every edit here must be applied to BOTH. */ for (const [id, ship, mod, seed, mimic] of [ ]) { test(`${id.toUpperCase()}-SHIP-GATE: ${id} stays a preview until its pairing measurement says otherwise, and the anti-mimic control reports its probes`, () => { const cx = CAMP.PARK_CROSSINGS.find(c => c.id === id); assert.ok(cx, `${id} slot must exist in PARK_CROSSINGS`); assert.strictEqual(cx.ship, ship(), `${id}.ship=${cx.ship} disagrees with its pairing measurement ${ship()} — the picker flag and ` + 'the measurement drifted apart. Flip the flag only WITH the measurement, never ahead of it.'); assert.strictEqual(cx.open, true, 'a non-shipped slot must carry the preview mark, or it reads as inert'); const mim = mimic(); assert.ok(mim.probes > 0, `the anti-mimic control reported ${mim.leaks} leaks over ${mim.probes} probes — zero probes means ` + 'the loop never ran, and "0 leaks" off a loop that never ran is not a measurement (see 5452ef6)'); assert.ok(mim.leaks <= mim.expressed, 'leaks cannot exceed the pairs the mimic even poses'); console.log(` [${id.toUpperCase()}-SHIP-GATE] pairing=${ship()} (preview) · module=${mod()} · ` + `mimic probes=${mim.probes} expressed=${mim.expressed} leaks=${mim.leaks} @seed ${seed()}`); }); } /* ============ Y33 THE FOOTBRIDGE (외나무다리) — the purpose-built C-vs-N gates (2026-07-23) ====== * Two gates. CN-FIELD-PAIRS pins the ACCOUNTING extension (parkPosedPairs — kind signature pairs * unioned with a field module's DECLARED posed pairs; no declaration => byte-identical, so every * pre-y33 slot's sweep reads exactly what it read before). Y33-YIELD-SHIP-GATE measures the cell * itself, and its headline INVERTS the y12/y14 tripwires: those gates assert posed.CN === 0 and * rec === 0 (the standing C-N gap, honestly recorded); this one REQUIRES posed.CN > 0 on every * persona — strictly post-calibration (the y8 lesson) — and CALIBRATED blind order recovery 6/6 * on every admitted cell. The bar rides admits() itself (the module's admission already contains * it), and the gate re-asserts the headline independently so a quiet admits() edit cannot lower * the bar without going red here. y33 stays ship:false open:true regardless: admission is the * MODULE bar; the ship bar is the full crossing suite (pairing, mimic control, diverse-path * widening) and belongs to the session that measures it (the warp precedent). */ test('CN-FIELD-PAIRS: parkPosedPairs = kind pairs ∪ declared field pairs; no declaration => byte-identical', () => { // (a) undeclared identity — for every kind, with no field / an unknown field / a field with no // declaration, the union IS the kind signature (the no-op guarantee every pre-y33 slot rides). for (const kind of ['m1', 'm2', 'm3', 'm4', 'm5']) { assert.deepStrictEqual(E.parkPosedPairs(kind), E._parkKindPair(kind), `${kind}: bare`); assert.deepStrictEqual(E.parkPosedPairs(kind, 'no_such_mech'), E._parkKindPair(kind), `${kind}: unknown mech`); assert.deepStrictEqual(E.parkPosedPairs(kind, 'stones'), E._parkKindPair(kind), `${kind}: undeclared mech`); } // (b) declaration audit — yield is the ONLY module that declares pairs today. A new declaration // must be a deliberate, NAMED addition here (it changes what a slot's sweep reads). const declared = Object.keys(E.PARK_FIELD_MECHS).filter(id => E.PARK_FIELD_MECHS[id].pairs); assert.deepStrictEqual(declared, ['yield'], 'a module declared posed pairs without being pinned here'); // (c) the union, ordered kind-first, duplicates dropped: m1 (G-C) + yield's [G-N, C-N]. assert.deepStrictEqual(E.parkPosedPairs('m1', 'yield'), [['G', 'C'], ['G', 'N'], ['C', 'N']], 'm1+yield must pose all three pairs, kind signature first'); // (d) the declaration is backed by the module gate below (expressed > 0 for all three pairs on // every admitted cell) — never taken on faith. console.log(' [CN-FIELD-PAIRS] undeclared identity holds on all kinds; declared: yield -> GN+CN'); }); const _Y33_BASE_SEEDS = [1, 2, 3, 4, 5, 6, 7, 8]; const _Y33_STRIDE = 7907; // the campaign sweep's own stride const _Y33_BUDGET = 128; // and its own candidate budget test('YIELD-MODULE-GATE: every base seed sweeps to an admitted cell; on it, all three pairs pose (C-N post-calibration) and CALIBRATED blind order recovery is 6/6', () => { const whys0 = E.parkYieldWhys(); const pairs = [['G', 'C'], ['G', 'N'], ['C', 'N']]; let cells = 0, rec = 0, tot = 0, misPair = 0, sweepMax = 0; const posed = { GC: 0, GN: 0, CN: 0 }; for (const base of _Y33_BASE_SEEDS) { // the campaign's own acceptance walk (stride/budget verbatim): first ADMITTED candidate. let cell = null, at = -1; for (let t = 0; t < _Y33_BUDGET && !cell; t++) { const cand = E._parkYieldCell((base + t * _Y33_STRIDE) >>> 0); if (E._parkYieldAdmissible(cand)) { cell = cand; at = t; } } assert.ok(cell, `base seed ${base}: no admitted cell within the campaign budget (${_Y33_BUDGET}) — ` + 'the preview would seat an unmeasured candidate'); sweepMax = Math.max(sweepMax, at); cells++; // fresh-board discipline: a built board starts un-yielded, stamped, dyn-opted-in. const st = E.parkFieldBuild(cell); assert.strictEqual(st.park.fieldMech, 'yield', 'the board must stamp its registry key'); assert.strictEqual(st.park.dyn.yields, 0, 'a fresh board has no yields'); // the headline, re-asserted INDEPENDENTLY of admits (see the header): six faithful playouts, // all alive, all three pairs expressed, C-N strictly post-calibration, calibrated order 6/6. for (let i = 0; i < E.PARK_PERSONAS.length; i++) { const persona = E.PARK_PERSONAS[i]; const P = E.parkPlayout(E._parkYieldBuild(cell), persona); assert.strictEqual(P.reason, 'complete', `seed ${cell.seed} ${persona.join('>')}: did not complete`); assert.ok(P.hearts >= 1, `seed ${cell.seed} ${persona.join('>')}: dead`); tot++; const cn = P.awards.filter(w => w.pair === 'CN'); assert.ok(cn.length > 0, `seed ${cell.seed} ${persona.join('>')}: C-N never posed — the cell's whole ambition`); assert.ok(cn.every(w => w.turn > E.PARK_CAL_TURNS), `seed ${cell.seed} ${persona.join('>')}: C-N evidence landed inside the calibration span (the y8 defect)`); for (const pair of pairs) { if (!(E.parkPairExpressed(E._parkYieldBuild(cell), P.moves, pair) > 0)) continue; posed[pair.join('')]++; const expect = E._parkPushPairDir(persona, pair); if (!E.parkRecoverPairLex(E._parkYieldBuild(cell), P.moves, pair, { expect }).recovered) misPair++; } const r = E.parkRecoverOrder(E._parkYieldBuild(cell), P.moves, E.PARK_CAL_TURNS); if (r && r.join() === persona.join()) rec++; } } assert.strictEqual(misPair, 0, 'a posed pair was blind-recovered in the WRONG direction'); assert.strictEqual(rec, tot, `calibrated blind order recovery ${rec}/${tot} — the admitted footbridge must recover 6/6 on every cell ` + '(this is the bar y12/y14 measured 0/144 on; an admitted cell that misses it is a filter defect)'); assert.strictEqual(posed.CN, tot, 'C-N must be expressed on EVERY persona\'s own faithful path'); assert.ok(posed.GC === tot && posed.GN === tot, 'G-C and G-N must also be expressed on every path'); // the sweep's reject telemetry: candidates may honestly fail cnearly/unexpressed/sig, but a // candidate that DIES or CAPS OUT is a geometry defect, not a timing miss — pinned at zero. const whys = E.parkYieldWhys(); assert.strictEqual(whys.complete - whys0.complete, 0, 'a swept candidate capped out / failed to complete'); assert.strictEqual(whys.dead - whys0.dead, 0, 'a swept candidate died'); assert.strictEqual(whys.norec - whys0.norec, 0, 'a swept candidate expressed a pair the blind read then missed'); // y33 의 슬롯 단언은 그 행과 함께 2026-08-03 에 나갔다. 남은 것은 MODULE 게이트다 — // yield 모듈 자체는 살아 있고, 라이브 xs 가 그 위에 앉아 있기 때문이다. 이 게이트를 통째로 // 지웠다면 살아 있는 모듈이 무검증으로 남았을 것이다. console.log(` [YIELD-MODULE-GATE] ${_Y33_BASE_SEEDS.length} base seeds -> ${cells} admitted cells ` + `(sweep max ${sweepMax}): faithful ${tot}/${tot} alive · GC ${posed.GC}/${tot} GN ${posed.GN}/${tot} ` + `CN ${posed.CN}/${tot} (post-cal) · calibrated order ${rec}/${tot} · misPair ${misPair}`); }); /* ---- PARK-CHAIN-ANYOF — 체인 한 단계를 여러 토큰 중 하나로 만족시키는 옵트인 (2026-08-03). * y46 v2 의 네 안전지대는 "넷 중 아무 데나 들어가면 그 판은 끝난다"라서 고정 토큰 하나로는 * 표현이 안 된다. 기계의 진짜 접합부는 _parkChainDone 이 아니라 커서를 굴리는 두 곳이다: * _parkAdvanceDest — 목록 중 하나라도 수확됐으면 커서를 전진시킨다 * _parkDestCell — 살아 있는 후보 중 가장 가까운 것을 겨냥한다 (collect 변형의 관용구, * 공개 상태만 읽으므로 C1 유지) * 커서만 옳게 굴러가면 _parkChainDone 은 한 글자도 안 고쳐도 된다. * 이 테스트는 v1 기하 위에서 기계만 검증하므로 y46 기하 교체(Task 2) 뒤에도 살아남는다. */ test('PARK-CHAIN-ANYOF: 목록 중 아무 토큰이나 하나면 그 단계는 끝난다; 필드가 없는 보드는 바이트 동일', () => { // (a) 회귀 방어가 먼저다 — 옵트인하지 않은 보드에서 엉뚱한 토큰을 죽여도 커서는 안 움직인다. const plain = E._parkSiegeBuild({ seed: 7 }); const Pp = E.parkStart(plain); assert.strictEqual(plain.park.chainAnyOf, undefined, '옵트인 안 한 보드가 필드를 갖고 있으면 안 된다'); assert.deepStrictEqual(plain.park.chain, [0], 'v1 의 체인은 결승 토큰 하나'); assert.strictEqual(Pp.dest, 0); plain.tokens[1].alive = false; // 체인에 없는 토큰(분홍의 보석)을 죽인다 E.parkStep(Pp, 'stay'); assert.strictEqual(Pp.dest, 0, '체인 밖 토큰이 죽어도 커서는 그대로 — 예전 동작 그대로'); // (b) 옵트인하면 목록의 아무 토큰이나 하나로 그 단계가 끝난다. const st = E._parkSiegeBuild({ seed: 7 }); st.park.chainAnyOf = [[0, 1]]; // 단계 0 은 토큰 0 또는 1 로 만족 const P = E.parkStart(st); assert.strictEqual(P.dest, 0); st.tokens[1].alive = false; // chain[0] 은 토큰 0 인데 토큰 1 을 먹었다 E.parkStep(P, 'stay'); assert.strictEqual(P.dest, 1, '목록의 다른 토큰을 먹어도 단계는 끝나야 한다'); assert.ok(P.dest >= st.park.chain.length, '체인이 끝났다'); // (c) 목록 중 아무것도 안 죽었으면 커서는 안 움직인다 (공허하지 않은 조건인지 확인). const st2 = E._parkSiegeBuild({ seed: 7 }); st2.park.chainAnyOf = [[0, 1]]; const P2 = E.parkStart(st2); E.parkStep(P2, 'stay'); assert.strictEqual(P2.dest, 0, '아무도 안 먹었는데 단계가 끝나면 그 조건은 공허하다'); }); test('Y46-SIEGE-BUILD: seed-pure; rear bands precomputed and disjoint from every errand cell; spawn and station ARE threatened; clock reads pure', () => { const a = E._parkSiegeBuild({ seed: 7 }), b = E._parkSiegeBuild({ seed: 7 }); assert.strictEqual(JSON.stringify(a.park.statue), JSON.stringify(b.park.statue), 'build must be deterministic (statue fixture)'); assert.strictEqual(JSON.stringify(a.park.siege), JSON.stringify(b.park.siege), 'build must be deterministic (bands)'); const sides = new Set(); for (let s = 1; s <= 12; s++) { const st = E._parkSiegeBuild({ seed: s }); sides.add(st.park.statue.side); const n = st.N, K = (p) => p.y * n + p.x; const band = new Set(st.park.siege.bands.flat()); assert.strictEqual(st.park.siege.bands.length, E.PARK_SIEGE_RINGS, 'one band per wave'); for (const bd of st.park.siege.bands) assert.ok(bd.length > 0, `seed ${s}: an empty band is a wave that never was`); // THE DRY GUARANTEE — the whole route-mask-Infinity defence is structural, so pin it per seed: // no band cell may ever be the finish, a token, the retire seat, or the mate's lane. assert.ok(!band.has(st.park.statue.finishKey), `seed ${s}: the finish would drown`); for (const t of st.tokens) assert.ok(!band.has(t.y * n + t.x), `seed ${s}: a token would drown`); assert.ok(!band.has(K(st.park.retire)), `seed ${s}: the retire seat would drown`); // laneKeys[0] IS the station — it sinks on wave 3 BY DESIGN (he leaves on turn one); what must // stay dry is his gem, laneKeys[1], the cell his whole errand banks on. assert.ok(!band.has(st.park.statue.laneKeys[1]), `seed ${s}: the mate gem would drown`); // ...and the threat is REAL: both spawns start on ground the sea will take. assert.ok(band.has(K(st.park.spawn)), `seed ${s}: the walker's spawn is never threatened — the wave is scenery`); assert.ok(band.has(K(st.park.companionSpawn)), `seed ${s}: the station is never threatened`); // dyn members all exist at build (the deep-clone rule), and the sea starts at rest. assert.strictEqual(st.park.dyn.siege.gone.size, 0); assert.strictEqual(st.park.dyn.siege.ringsGone, 0); assert.strictEqual(st.park.dyn.statue.gazePrev, false, 'beat 0 is a song beat'); assert.strictEqual(E._parkSiegeGazing(st), false); assert.strictEqual(E._parkSiegeTillGaze(st), E.PARK_SIEGE_SING); } assert.ok(sides.size >= 2, 'the doll face is seed-drawn (anti-mimic axis), not a constant'); // the two clocks are ONE clock — the phase alignment is the admission's lifeline, so pin it. assert.strictEqual(E.PARK_SIEGE_EVERY % E.PARK_SIEGE_PERIOD, 0, 'EVERY must stay a multiple of the statue period — mis-phased clocks were the y29 dead-collapse lesson'); }); test('Y46-SIEGE-SINK: the wave breaks when the beat reaches EVERY (a song beat); sunk ground walls all three domains; a caught body is shoved and billed on its own log', () => { const st = E._parkSiegeBuild({ seed: 42 }); const P = E.parkStart(st), dyn = st.park.dyn, M = E.PARK_FIELD_MECHS.siege, n = st.N; for (let i = 0; i < E.PARK_SIEGE_EVERY; i++) E.parkStep(P, 'stay'); // waiting is legal and gaze-innocent assert.strictEqual(dyn.beat, E.PARK_SIEGE_EVERY); assert.strictEqual(dyn.siege.ringsGone, 1, 'the first wave breaks exactly when the beat reaches EVERY'); assert.strictEqual((E.PARK_SIEGE_EVERY % E.PARK_SIEGE_PERIOD), 0); assert.strictEqual(E._parkSiegeGazing(st), false, 'the sink lands on a song beat — ground only dies when he may move'); for (const k of st.park.siege.bands[0]) { assert.ok(dyn.siege.gone.has(k), 'band 0 fully sunk'); for (const who of ['me', 'mate', 'route']) assert.strictEqual(M.legalMask(P, k, who), true, `sunk ground must wall the '${who}' domain`); } assert.strictEqual(P.hearts, 3, 'nobody stood in the water: waiting at a dry spawn cell costs nothing'); // note the spawn CAN be band 0 or band 1 depending on the lateral draw — seed 42's walker sat dry // through wave 1; the caught-body physics is constructed directly below, the y29 hooks idiom. // THE CAUGHT BODIES, constructed: put both on band-1 cells and let wave 2 break. const b1 = st.park.siege.bands[1].filter(k => !dyn.siege.gone.has(k)); assert.ok(b1.length >= 2, 'need two dry band-1 cells to stage the catch'); st.pos[0] = { x: b1[0] % n, y: (b1[0] / n) | 0 }; st.pos[1] = { x: b1[1] % n, y: (b1[1] / n) | 0 }; dyn.statue.matePrev = { x: st.pos[1].x, y: st.pos[1].y }; // he did not walk; the sea moves him dyn.beat = 2 * E.PARK_SIEGE_EVERY; // as the beat the wave-2 tick would see const h0 = P.hearts, sw0 = dyn.siege.swept.length, ms0 = dyn.siege.mateSwept, mc0 = dyn.statue.mateCaught; M.tick(P); assert.strictEqual(dyn.siege.ringsGone, 2, 'wave 2 broke'); assert.strictEqual(P.hearts, h0 - 1, 'the walker paid ONE heart to the sea'); assert.strictEqual(dyn.siege.swept.length, sw0 + 1, 'the sweep is confessed on its OWN log'); assert.strictEqual(dyn.statue.caught.length, 0, '...never on the gaze log (channel purity, spec §5)'); assert.strictEqual(dyn.siege.mateSwept, ms0 + 1, 'the companion was swept too'); assert.strictEqual(dyn.statue.mateStun, E.PARK_SIEGE_STUN, 'a swept companion is stunned, not judged'); assert.strictEqual(dyn.statue.mateCaught, mc0, 'the sea moving him is NOT him walking under the gaze'); const meK = st.pos[0].y * n + st.pos[0].x, coK = st.pos[1].y * n + st.pos[1].x; assert.ok(!dyn.siege.gone.has(meK) && !st.wall.has(meK), 'the walker stands on dry ground'); assert.ok(!dyn.siege.gone.has(coK) && !st.wall.has(coK), 'the companion stands on dry ground'); assert.notStrictEqual(meK, coK, 'the shove never stacks the two bodies'); }); test('Y46-SIEGE-GAZE: the toll splits — the STARE bills, the head-TURN is a free telegraph, stay and the song are innocent', () => { /* PROMOTION 2026-07-26 REWRITE, and the old pin is quoted here so the change is legible rather * than silent. This gate used to assert `dyn.beat = PARK_SIEGE_SING` (the FIRST looking beat) * bills a heart. It no longer does, ON PURPOSE: the gaze now has two beats with different jobs — * the doll TURNS, then STARES — and only the stare bills (_parkSiegeStare). That split is what * bought this cell its ship bar: with both beats billing, every read-window configuration that * staged the C-N pair killed the goal>care>safety walker on his third crossing (8 of 48 runs; * admission 0/40). With the split, module order recovery is 144/144 over seeds 1..24 x 6 personas * at BOTH skip 2 and skip 0, admission 40/40, reject histogram all zero. So the gate now pins the * SPLIT itself in both directions: free on the turn, billed on the stare. */ // DIRECT-HOOK STAGING (the y29 hooks idiom), not a walked run: a walked stage let the 6th step // coincide with the first wave, and the toll and the sweep double-billed the same step — which is // correct physics (two channels, two logs) but a muddled stage for pinning the toll ALONE. So the // clock is set by hand and the step happens on dry, shadow-free forward ground. const st = E._parkSiegeBuild({ seed: 7 }); const P = E.parkStart(st), dyn = st.park.dyn, M = E.PARK_FIELD_MECHS.siege, n = st.N; const band = new Set(st.park.siege.bands.flat()); let from = null, to = null; outer: for (let y = 1; y < n - 1; y++) for (let x = 1; x < n - 2; x++) { const a = y * n + x, b = y * n + x + 1; if (st.wall.has(a) || st.wall.has(b) || band.has(a) || band.has(b)) continue; if (E._parkSiegeShadow(st, a) || E._parkSiegeShadow(st, b)) continue; from = { x, y }; to = { x: x + 1, y }; break outer; } assert.ok(from, 'no dry shadow-free adjacent pair found — the yard shape changed under this gate'); st.pos[0] = { ...from }; const fk = from.y * n + from.x, tkey = to.y * n + to.x; // (1) THE TURN IS FREE — the same step, on the first looking beat, bills nothing and logs nothing. dyn.beat = E.PARK_SIEGE_SING; // the head-turn beat assert.strictEqual(E._parkSiegeGazing(st), true, 'the turn beat is inside the gaze span'); assert.strictEqual(E._parkSiegeStare(st), false, 'the turn beat is not the stare'); M.onLeave(P, { mvKey: 'R', from, to, fromKey: fk, toKey: tkey }); assert.strictEqual(P.hearts, 3, 'walking through the head-turn is free — it is a telegraph, not a look'); assert.strictEqual(dyn.statue.caught.length, 0, 'and it leaves nothing on the gaze log'); // (2) THE STARE BILLS — same step, last looking beat. dyn.beat = E.PARK_SIEGE_PERIOD - 1; // the stare beat assert.strictEqual(E._parkSiegeStare(st), true); M.onLeave(P, { mvKey: 'R', from, to, fromKey: fk, toKey: tkey }); assert.strictEqual(P.hearts, 2, 'a body still moving when the stare lands costs a heart'); assert.strictEqual(dyn.statue.caught.length, 1); assert.strictEqual(dyn.statue.caught[0].beat, E.PARK_SIEGE_PERIOD - 1, 'billed against the beat shown at decision time'); const tk = tkey; M.onLeave(P, { mvKey: 'stay', from: to, to, fromKey: tk, toKey: tk }); assert.strictEqual(P.hearts, 2, 'holding still under the stare is the whole game — always innocent'); assert.strictEqual(dyn.statue.caught.length, 1); dyn.beat = 0; // a song beat M.onLeave(P, { mvKey: 'L', from: to, to: from, fromKey: tk, toKey: from.y * n + from.x }); assert.strictEqual(P.hearts, 2, 'the song is free'); assert.strictEqual(dyn.statue.caught.length, 1); }); test('Y46-SIEGE-ADMIT: measured seeds admit with a clean reject histogram (full sweep 40/40 over seeds 1..40, 2026-07-25)', () => { const w0 = E.parkSiegeWhys(); for (const s of [7, 21]) { assert.strictEqual(E._parkSiegeAdmissible(E._parkSiegeCell(s)), true, `seed ${s} must admit`); // every playout on an admitted seed met the wave: the hybrid clause is live, not decorative. const probe = E._parkSiegePlay(E._parkSiegeCell(s), E.PARK_PERSONAS[0]); assert.ok(probe._siegeRings >= 1, `seed ${s}: the run ended before the first wave — noring should have fired`); } const w1 = E.parkSiegeWhys(); for (const k of Object.keys(w1)) assert.strictEqual(w1[k] - w0[k], 0, `an admitted seed bumped whys.${k} — the histogram and the verdicts disagree`); }); /* ---- GATE: Y46-SIEGE-SHIP-GATE — the promotion, derived on BOTH bars (2026-07-26) ---- * y46 is the first HYBRID cell to ship, and shipping is two measurements, not one: * MODULE bar E.PARK_SIEGE_SHIPPABLE — calibrated 6/6 blind order recovery on the module's own * shipped-seed cell (PARK-SHIP-BAR-CALIBRATED cross-checks the calibration itself). * PAIRING bar CAMP.PARK_Y46_SHIPPABLE — the crossing's own board: faithful 6/6 AND the surface * mimic recovering NOTHING. The two are DIFFERENT BOARDS (the y29 confusion, named * again here so nobody re-conflates them). * Both are derived calls, never literals, and the slot flag must equal the pairing measurement — * that equality is what makes `ship: true` a fact about evidence rather than a flag somebody set. * NON-VACUITY: the mimic leg must have PROBED (probes > 0). A short-circuiting recovery that never * reached the mimic would report "0 leaks" having tested nothing, which is the exact trap the * _parkYNNMimicLeaks / _parkYNNRecovers pair exists to keep open. */ test('Y46-SIEGE-SHIP-GATE: both bars are derived, both hold, and the slot flag equals the pairing measurement', () => { const slot = CAMP.PARK_CROSSINGS.find(c => c.id === 'y46'); assert.ok(slot, 'the y46 slot vanished from the registry'); assert.strictEqual(slot.ship, CAMP.PARK_Y46_SHIPPABLE, 'the y46 slot flag and its PAIRING measurement disagree — ship:true is earned by the measurement ' + 'or it is not earned at all (derive-never-assert)'); assert.strictEqual(slot.ship, true, 'y46 ships as of 2026-07-26 (pairing 24/24, mimic 0 leaks)'); assert.ok(!slot.open, 'a measured crossing is not a marked preview — `open` must be gone, not false'); assert.strictEqual(slot.kind, 'm3', 'y46 is measured on C-vs-N, the pair its CLOCK separates (kind m3). It was declared m1 until ' + '2026-07-26 and the pairing bar then measured goal-vs-safety — the one conflict this yard poses ' + 'by accident, and the one a surface imitator cannot lose on.'); assert.strictEqual(E.PARK_SIEGE_SHIPPABLE, true, 'the MODULE bar must hold too, and it is derived'); assert.strictEqual(E._parkSiegeRecovers(E._parkSiegeCell(E.PARK_SIEGE_SHIP_SEED)), E.PARK_SIEGE_SHIPPABLE, 'PARK_SIEGE_SHIPPABLE is not what its own predicate returns — somebody pinned a literal'); // the pairing sweep + the anti-mimic control, recomputed here rather than trusted let pair = 0; const mim = { probes: 0, expressed: 0, leaks: 0 }; for (let s = 1; s <= 8; s++) { if (CAMP._parkY46Recovers(s)) pair++; const m = CAMP._parkY46MimicLeaks(s); mim.probes += m.probes; mim.expressed += m.expressed; mim.leaks += m.leaks; } assert.strictEqual(pair, 8, `y46 pairing recovery is ${pair}/8 on seeds 1..8 (measured 24/24 over 1..24)`); assert.ok(mim.probes > 0, 'NON-VACUOUS: the mimic leg never probed, so "0 leaks" would prove nothing'); assert.strictEqual(mim.leaks, 0, `the surface mimic recovered ${mim.leaks} of ${mim.probes} probes. Measured 0 at promotion, with ` + 'expressed 0 as well — the copier does not even land where the pair is posed, which is the same ' + 'signature y22 and y26 ship with. A leak here means the cell is measuring imitation again.'); console.log(` [Y46-SIEGE-SHIP-GATE] module pin ${E.PARK_SIEGE_SHIPPABLE} (seed ${E.PARK_SIEGE_SHIP_SEED}), ` + `pairing ${pair}/8, mimic probes ${mim.probes} expressed ${mim.expressed} leaks ${mim.leaks}`); }); /* ==== Y46-GATES-END ==== */ /* y50 골목 질주 (alley) — the MODULE gates (push×bull ladder-maze hybrid, plan 2026-07-25). * The seating gates (FIELD_SHIPPABLE / PIN / CROSS-DEMO-SEAM / fieldChecked) live with the other * slots; what is pinned HERE is the module's own physics and its two measured design answers: * the care mind finally REACHES the interpose (y25's STOP-gate geometry bound, dist 0 on every * seed), and the crate is a real, working shield whose faithful-persona unreachability is * confessed in the module header rather than discovered by a reviewer. */ test('Y50-ALLEY-BUILD: seed-pure; ladder geometry constructs every separator (aligned-null spawn, notch order, shove lands on the street west of the crossing) on seeds 1..40', () => { for (let s = 1; s <= 40; s++) { const a = E._parkAlleyBuild(E._parkAlleyCell(s)); const b = E._parkAlleyBuild(E._parkAlleyCell(s)); assert.strictEqual(JSON.stringify(a.park.alley), JSON.stringify(b.park.alley), `seed ${s}: build must be deterministic`); const n = a.N, al = a.park.alley; // the aim logic's calibration guard: nobody is on a bull lane before anyone has moved assert.strictEqual(E._parkAlleyAligned(a), null, `seed ${s}: a body is aligned at spawn`); // the notch order that makes the crate a SHIELD: its landing sits between bull and crossing assert.ok(al.notch < al.cross, `seed ${s}: crate notch must be west of the crossing`); assert.strictEqual(E._parkAlleyPushDest(a, al.stand, al.crateSpawn), 6 * n + al.notch, `seed ${s}: the north shove must land the crate on the street`); const street = E._parkAlleyLane(a, a.park.bull.home, 3); assert.ok(street.indexOf(6 * n + al.notch) !== -1, `seed ${s}: the landing must be ON the east lane`); assert.ok(street.indexOf(6 * n + al.cross) !== -1, `seed ${s}: the crossing must be ON the east lane`); // the STOP-gate fix is a PLACEMENT: the spawn's south neighbour is a street cell strictly // west of the crossing — one step from the interpose segment of every mate-aim scene. const sp = a.park.spawn; assert.strictEqual(sp.y, 5, `seed ${s}: spawn leaves the north street row`); assert.ok(street.indexOf(6 * n + sp.x) !== -1 && sp.x < al.cross, `seed ${s}: spawn's south neighbour must be an interpose cell`); // the pen: an alley bull aims down the street axis only — a body on the home pen's short // column stub must NOT align it (the migration that killed safety personas 3-block deaths). a.pos[0] = { x: 1, y: 5 }; assert.strictEqual(E._parkAlleyAligned(a), null, `seed ${s}: the north stub must not aim`); a.pos[0] = { x: 3, y: 6 }; const al2 = E._parkAlleyAligned(a); assert.ok(al2 && al2.dirIdx >= 2, `seed ${s}: a street body aims east/west only`); } }); test('Y50-ALLEY-TICK: the resolution scan — a crate on the locked lane shields for free, a walker pays a heart, a companion goes down in the y3 shape', () => { const n = E.PARK_ALLEY_N, K = (x, y) => y * n + x; { // THE SHIELD (the module's one new clause): crate shoved onto the stored lane mid-fuse. const st = E._parkAlleyBuild(E._parkAlleyCell(3)); const P = E.parkStart(st), dyn = st.park.dyn, M = E.PARK_FIELD_MECHS.alley; const al = st.park.alley, B = dyn.ents[0]; st.pos[1] = { x: al.cross, y: 6 }; // the companion's one street beat M.tick(P); assert.ok(B.aim && B.aim.targetWho === 'mate' && B.aim.fuse === E.PARK_ALLEY_TELEGRAPH, 'the crossing must telegraph a mate-targeted aim'); st.pos[1] = { x: al.cross, y: 7 }; // he steps off during the fuse (y25's point) dyn.alley.crate = K(al.notch, 6); // the human's play, staged directly const h0 = P.hearts, s0 = dyn.alley.shields, b0 = dyn.alley.blocks; M.tick(P); M.tick(P); assert.ok(B.aim, 'the fuse must still be burning after two beats'); M.tick(P); assert.strictEqual(B.aim, null, 'the charge resolved'); assert.strictEqual(dyn.alley.shields - s0, 1, 'the crate confesses the block on its OWN log'); assert.strictEqual(dyn.alley.blocks - b0, 0, 'no body was hit'); assert.strictEqual(P.hearts, h0, 'a crate-block is FREE — that is what the labour bought'); assert.strictEqual(B.key, K(al.notch - 1, 6), 'the bull stops one cell short of the crate'); assert.strictEqual(B.stunned, E.PARK_ALLEY_STUN, 'and shakes it off'); } { // THE BODY-BLOCK, y25 verbatim: a walker standing on the stored lane at resolve pays ♥1. const st = E._parkAlleyBuild(E._parkAlleyCell(3)); const P = E.parkStart(st), dyn = st.park.dyn, M = E.PARK_FIELD_MECHS.alley; const B = dyn.ents[0]; st.pos[0] = { x: 5, y: 6 }; M.tick(P); assert.ok(B.aim && B.aim.targetWho === 'me', 'a street walker aims the bull at himself'); const h0 = P.hearts, b0 = dyn.alley.blocks; M.tick(P); M.tick(P); M.tick(P); assert.strictEqual(P.hearts, h0 - 1, 'the body pays the heart'); assert.strictEqual(dyn.alley.blocks - b0, 1, 'and the block is logged'); assert.strictEqual(B.key, K(4, 6), 'the bull stops one cell short of him'); } { // THE DOWNED COMPANION: y3 shape + the cadence crawl away from the charge. const st = E._parkAlleyBuild(E._parkAlleyCell(3)); const P = E.parkStart(st), dyn = st.park.dyn, M = E.PARK_FIELD_MECHS.alley; const al = st.park.alley, B = dyn.ents[0]; st.pos[1] = { x: al.cross, y: 6 }; M.tick(P); // aim M.tick(P); M.tick(P); M.tick(P); // he stands his ground: the charge lands assert.ok(dyn.downed && dyn.downed.rescued === false && dyn.downed.crawl === 0, 'the companion goes down in the y3 shape'); assert.strictEqual(dyn.alley.downDir, 3, 'the crawl continues the charge direction'); dyn.beat = E.PARK_ALLEY_CRAWL_EVERY; // his beat comes const before = st.pos[1].x; M.tick(P); assert.strictEqual(st.pos[1].x, before + 1, 'he drags himself one cell east on cadence'); assert.strictEqual(dyn.downed.crawl, 1); } }); test('Y50-ALLEY-PUSH: the ledge action-idiom — legalAdd opens only a free shove, onEnter moves the crate and puts the pusher back, the route metric never sees the crate', () => { const n = E.PARK_ALLEY_N, K = (x, y) => y * n + x; const st = E._parkAlleyBuild(E._parkAlleyCell(5)); const P = E.parkStart(st), dyn = st.park.dyn, M = E.PARK_FIELD_MECHS.alley; const al = st.park.alley; st.pos[0] = { x: al.notch, y: 8 }; // the stand — the south wall row's own notch assert.strictEqual(M.legalAdd(P, dyn.alley.crate, 'me'), true, 'the north shove is open'); assert.strictEqual(M.legalAdd(P, dyn.alley.crate, 'mate'), false, 'the companion never shoves'); assert.strictEqual(M.legalMask(P, dyn.alley.crate, 'me'), true, 'solid to the body...'); assert.strictEqual(M.legalMask(P, dyn.alley.crate, 'route'), false, '...transparent to the metric'); const from = { x: al.notch, y: 8 }, to = { x: al.notch, y: 7 }; M.onEnter(P, { from, to, fromKey: K(from.x, from.y), toKey: K(to.x, to.y) }); assert.strictEqual(dyn.alley.crate, K(al.notch, 6), 'the crate moved one cell north'); assert.deepStrictEqual(st.pos[0], from, 'the pusher was put back — an action, not a move'); // refusals: a wall behind, the bull behind, a live gem behind — each keeps the cell shut. dyn.alley.crate = K(3, 5); st.pos[0] = { x: 3, y: 6 }; assert.strictEqual(M.legalAdd(P, dyn.alley.crate, 'me'), false, 'a wall-row cell behind refuses'); dyn.alley.crate = K(2, 6); st.pos[0] = { x: 3, y: 6 }; assert.strictEqual(M.legalAdd(P, dyn.alley.crate, 'me'), false, 'the bull behind refuses'); const bonus = st.tokens[3]; dyn.alley.crate = K(bonus.x - 1, bonus.y); st.pos[0] = { x: bonus.x - 2, y: bonus.y }; assert.strictEqual(M.legalAdd(P, dyn.alley.crate, 'me'), false, 'a live gem behind refuses — no buried chains'); }); test('Y50-MAZE-RUNNER: live play is a connected 15x15 maze; the nearest-agent bull charges then rests two turns, walks corners without rest', () => { const st = E._parkAlleyBuild({ ...E._parkAlleyCell(5), mazeRunner: true }); const P = E.parkStart(st), M = E.PARK_FIELD_MECHS.alley, n = st.N; const K = (x, y) => y * n + x, B = st.park.dyn.ents[0], D = st.park.dyn.alley; assert.strictEqual(n, 15); assert.strictEqual(st.park.alley.runner, true); const seen = new Set([K(1, 1)]), q = [K(1, 1)]; for (let h = 0; h < q.length; h++) { const key = q[h], x = key % n, y = (key / n) | 0; for (const d of [[0,-1],[0,1],[-1,0],[1,0]]) { const nk = (y + d[1]) * n + x + d[0]; if (!st.wall.has(nk) && !seen.has(nk)) { seen.add(nk); q.push(nk); } } } assert.strictEqual(seen.size, st.park.walkway.size, 'every carved maze cell is connected'); st.wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) st.wall.add(K(x, y)); /* THE CHARGE IS TELEGRAPHED, AND THAT IS THE WHOLE GAME (rewritten 2026-08-03). The old pin here asserted that ONE tick both charged and billed. It was pinning the defect: a hit computed and taken in the same beat can never be dodged, and a danger with no answer poses no comparison. Measured before the fix, on the live y50 play board: 완주 0/6 (all six personas dead, same turn, same hearts, same score) and GC 0/6 — the pair this slot is graded on never once stood. The bull now COMMITS a direction, burns PARK_ALLEY_TELEGRAPH beats with the lane lit, and re-reads the ray only when it fires. */ B.key = K(1, 1); B.stunned = 0; B.aim = null; st.pos[0] = { x: 5, y: 1 }; st.pos[1] = { x: 13, y: 13 }; const h0 = P.hearts; M.tick(P); assert.ok(B.aim, 'the bull COMMITS before it charges — no aim means the old same-beat bill is back'); assert.strictEqual(P.hearts, h0, 'the aiming beat is free'); assert.strictEqual(B.key, K(1, 1), 'and the bull has not moved yet'); M.tick(P); assert.strictEqual(P.hearts, h0, 'still free while the lane is only lit'); M.tick(P); assert.strictEqual(B.aim, null, 'the fuse expired and the charge resolved'); assert.strictEqual(P.hearts, h0 - 1, 'a body that stayed in the lit lane is hit'); assert.strictEqual(B.key, K(4, 1), 'the bull stops one cell short of what it hit'); assert.strictEqual(B.stunned, 2, 'a charge buys exactly two rest turns'); M.tick(P); assert.strictEqual(B.stunned, 1); M.tick(P); assert.strictEqual(B.stunned, 0); // AND THE DODGE — the clause the whole rewrite exists for. Same setup, but the walker leaves the // ray while the fuse burns; the bull runs the lane out and hits nothing. If this ever fails, the // ray is being read at AIM time instead of at FIRE time and the board is a tax again. B.key = K(1, 1); B.stunned = 0; B.aim = null; st.pos[0] = { x: 5, y: 1 }; st.pos[1] = { x: 13, y: 13 }; const h1 = P.hearts; M.tick(P); assert.ok(B.aim, 'the bull committed'); st.pos[0] = { x: 5, y: 3 }; // step out of row 1 while the lane is lit M.tick(P); M.tick(P); assert.strictEqual(B.aim, null, 'the charge resolved'); assert.strictEqual(P.hearts, h1, 'a walker who left the lane pays nothing — this is the decision'); B.key = K(1, 1); B.stunned = 0; B.aim = null; st.pos[0] = { x: 2, y: 2 }; st.pos[1] = { x: 13, y: 13 }; M.tick(P); assert.ok(B.key === K(1, 2) || B.key === K(2, 1) || B.aim, 'a corner chase either advances one cell or commits a lane'); }); /* ---- GATE: Y50-LURE-GONE — 소용돌이는 없다 (2026-08-04) ---- Y50-MAZE-RUNNER 안에 두지 않는다. 그 테스트는 이 작업과 무관한 이유로 이미 red 이고 (fuse/charge 해소, engine.test.js:12672), 첫 FAIL 뒤의 단언은 실행되지 않는다 — 죽은 테스트에 얹은 단언은 영원히 공허한 green 이다. 유인 칸은 "황소는 가장 가까운 몸에게 온다"는 이 판의 유일한 약속에 뚫린 구멍이었고, 바닥의 청록 나선은 공원 어디에도 대조할 문법이 없는 마크였다. */ test('Y50-LURE-GONE: the decoy cell, its counter, and its painter are all gone', () => { for (let seed = 1; seed <= 4; seed++) { const st = E._parkAlleyBuild({ ...E._parkAlleyCell(seed), mazeRunner: true }); assert.strictEqual(st.park.alley.lure, undefined, `seed ${seed}: the lure cell is gone`); assert.deepStrictEqual(Object.keys(st.park.alley), ['runner'], `seed ${seed}: park.alley carries nothing but the runner flag`); assert.ok(!('lureTurns' in st.park.dyn.alley), `seed ${seed}: and its counter is gone`); } const app = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const paint = app.slice(app.indexOf('function _paintParkAlley'), app.indexOf('PARK_FIELD_RENDER.alley')); assert.ok(!/lure/i.test(paint), 'the teal spiral painter is gone from _paintParkAlley'); }); /* ---- GATE: Y50-ROUND0-FAIR — 라운드 0 은 선택이지 강제가 아니다 (2026-08-04) ---- 분홍이 자기가 찜한 패드 위에 서면 그 칸은 파랑에게 불법 입력이 된다(동료 칸). 그러니 "그래도 남의 골을 뺏을까"라는 이 라운드의 질문이 살아 있으려면 파랑이 그녀보다 먼저 닿을 수 있어야 하고, 찜한 쪽이 더 싼 쪽이어야 한다 — 싸지 않으면 그것을 고르는 것이 조급함의 증거가 되지 못한다. 두 조건을 시드 전 구간에서 못 박는다. */ test('Y50-ROUND0-FAIR: the walker reaches the CLAIMED pad first, and it is the cheaper of the two', () => { for (let seed = 1; seed <= 8; seed++) { const st = E._parkAlleyBuild({ ...E._parkAlleyCell(seed), mazeRunner: true }); const n = st.N, K = (x, y) => y * n + x; const bfs = (sx, sy) => { const d = new Array(n * n).fill(Infinity), q = [K(sx, sy)]; d[K(sx, sy)] = 0; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const dd of [[0, -1], [0, 1], [-1, 0], [1, 0]]) { const nx = x + dd[0], ny = y + dd[1]; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || d[nk] <= d[k] + 1) continue; d[nk] = d[k] + 1; q.push(nk); } } return d; }; const dw = bfs(st.park.spawn.x, st.park.spawn.y); const dm = bfs(st.park.companionSpawn.x, st.park.companionSpawn.y); const kc = K(st.tokens[1].x, st.tokens[1].y); const kf = K(st.tokens[0].x, st.tokens[0].y); assert.ok(dw[kc] < dm[kc], `seed ${seed}: walker ${dw[kc]} must beat companion ${dm[kc]} to the claimed pad`); assert.ok(dw[kf] > dw[kc], `seed ${seed}: the claimed pad (${dw[kc]}) must be cheaper than the free one (${dw[kf]})`); } }); /* ---- GATE: Y50-MATE-WALKS — 분홍이 실제로 걷는다 (2026-08-04) ---- 회귀 핀이다. 이 무대의 동료는 park.contracts 가 비어 있다는 이유만으로 한 판 내내 단 한 칸도 움직이지 못했고(_parkCompanionStep 첫 줄에서 return), 라운드 1 이 그녀를 위해 심어 둔 목표도 한 번도 읽히지 않았다. 화면 순서까지 함께 못 박는다: 본다 -> 찜한다 -> 걷는다. 찜 링이 그가 고르기 전에 떠 있지 않으면 선택은 제안된 적이 없는 것이다. */ test('Y50-MATE-WALKS: round 0 stages notice -> claim -> walk, and the companion actually closes on her pad', () => { const st = E._parkAlleyBuild({ ...E._parkAlleyCell(5), mazeRunner: true }); const P = E.parkStart(st), M = E.PARK_FIELD_MECHS.alley; const D = st.park.dyn.alley, tok = st.tokens[1]; const dOf = () => Math.abs(st.pos[1].x - tok.x) + Math.abs(st.pos[1].y - tok.y); assert.strictEqual(D.round, -1, 'no round has opened yet'); const fx0 = st.fx.length; M.tick(P); // beat 1: she SEES it assert.strictEqual(D.mateStage, 'notice'); assert.strictEqual(D.goalClaim, null, 'the ring is earned on the NEXT beat, not this one'); const seen = st.fx.slice(fx0).filter(f => f.k === 'notice' && f.seat === 1); assert.strictEqual(seen.length, 1, 'exactly one discovery mark, on her'); assert.strictEqual(seen[0].token, 1, 'and it names the pad she is about to claim'); M.tick(P); // beat 2: she CLAIMS it assert.deepStrictEqual(D.goalClaim, { token: 1, seat: 1 }); assert.strictEqual(D.mateStage, 'walk'); assert.strictEqual(P.mode, 'relocate', 'and the shared mover is finally switched on'); assert.deepStrictEqual(P.target, { x: tok.x, y: tok.y }); const d0 = dOf(); for (let i = 0; i < 8; i++) { E._parkCompanionStep(P); M.tick(P); } assert.ok(dOf() < d0, `she closed on her pad: ${d0} -> ${dOf()} (0 movement was the bug)`); }); /* ---- GATE: Y50-MATE-SHOVE — 찜은 벽이 아니다 (2026-08-04) ---- 그녀가 먼저 닿았을 때의 답. 이 모듈에 이미 있는 상자 밀기 관용구를 그대로 복제하되 한 가지만 다르다: 미는 쪽이 그 칸에 남는다. 상자는 옮기는 것이고, 이건 차지하는 것이다. 그리고 배려는 여전히 밀지 않는다 — N 의 prefer 가 찜한 칸을 계속 거부하기 때문이다. */ test('Y50-MATE-SHOVE: the walker may shove the companion off a claimed pad, and keeps the cell', () => { const st = E._parkAlleyBuild({ ...E._parkAlleyCell(5), mazeRunner: true }); const P = E.parkStart(st), M = E.PARK_FIELD_MECHS.alley, n = st.N; const K = (x, y) => y * n + x; const pad = st.tokens[1]; st.pos[1] = { x: pad.x, y: pad.y }; // she got there first st.pos[0] = { x: pad.x - 1, y: pad.y }; // he is one step west of her const from = K(st.pos[0].x, st.pos[0].y), to = K(pad.x, pad.y); assert.strictEqual(M.legalAdd(P, to, 'me'), true, 'the shove is open to the walker'); assert.strictEqual(M.legalAdd(P, to, 'mate'), false, 'and to nobody else'); st.pos[0] = { x: pad.x, y: pad.y }; // parkStep commits the walker BEFORE onEnter M.onEnter(P, { fromKey: from, toKey: to, from: { x: pad.x - 1, y: pad.y }, to: { x: pad.x, y: pad.y } }); assert.deepStrictEqual(st.pos[1], { x: pad.x + 1, y: pad.y }, 'she is pushed on, in his direction'); assert.deepStrictEqual(st.pos[0], { x: pad.x, y: pad.y }, 'and HE keeps the cell — that is the point'); // refusals: a wall behind her, and a body on the ground is never shoved. st.pos[1] = { x: 1, y: 1 }; st.pos[0] = { x: 2, y: 1 }; assert.strictEqual(M.legalAdd(P, K(1, 1), 'me'), false, 'a wall behind her refuses'); st.pos[1] = { x: pad.x, y: pad.y }; st.pos[0] = { x: pad.x - 1, y: pad.y }; st.park.dyn.downed = { rescued: false, crawl: 0, by: null, trail: [] }; assert.strictEqual(M.legalAdd(P, to, 'me'), false, 'a companion on the ground is never shoved'); }); /* ---- GATE: Y50-TAIL-PAD — 세 번째 골은 황소를 쫓아야 닿는다 (2026-08-04) ---- 라운드 2 의 짧은 길은 이제 짧은 길이 아니라 짐승의 등 뒤다. 거부 조항이 본론이다: 패드는 파랑의 칸에 절대 오지 않는다 — 황소가 몸을 돌린 것만으로 레그를 공짜로 내주는 일은 이 라운드가 재려는 것의 정반대다. */ test('Y50-TAIL-PAD: round 2 goal rides the cell behind the bull, and never lands on the walker', () => { const st = E._parkAlleyBuild({ ...E._parkAlleyCell(5), mazeRunner: true }); const P = E.parkStart(st), n = st.N, K = (x, y) => y * n + x; const B = st.park.dyn.ents[0], t = st.tokens[3]; P.dest = 2; st.park.dyn.alley.round = 2; B.key = K(7, 7); B.face = 3; // facing RIGHT -> tail is west E._parkAlleyTailPad(P); assert.deepStrictEqual({ x: t.x, y: t.y }, { x: 6, y: 7 }, 'the pad sits directly behind it'); assert.deepStrictEqual({ x: st.park.clusters[3].x, y: st.park.clusters[3].y }, { x: 6, y: 7 }, 'and the cluster mirror moved with it'); B.face = 2; // facing LEFT -> tail is east E._parkAlleyTailPad(P); assert.deepStrictEqual({ x: t.x, y: t.y }, { x: 8, y: 7 }, 'it turns with the animal'); st.pos[0] = { x: 8, y: 7 }; // the walker is standing on the tail cell const was = { x: t.x, y: t.y }; B.key = K(7, 7); B.face = 2; E._parkAlleyTailPad(P); assert.notDeepStrictEqual({ x: t.x, y: t.y }, { x: 8, y: 7 }, 'the pad never lands under the walker — no free leg for a bull that merely turned round'); assert.deepStrictEqual({ x: t.x, y: t.y }, { x: 7, y: 6 }, 'it takes the first free side instead (DIRS order: up)'); P.dest = 0; // off round 2 it is inert const hold = { x: t.x, y: t.y }; B.key = K(1, 1); B.face = 0; E._parkAlleyTailPad(P); assert.deepStrictEqual({ x: t.x, y: t.y }, hold, 'outside round 2 the pad does not move'); }); /* ---- GATE: Y50-MATE-EVADES — 마지막 라운드의 그녀는 귀가하지 않는다 (2026-08-04) ---- 세 절이 순서대로다: 불붙은 레인에서 내려온다 / 짐승이 가까우면 물러선다 / 아니면 선다. 마지막 절이 게으름이 아니라 설계다 — 매 박자 발을 옮기는 몸은 자기 움직임의 뜻을 스스로 지운다. 이 라운드에서 그녀의 도망은 사건으로 읽혀야 한다. */ test('Y50-MATE-EVADES: in round 2 she backs away from a near bull, leaves a lit lane, and otherwise holds', () => { const st = E._parkAlleyBuild({ ...E._parkAlleyCell(5), mazeRunner: true }); const P = E.parkStart(st), n = st.N, K = (x, y) => y * n + x; const B = st.park.dyn.ents[0], D = st.park.dyn.alley; st.wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) st.wall.add(K(x, y)); P.dest = 2; D.round = 2; D.mateStage = 'evade'; st.pos[0] = { x: 13, y: 13 }; B.key = K(5, 5); B.aim = null; st.pos[1] = { x: 7, y: 5 }; // two cells away: inside the near band const d0 = 2; E._parkAlleyMateEvade(P); const d1 = Math.abs(st.pos[1].x - 5) + Math.abs(st.pos[1].y - 5); assert.ok(d1 > d0, `she backs away: ${d0} -> ${d1}`); st.pos[1] = { x: 13, y: 2 }; // far away const far = { ...st.pos[1] }; E._parkAlleyMateEvade(P); assert.deepStrictEqual(st.pos[1], far, 'a safe companion HOLDS — no idle shuffling'); B.key = K(1, 3); B.face = 3; B.aim = { dirIdx: 3, lane: [K(2, 3), K(3, 3), K(4, 3), K(5, 3)], fuse: 2 }; st.pos[1] = { x: 4, y: 3 }; // standing in the lit lane E._parkAlleyMateEvade(P); assert.ok(st.pos[1].y !== 3, 'she steps OFF the lane, not along it'); D.mateStage = 'walk'; // outside round 2 the clause is inert const kept = { ...st.pos[1] }; E._parkAlleyMateEvade(P); assert.deepStrictEqual(st.pos[1], kept, 'evade belongs to round 2 alone'); }); // Y50-ALLEY-PATH-SIG: the path-testimony separation on the alley observables — constructed playout // stubs (aligned to PARK_PERSONAS = [gsc, gcs, sgc, scg, cgs, csg]) so the predicate is proven // falsifiable seven distinct ways regardless of any single board's geometry. // RE-DERIVED 2026-08-01 with the signature itself: the partition is CARE-vs-CAUTION rank (indices // 1/4/5 rank care above caution; 0/2/3 rank caution above care), NOT the lead attitude. The scene // that used to be degenerate here — "goal that approaches must FAIL" — is now the board's MEASURED // behavior for gcs (goal>care>safety), because a board can only pose C-N on a beat where the goal // mind is silent or split, and on such a beat gcs takes the care move by its own lexical order. // The tooth that replaces it is stronger and sits on the new line: a persona that ranks CAUTION // above care must never approach as near as one that ranks care above caution. test('Y50-ALLEY-PATH-SIG: approach separates the C-N rank; seven degenerate scenes each FAIL the signature', () => { const stub = (minSeg, score, extra = {}) => ({ _alleyMinSegDist: minSeg, _alleyBlocks: 0, _alleyLaneEntries: 0, st: { score: { 0: score } }, ...extra }); // the MEASURED shape (seeds 1..8 x 6 personas): care-over-caution reaches 0, caution-over-care // stops at 1, goal-top banks strictly above safety-top. const base = () => [ stub(1, 10), stub(0, 10), // gsc (caution>care) | gcs (care>caution) — goal-top pair stub(1, 9), stub(1, 9), // sgc, scg — safety-top pair, both caution>care, off the lane stub(0, 10), stub(0, 9), // cgs, csg — care-top pair, care>caution ]; assert.ok(E._parkAlleySignature(base()), 'the separating scene must PASS'); { const p = base(); p[4]._alleyMinSegDist = Infinity; assert.ok(!E._parkAlleySignature(p), 'a fleeing care-over-caution persona must FAIL'); } { const p = base(); p[1]._alleyMinSegDist = 1; assert.ok(!E._parkAlleySignature(p), 'a care-over-caution persona that only TIES must FAIL'); } { const p = base(); p[0]._alleyMinSegDist = 0; assert.ok(!E._parkAlleySignature(p), 'a caution-over-care persona that approaches as near must FAIL'); } { const p = base(); p[2]._alleyLaneEntries = 1; assert.ok(!E._parkAlleySignature(p), 'safety-top on a live lane must FAIL'); } { const p = base(); p[3]._alleyBlocks = 1; assert.ok(!E._parkAlleySignature(p), 'safety-top that body-blocks must FAIL'); } { const p = base(); p[0].st.score[0] = 9; p[1].st.score[0] = 9; assert.ok(!E._parkAlleySignature(p), 'goal that does not out-bank safety must FAIL'); } { const p = base(); p[0]._alleyMinSegDist = Infinity; p[2]._alleyMinSegDist = Infinity; p[3]._alleyMinSegDist = Infinity; assert.ok(!E._parkAlleySignature(p), 'an unposed scene must FAIL'); } }); // RE-MEASURED 2026-08-01 after the gag pair + the re-derived signature: full sweep still 40/40 over // seeds 1..40 and the vacuity probe still 200/200 over 1..200, histogram still all zero, and care // still reaches dist 0 on 80/80 care playouts (seeds 1..40). The geometry moved; this bar did not. test('Y50-ALLEY-ADMIT: measured seeds admit with a clean histogram (full sweep 40/40 over seeds 1..40, probe 200/200 over 1..200, 2026-07-25; re-measured identical 2026-08-01) — and care REACHES the interpose', () => { const w0 = E.parkAlleyWhys(); // seed 2 is the witness seed: it REJECTED under two intermediate geometries (the verge-frozen // crossing column at 21/40, then the pocket cul-de-sac at 21/40 again) before the relative deep // pocket + the south chain fixed it — an accepted seed here is not a seed that could never fail. for (const s of [2, 7]) { assert.strictEqual(E._parkAlleyAdmissible(E._parkAlleyCell(s)), true, `seed ${s} must admit`); } const w1 = E.parkAlleyWhys(); for (const k of Object.keys(w1)) assert.strictEqual(w1[k] - w0[k], 0, `an admitted seed bumped whys.${k}`); // THE STOP-GATE ANSWER, pinned: on this yard the care-led walker's closest approach to the // interpose segment is 0 — he stands ON the locked lane inside the fuse (y25 measured floor: 2). for (const s of [2, 7]) { const P = E._parkAlleyPlay(E._parkAlleyCell(s), ['care', 'goal', 'safety']); assert.strictEqual(P._alleyMinSegDist, 0, `seed ${s}: care must reach the interpose (dist 0)`); } }); /* ==== Y50-GATES-END ==== */ /* ==== FIELD-REGISTRY-SURFACE (2026-07-23) — every seated mechanic must expose the CAMPAIGN * surface, and it must be alive. * * WHY THIS EXISTS. y32 warp shipped its module gates green with `_parkWarpAdmissible` at 40/40 over * seeds 1..40 — and the crossing layer could not measure the cell at all, because the registry entry * never wired `admits`. `_parkFieldCellAdmits` opens with `!!(m && m.admits)`, so a missing key is * not a crash: every candidate is silently skipped, the sweep exhausts, `filtered` comes back false, * and the anti-mimic control reports 0 probes. "0 leaks" off a loop that never ran. * * NEITHER EXISTING GATE COULD SEE IT. The module's own gates call `_parkWarpAdmissible` DIRECTLY, * never through the registry, so they prove the predicate and not the wiring. CAMP-CROSS-SWEEP only * sweeps SHIPPED slots — a preview is counted as "non-shipped (unswept)" — so the whole preview * shelf was outside its reach. This gate walks the registry the way campaign.js walks it. * * Two claims, and the second is the one with teeth: the surface EXISTS, and it ACCEPTS something. * An `admits` wired to a predicate that is false everywhere fails exactly like a missing one. */ test('FIELD-REGISTRY-SURFACE: every field mechanic a crossing slot names exposes cell+admits, and the registry path actually accepts cells', () => { const named = [...new Set(CAMP.PARK_CROSSINGS .filter(c => c.playMech && c.playMech.fieldMech).map(c => c.playMech.fieldMech))]; assert.ok(named.length >= 21, `only ${named.length} field mechanics are named by slots — the roster shrank`); const rows = []; for (const fv of named) { const m = E.PARK_FIELD_MECHS[fv]; assert.ok(m, `slot names field mechanic '${fv}' but nothing is registered under it`); assert.strictEqual(typeof m.cell, 'function', `${fv} has no cell() — the campaign cannot mint its play cell`); assert.strictEqual(typeof m.admits, 'function', `${fv} has no admits() — _parkFieldCellAdmits opens with !!(m && m.admits), so EVERY candidate ` + 'is skipped, the play-cell sweep exhausts, and the pairing measurement reports zero probes ' + 'instead of failing loudly. This is the y32 defect; it was invisible to the module gates ' + 'because they call the predicate directly rather than through this registry.'); // ALIVE, not merely present: a predicate false everywhere breaks the sweep the same way. // The range is seeds 1..40 because that is the sweep every module's own STOP gate reports on, // and some cells are legitimately SPARSE — y23 storm admits 6 of 40 and none of the first // eight, so a tighter bound would condemn a board that works. Short-circuits on the first // acceptance, so a healthy mechanic costs one seed. let ok = 0, at = -1; for (let s = 1; s <= 40 && ok === 0; s++) if (m.admits(m.cell(s))) { ok++; at = s; } assert.ok(ok > 0, `${fv}.admits accepted none of its own cells over seeds 1..40 — the registry ` + 'surface is wired but dead, which the crossing sweep cannot tell apart from missing'); rows.push(at > 1 ? `${fv}@${at}` : fv); } console.log(` [FIELD-REGISTRY-SURFACE] ${rows.length} mechanics carry a live cell+admits surface`); }); /* ==== Y29/Y31/Y32-SHIP-GATE (2026-07-23) — the three new crossings are PREVIEWS, and these gates * are what make that word mean something. `ship:false` asserted by hand is an opinion; here the flag * is pinned to the MEASUREMENT, so the day one of these boards starts recovering its pairing the * gate goes red and forces the flip to be deliberate. * * The anti-mimic control is reported SEPARATELY from the pairing verdict on purpose. * `_parkYNNRecovers` short-circuits on the first persona that fails, so its mimic leg may never run * — a slot could report "no leaks" having probed nothing. `_parkYNNMimicLeaks` runs the mimic leg * unconditionally and reports `probes` next to `leaks`, and the gate refuses a zero-probe report. * That is not hypothetical: y32 reported exactly zero probes until 5452ef6, because its registry * entry was missing `admits` and the play-cell sweep had been exhausting in silence. */ /* y29 and y31 left this table with their crossing SLOTS on 2026-07-29 (m1 x CN 0.56 and 0.67 * against a 0.80 bar, redundant on GC/GN, and skip-independent — separation map 2026-07-29). * Their rows could not stay: CAMP.PARK_Y{29,31}_SHIPPABLE and CAMP._parkY{29,31}MimicLeaks are * gone from campaign.js's exports, and `assert.ok(cx, ...)` below wants a row in PARK_CROSSINGS. * The MODULE gates Y29-STATUE-* (7) and Y31-RELAY-* (7) are untouched and still run — that is the * separation, and the shipped y46 siege board depends on the statue module at runtime. * NOTE this block is DUPLICATED verbatim further down the file (merge damage, 2026-07-29 finding): * both copies execute, so every edit here must be applied to BOTH. */ for (const [id, ship, mod, seed, mimic] of [ ]) { test(`${id.toUpperCase()}-SHIP-GATE: ${id} stays a preview until its pairing measurement says otherwise, and the anti-mimic control reports its probes`, () => { const cx = CAMP.PARK_CROSSINGS.find(c => c.id === id); assert.ok(cx, `${id} slot must exist in PARK_CROSSINGS`); assert.strictEqual(cx.ship, ship(), `${id}.ship=${cx.ship} disagrees with its pairing measurement ${ship()} — the picker flag and ` + 'the measurement drifted apart. Flip the flag only WITH the measurement, never ahead of it.'); assert.strictEqual(cx.open, true, 'a non-shipped slot must carry the preview mark, or it reads as inert'); const mim = mimic(); assert.ok(mim.probes > 0, `the anti-mimic control reported ${mim.leaks} leaks over ${mim.probes} probes — zero probes means ` + 'the loop never ran, and "0 leaks" off a loop that never ran is not a measurement (see 5452ef6)'); assert.ok(mim.leaks <= mim.expressed, 'leaks cannot exceed the pairs the mimic even poses'); console.log(` [${id.toUpperCase()}-SHIP-GATE] pairing=${ship()} (preview) · module=${mod()} · ` + `mimic probes=${mim.probes} expressed=${mim.expressed} leaks=${mim.leaks} @seed ${seed()}`); }); } /* ==================== SUMMON: THE ONE-USE SHIELD (2026-07-27) ==================== * DESIGN CHANGE, decided with the owner: the call is a SHIELD YOU SPEND, not a leash. The * companion answers, stands where he was asked, and is RELEASED there — his own errand resumes. * What still holds him through the looking span is the y29 shoulder rule (gazing && near<=1), * not the call, so the shield survives exactly the beats the player is sheltering behind him. * MEASURED BEFORE THIS CHANGE (seeds 1..24, both seated cells): summoning ONCE dropped the * companion's homecoming from 24/24 to 0/24 on cx:y46 AND cx:y29 — the call had no release path * anywhere in the engine, so a called body was masked out of its own planner for the whole run. */ test('SUMMON-ONE-USE-STATUE: the post is spent on arrival — the call clears and his own domain returns', () => { const st = E._parkStatueBuild({ seed: 42 }); const P = E.parkStart(st), dyn = st.park.dyn, D = dyn.statue, n = st.N; const M = E.PARK_FIELD_MECHS.statue, S = st.park.statue; const d = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }][S.side]; const lat = { x: Math.abs(d.y), y: Math.abs(d.x) }, c = (n - 1) >> 1; const at = (fwd, side) => ({ x: c + d.x * fwd + lat.x * side, y: c + d.y * fwd + lat.y * side }); st.pos[0] = at(0, 0); st.pos[1] = at(0, -2); assert.strictEqual(E.parkStatueSummon(P), true); const dest = D.summon; dyn.beat = 0; // song beats only: the walk is free let guard = 0; while ((st.pos[1].y * n + st.pos[1].x) !== dest && guard++ < 10) { D.gazePrev = false; D.matePrev = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); } assert.strictEqual(st.pos[1].y * n + st.pos[1].x, dest, 'he reached the post'); D.gazePrev = false; D.matePrev = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); // the beat that spends the call assert.strictEqual(D.summon, null, 'the post is spent: the call releases itself at the post'); st.pos[0] = at(-3, 0); // the caller has walked on; nothing holds him dyn.beat = 0; // a song beat: the shoulder rule is not in force assert.strictEqual(M.legalMask(P, 0, 'mate'), false, 'his own planner has its domain back'); }); test('SUMMON-ONE-USE-SIEGE: the hybrid spends the post the same way', () => { const st = E._parkSiegeBuild({ seed: 7 }); const P = E.parkStart(st), dyn = st.park.dyn, D = dyn.statue, n = st.N; const M = E.PARK_FIELD_MECHS.siege; st.pos[0] = { x: st.pos[1].x, y: st.pos[1].y + 2 }; assert.strictEqual(E.parkStatueSummon(P), true); const dest = D.summon; dyn.beat = 0; let guard = 0; while ((st.pos[1].y * n + st.pos[1].x) !== dest && guard++ < 10) { D.gazePrev = false; D.matePrev = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); } assert.strictEqual(st.pos[1].y * n + st.pos[1].x, dest, 'he reached the post'); D.gazePrev = false; D.matePrev = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(D.summon, null, 'the post is spent on the hybrid board too'); }); test('SUMMON-LAPSE-UNREACHABLE: a post the sea took releases the body instead of pinning it forever', () => { const st = E._parkSiegeBuild({ seed: 7 }); const P = E.parkStart(st), dyn = st.park.dyn, D = dyn.statue, D2 = dyn.siege; const M = E.PARK_FIELD_MECHS.siege; st.pos[0] = { x: st.pos[1].x, y: st.pos[1].y + 2 }; assert.strictEqual(E.parkStatueSummon(P), true); D2.gone.add(D.summon); // the wave takes the post before he arrives D.gazePrev = false; D.matePrev = { ...st.pos[1] }; M.tick(P, { mvKey: 'stay' }); assert.strictEqual(D.summon, null, 'an unreachable post lapses — a call nobody can answer is not a call'); }); test('SUMMON-ERRAND-SURVIVES: a companion who was used as a shield still gets home', () => { // seed 1 / care>goal>safety on the siege module: MEASURED 0 before this change, 2 after. const persona = E.PARK_PERSONAS.find(p => p.join('>') === 'care>goal>safety'); const near = s => Math.max(Math.abs(s.pos[0].x - s.pos[1].x), Math.abs(s.pos[0].y - s.pos[1].y)); const play = (useSummon) => { const st = E._parkSiegeBuild({ seed: 1 }); const P = E.parkStart(st), D = st.park.dyn.statue; let called = false; while (!P.over && P.turns < 80) { if (useSummon && !called && D.mateStun === 0 && near(st) <= 2 && E._parkStatueSummonDest(st) >= 0) called = E.parkStatueSummon(P); E.parkStep(P, E.parkOracleMove(P, persona)); } return { called, home: st.score[1], reason: P.reason }; }; const off = play(false), on = play(true); assert.ok(off.home > 0, 'CONTROL: with no call at all he always gets home — else this gate is vacuous'); assert.strictEqual(on.called, true, 'the shield was actually used, or the case proves nothing'); assert.ok(on.home > 0, 'using him as a shield must not cost him his errand for the whole run'); }); /* ==================== y51 ESCAPE — THE MODULE'S OWN GATES (2026-07-27) ==================== * WHY THESE SEVEN TESTS AND NOT A SWEEP. `parkEscapePull` is a HUMAN-ONLY verb: app.js's click * handler is its one caller and the oracle never reaches it. So the module admission bar, the * crossing pairing bar and the mimic bar are all STRUCTURALLY blind to it — they can run green * over a pull that does nothing, or a pull that maims the companion for the rest of the run, and * report the same number either way. That is not hypothetical: on 2026-07-27 a companion SUMMON * shipped with no release path, the companion's homecoming fell 24/24 -> 0/24 on two seated cells, * and every gate stayed green through all of it. ESCAPE-ERRAND-RESUMES is that bug's regression * test on this board. These are the only eyes on the pull. */ test('ESCAPE-CLOCKS-ALIGNED: the ground only ever dies on a beat both dolls are singing through', () => { const M = E.PARK_FIELD_MECHS.escape; // ⓪ ONE CLOCK, TWO READERS. The gaze predicates read the module CONSTANTS; the board carries // esc.a/esc.b, and the render's pip ring counts off THOSE. They agree only because the builder // stamps the constants onto the board (engine.js ~21350). The day a per-board clock is tuned — // Task 6's lever ② is exactly that — the halo and the pip count would desync IN SILENCE: the // player would be told one schedule and billed on another. So the two sources are pinned equal // here, where a tuner will trip over it, rather than discovered on screen. for (const seed of [1, 7, 23]) { const b0 = E._parkEscapeBuild({ seed }).park.escape; assert.strictEqual(b0.a.sing, E.PARK_ESCAPE_SING, `seed ${seed}: doll A's board clock and PARK_ESCAPE_SING have parted. The halo reads the ` + 'constant and the pip ring reads the board, so they now tell the player two different ' + 'schedules. If a per-board clock is intended, the render must read the board on BOTH sides.'); assert.strictEqual(b0.a.gaze, E.PARK_ESCAPE_GAZE, `seed ${seed}: doll A's gaze span has parted`); assert.strictEqual(b0.b.sing, E.PARK_ESCAPE_SING_B, `seed ${seed}: doll B's song has parted`); assert.strictEqual(b0.b.gaze, E.PARK_ESCAPE_GAZE_B, `seed ${seed}: doll B's gaze span has parted`); } // ① THE SCHEDULE THE ENGINE ACTUALLY RUNS. Driving tick and reading where lines LAND, never // poking the predicate in isolation: a gate that only reads _parkEscapeFloodBeat would stay // green if tick stopped consulting it altogether. for (const seed of [1, 7]) { const st = E._parkEscapeBuild({ seed }); const P = E.parkStart(st), D = st.park.dyn.escape; const sinks = []; let seen = 0; for (let b = 0; b < 60; b++) { st.park.dyn.beat = b; M.tick(P); if (D.rowsGone > seen) { seen = D.rowsGone; sinks.push(b); assert.strictEqual(E._parkEscapeBills(st), false, `seed ${seed}: a line went under on beat ${b}, a beat an eye is open — the water and a doll ` + 'can then bill the same step, and a forced heart loss is a trap, not a clock'); assert.strictEqual(E._parkEscapeFloodBeat(st), true, `seed ${seed}: a line sank on beat ${b}, which _parkEscapeFloodBeat calls no flood beat — ` + 'the front and its own predicate have drifted apart, so the header invariant is unchecked'); } } assert.strictEqual(D.rowsGone, st.park.escape.rows.length, `seed ${seed}: the sea never finished its timetable in 60 beats — nothing above was measured`); assert.ok(sinks.length >= 6, `seed ${seed}: only ${sinks.length} lines ever sank; too few to mean anything`); } // ② THE PREDICATE IS THE GATE, not a second guard beside one. Beats 4 and 5 pass tick's line // BUDGET (rowsGone 0 needs beat >= 3) and are refused by the flood-beat predicate alone. Both // are BILLED beats, so this is exactly the state the invariant exists to forbid. for (const b of [4, 5]) { const st = E._parkEscapeBuild({ seed: 7 }); const P = E.parkStart(st), D = st.park.dyn.escape; st.park.dyn.beat = b; assert.strictEqual(E._parkEscapeBills(st), true, `beat ${b} must be a billed beat, or this case probes nothing`); M.tick(P); assert.strictEqual(D.rowsGone, 0, `the front advanced on beat ${b}: the line budget let it through and nothing else stopped it, so ` + 'tick is no longer gating on _parkEscapeFloodBeat and the alignment holds only by arithmetic luck'); } { // and the very next flood beat DOES take a line, or the case above proves only that tick is dead const st = E._parkEscapeBuild({ seed: 7 }); const P = E.parkStart(st), D = st.park.dyn.escape; st.park.dyn.beat = 6; M.tick(P); assert.strictEqual(D.rowsGone, 1, 'a free beat past the budget must take a line — else tick sinks nothing at all'); } // ③ NON-VACUITY. The alignment is only worth asserting if the frame HAS dangerous beats. const st = E._parkEscapeBuild({ seed: 7 }); let billing = 0; for (let b = 0; b < E.PARK_ESCAPE_PERIOD; b++) { st.park.dyn.beat = b; if (E._parkEscapeBills(st)) billing++; } assert.strictEqual(billing, 3, 'the 6-beat frame must carry exactly 3 dangerous beats (2, 4, 5)'); }); test('ESCAPE-ONE-TOLL-PER-STEP: two dolls looking still costs one heart, not two', () => { const st = E._parkEscapeBuild({ seed: 7 }); const P = E.parkStart(st), n = st.N, D = st.park.dyn.escape, M = E.PARK_FIELD_MECHS.escape; st.park.dyn.beat = 5; // the doubled beat: both eyes open assert.ok(E._parkEscapeGazeA(st) && E._parkEscapeGazeB(st), 'beat 5 must be the doubled beat'); const from = { ...st.pos[0] }, to = { x: from.x, y: from.y - 1 }; const h0 = P.hearts; M.onLeave(P, { mvKey: 'U', from, to, fromKey: from.y * n + from.x, toKey: to.y * n + to.x }); assert.strictEqual(P.hearts, h0 - 1, 'the unit of judgement is the STEP, not the pair of eyes — priced per eye, a 3-heart body cannot ' + 'stand two dolls at all and the confession log stops counting decisions'); assert.strictEqual(D.caught.length, 1, 'and one step taken under one or both eyes is ONE confession'); // CONTROL: standing still is innocent on the same beat, or the assertion above says nothing about eyes. const h1 = P.hearts; M.onLeave(P, { mvKey: 'stay', from: to, to, fromKey: to.y * n + to.x, toKey: to.y * n + to.x }); assert.strictEqual(P.hearts, h1, 'stay is always innocent — the toll is on the step, not on the beat'); assert.strictEqual(D.caught.length, 1, 'and it writes no confession'); }); test('ESCAPE-PULL-TRANSFERS: the beat you pull, the toll lands on him', () => { const st = E._parkEscapeBuild({ seed: 7 }); const P = E.parkStart(st), n = st.N, D = st.park.dyn.escape, M = E.PARK_FIELD_MECHS.escape; st.park.dyn.beat = 5; st.pos[1] = { x: st.pos[0].x, y: st.pos[0].y - 2 }; // inside the reach assert.strictEqual(E.parkEscapePull(P), true, 'the scene must actually be pullable, or nothing below is measured'); const h0 = P.hearts, fx0 = st.fx.length; const from = { ...st.pos[0] }, to = { x: from.x, y: from.y - 1 }; M.onLeave(P, { mvKey: 'U', from, to, fromKey: from.y * n + from.x, toKey: to.y * n + to.x }); assert.strictEqual(P.hearts, h0, 'I do not pay on the beat I put him there'); assert.strictEqual(D.pulled.length, 1, 'and the ledger records that I did it'); assert.strictEqual(D.caught.length, 0, 'a beat he paid for is not a beat I was caught on'); // HIS GLYPH IS HIS OWN. A toll he took FOR the walker and a body the water carried off are two // different scenes; the render layer can only tell them apart if the engine already does. const kinds = st.fx.slice(fx0).map(f => f.k); assert.ok(kinds.indexOf('mateHurt') >= 0, 'the transfer must show as mateHurt — his glyph, never a heart'); assert.strictEqual(kinds.indexOf('mateSwept'), -1, 'and never as mateSwept, which is the water carrying him off'); // THE SHIELD IS SPENT ON ITS OWN BEAT — and the beat that proves it must come AFTER the one it // was bought on. `dyn.beat` is monotone in play, so the only leak the world can actually have is // a shield reaching FORWARD; rewinding the clock to an earlier billed beat would test a // direction that cannot happen and would leave a three-beat forward leak entirely unseen. // BEAT 8 IS ALSO THE ONLY PLACE IN THESE SEVEN TESTS WHERE DOLL B BILLS ALONE (8 % 6 = 2, so A // is singing; 8 % 3 = 2, so B is looking). Test 315 uses beat 5 (both eyes) and the transfer // above uses beat 5 too, so without this the second doll could quietly lose its toll — // `onLeave` narrowed from _parkEscapeBills to _parkEscapeGazeA — and nothing here would notice. st.park.dyn.beat = 8; // a LATER billed beat, and doll B's alone assert.ok(!E._parkEscapeGazeA(st) && E._parkEscapeGazeB(st), 'beat 8 must be the left-flank doll billing alone'); const h1 = P.hearts; M.onLeave(P, { mvKey: 'U', from: to, to: from, fromKey: to.y * n + to.x, toKey: from.y * n + from.x }); assert.strictEqual(P.hearts, h1 - 1, 'the shield covers the beat I bought and no LATER one — and doll B alone is enough to bill a step, ' + 'or the second clock this cell is named for has quietly stopped costing anything'); assert.strictEqual(D.pulled.length, 1, 'and the ledger does not grow on a beat nobody paid for'); assert.strictEqual(D.caught.length, 1, 'the walker wears that one himself'); }); test('ESCAPE-PULL-REFUSED: out of reach, stunned, or onto water — the answer is no and the world is untouched', () => { // one snapshot helper for all three refusals: a refused pull must move nobody, freeze nobody, // buy no shield, and write neither a ledger line nor a glyph. const snap = (st) => ({ mate: { ...st.pos[1] }, stun: st.park.dyn.escape.mateStun, shield: st.park.dyn.escape.shieldBeat, pulled: st.park.dyn.escape.pulled.length, fx: st.fx.length }); const refused = (label, prep) => { const st = E._parkEscapeBuild({ seed: 7 }); const P = E.parkStart(st); prep(st, P); const before = snap(st); assert.strictEqual(E.parkEscapePull(P), false, label); assert.deepStrictEqual(snap(st), before, `${label} — but the world moved anyway: a refused pull that still displaces, freezes, shields or ` + 'logs is a pull with two contracts, and the human never learns which one he got'); }; refused('four cells away is out of reach', (st) => { st.pos[1] = { x: st.pos[0].x, y: st.pos[0].y - 4 }; assert.ok(E._parkEscapeNear(st) > E.PARK_ESCAPE_REACH, 'the setup must actually be out of reach'); }); refused('a stunned body cannot be pulled', (st) => { st.pos[1] = { x: st.pos[0].x, y: st.pos[0].y - 2 }; assert.ok(E._parkEscapeNear(st) <= E.PARK_ESCAPE_REACH, 'reach must NOT be what refuses this one'); st.park.dyn.escape.mateStun = 1; }); refused('a landing cell the water took is no landing cell', (st) => { st.pos[1] = { x: st.pos[0].x, y: st.pos[0].y - 2 }; const dest = E._parkEscapePullDest(st); assert.ok(dest >= 0, 'CONTROL: the landing cell must exist before the water takes it, or nothing is tested'); st.park.dyn.escape.gone.add(dest); // the front swallows the cell he would land on assert.strictEqual(E._parkEscapePullDest(st), -1, 'sunk ground must stop being a destination'); }); // CONTROL: with none of the three refusals in force the same scene IS pullable — else the three // cases above could all be passing off a pull that never works at all. const st = E._parkEscapeBuild({ seed: 7 }); const P = E.parkStart(st); st.pos[1] = { x: st.pos[0].x, y: st.pos[0].y - 2 }; assert.strictEqual(E.parkEscapePull(P), true, 'CONTROL: the unobstructed pull must succeed'); }); test('ESCAPE-PULL-COSTS-HIM: the pull moves him toward the water, and his errand pays for it', () => { const man = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); // ① THE DISPLACEMENT, swept over every seed whose spawn scene is pullable at all. The pull is // not free care: it drags him off his own line to stand in front of a doll. let pullable = 0, farther = 0; for (let seed = 1; seed <= 24; seed++) { const st = E._parkEscapeBuild({ seed }); const P = E.parkStart(st); const gem = st.tokens[st.park.contracts[0].gem]; const d0 = man(st.pos[1], gem); if (!E.parkEscapePull(P)) continue; // out of reach at spawn on this seed's draw pullable++; if (man(st.pos[1], gem) > d0) farther++; } assert.ok(pullable >= 8, `only ${pullable} of seeds 1..24 could be pulled at spawn — too few to measure`); assert.strictEqual(farther, pullable, `${farther} of ${pullable} pulls left him farther from his contract gem — a pull that costs him ` + 'nothing is a free shield, and the whole tension of the cell is that sheltering behind him has a price'); // ② THE DELAY, with a control whose ONLY difference is whether the pull happened. The walker // stands still in both arms, so his own trajectory cannot be the thing that moved. const play = (doPull) => { const st = E._parkEscapeBuild({ seed: 7 }); const P = E.parkStart(st); const gem = st.tokens[st.park.contracts[0].gem]; let pulled = false, gemTurn = -1; if (doPull) pulled = E.parkEscapePull(P); const distAfterAct = man(st.pos[1], gem); // read at the SAME point in both arms while (!P.over && P.turns < 40) { E.parkStep(P, 'stay'); if (gemTurn < 0 && st.score[1] > 0) gemTurn = P.turns; } const plan = E._parkCompanionPlan(P); return { pulled, gemTurn, turns: P.turns, distAfterAct, distEnd: man(st.pos[1], gem), gemAlive: gem.alive, mode: P.mode, stunned: st.park.dyn.escape.mateStun, stuck: !!(plan && plan.stuck) }; }; const off = play(false), on = play(true); assert.ok(off.gemTurn > 0, 'CONTROL: left alone he reaches his gem inside the run — else this half is vacuous'); assert.strictEqual(on.pulled, true, 'the pull was actually used, or the arms are the same run twice'); assert.ok(on.gemTurn < 0 || on.gemTurn > off.gemTurn, `pulled, he still had his gem by turn ${on.gemTurn} against the control's ${off.gemTurn} — the two ` + 'beats of freeze and the two cells of displacement cost his errand nothing, so the shield is free'); // DELAYED, NOT DESTROYED. The line above, left alone, would pass MORE easily the WORSE the // outcome: `gemTurn < 0` is satisfied just as well by a pull that annihilated the errand as by // one that merely cost him beats, and an assertion that rewards destruction is pointing the // wrong way. Spec §9 claims a DELAY, so when he has not arrived inside the horizon this has to // show the errand still standing AND still moving. (MEASURED, seed 7 and every other // pull-admissible seed: at the walker's death on turn 12 his gem is untaken, his mode is still // toGem, he is unfrozen, his planner has a route, and he is ONE cell short of it — six cells of // route walked down to one. Late by a hair, not broken.) if (on.gemTurn < 0) { assert.strictEqual(on.gemAlive, true, 'his gem was gone without his ever scoring it — the pull consumed the errand rather than delaying it'); assert.strictEqual(on.mode, 'toGem', 'he is no longer even ON the errand — a mode that walked away from the contract is destruction ' + 'wearing a delay costume, and it is exactly the shape the 2026-07-27 summon bug had'); assert.strictEqual(on.stunned, 0, 'and the freeze must have lapsed long before the run ended'); assert.strictEqual(on.stuck, false, 'his planner has no route to his own gem at the end of the run — a stuck body is STOPPED, not late'); assert.ok(on.distEnd < on.distAfterAct, `pulled, he stood ${on.distAfterAct} cells from his gem and ended the run ${on.distEnd} away — he ` + 'closed no ground at all, so the pull did not delay his errand, it ended it'); } }); test('ESCAPE-ERRAND-RESUMES: the stun expires and he is his own again (the summon bug must not return)', () => { const st = E._parkEscapeBuild({ seed: 7 }); const P = E.parkStart(st), n = st.N, D = st.park.dyn.escape, M = E.PARK_FIELD_MECHS.escape; st.pos[1] = { x: st.pos[0].x, y: st.pos[0].y - 2 }; assert.strictEqual(E.parkEscapePull(P), true); const mk = () => st.pos[1].y * n + st.pos[1].x; assert.strictEqual(M.legalMask(P, mk(), 'mate'), true, 'while stunned his domain is empty'); // and the freeze is real where it counts: his own planner cannot move him while it holds. const frozenAt = mk(); E._parkCompanionStep(P); assert.strictEqual(mk(), frozenAt, 'a frozen companion does not walk'); for (let i = 0; i < E.PARK_ESCAPE_STUN; i++) M.tick(P); assert.strictEqual(D.mateStun, 0, 'the freeze is TRANSIENT — tick spends one beat of it per beat'); assert.strictEqual(M.legalMask(P, mk(), 'mate'), false, 'and then his planner has its domain back'); // THE REGRESSION ITSELF. `mateStun === 0` is only the mechanism; what the 2026-07-27 summon bug // actually cost was the HOMECOMING (24/24 -> 0/24 with every gate green), so the bar is that his // errand completes, not that a counter reached zero. let guard = 0; while (st.score[1] === 0 && guard++ < 60) E._parkCompanionStep(P); assert.ok(st.score[1] > 0, 'a companion who was used as a shield never got home again — that is the summon bug returning by ' + 'another door: the body is released on paper and masked out of its own errand in fact'); }); test('ESCAPE-DRY-PATH: the finish and his errand never sink, and a dry path to the line survives every wave', () => { const M = E.PARK_FIELD_MECHS.escape; for (let seed = 1; seed <= 8; seed++) { const st = E._parkEscapeBuild({ seed }); const P = E.parkStart(st), D = st.park.dyn.escape, n = st.N; const fin = st.park.escape.finishKey, dry = st.park.escape.dryKeys; assert.strictEqual(dry.length, 3, 'the hold-out names his station, his contract gem and the retire seat'); let waves = 0; for (let b = 0; b < 120 && D.rowsGone < st.park.escape.rows.length; b++) { st.park.dyn.beat = b; const was = D.rowsGone; M.tick(P); if (D.rowsGone > was) waves++; assert.ok(!D.gone.has(fin), `seed ${seed} wave ${D.rowsGone}: the finish must stay dry`); // ② THE ERRAND HOLD-OUT. Sunk ground is masked for the COMPANION too, so a drowned station, // gem or seat leaves _parkCompanionPlan with no route and freezes him for the rest of the // run — the exact soft-lock a85791f fixed, and one no reseeding filter could catch because // the offsets are fixed on every seed. for (const k of dry) assert.ok(!D.gone.has(k), `seed ${seed} wave ${D.rowsGone}: the water took cell ${k} of his errand — his planner loses its ` + 'target mid-errand, holds forever, and every care persona rejects as matelost'); // and a dry path from where he actually stands to the finish (BFS over walls + sunk ground). // THE ROOT MUST ITSELF BE DRY, and that is not a formality: it is the only assertion in the // suite on _parkEscapeShove's contract (spec §8 condition 3 — a body standing where the water // lands is always pushed to dry ground). A BFS rooted in a sunk cell would also quietly // measure the wrong thing, since a drowned root can still reach the finish. const startKey = st.pos[0].y * n + st.pos[0].x; assert.ok(!D.gone.has(startKey), `seed ${seed} wave ${D.rowsGone}: the water closed over the walker and left him there — the shove ` + 'found no dry ground, so the body is standing in the sea and every reading below is of a drowned man'); const seen = new Set([startKey]), q = [...seen]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const dd of [[0, -1], [0, 1], [-1, 0], [1, 0]]) { const nx = x + dd[0], ny = y + dd[1]; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (st.wall.has(nk) || D.gone.has(nk) || seen.has(nk)) continue; seen.add(nk); q.push(nk); } } assert.ok(seen.has(fin), `seed ${seed} wave ${D.rowsGone}: the escape went unsolvable — soft-lock`); } assert.strictEqual(D.rowsGone, st.park.escape.rows.length, `seed ${seed}: the sea stopped short of its timetable, so the late waves were never checked`); assert.ok(waves >= 6, `seed ${seed}: only ${waves} waves ever came — too few to call this a sweep`); } }); /* ESCAPE-BUILDER-FILTER replaces the brief's own Step-1 test, which is now VACUOUS: after a85791f * the contract station, contract gem and retire seat are held out of the flood STRUCTURALLY (the * `dry` set _parkEscapeAssemble builds `rows` from), so `rowOf(gemKey)` and `rowOf(stationKey)` are * -1 on every seed and the brief's `if (gemRow >= 0)` / `if (stRow >= 0)` guards never fire — a bar * that cannot fail. This version reads the flood lines themselves instead of trusting a name, and * carries three positive controls (one per spec §8 condition) so the filter's ability to REJECT a * genuinely broken layout is itself under test — a reseed loop that never rejects anything looks, * from outside, identical to one that never ran at all. */ test('ESCAPE-BUILDER-FILTER: no accepted seed ever names an errand cell the flood still owns, and none ever falls back silently', () => { // ---- (1) EVERY ACCEPTED BOARD, seeds 1..64 (_parkEscapeBuild's own reseed budget). Checked // against what _parkEscapeBuild actually RETURNS, not the pre-filter layout — the filter's whole // job is to make these true of the board a caller receives. let checked = 0, fallbacks = 0; for (let seed = 1; seed <= 64; seed++) { const st = E._parkEscapeBuild({ seed }); if (st.park.escape.fallback) fallbacks++; const n = st.N; const stationKey = st.park.contracts[0].station.y * n + st.park.contracts[0].station.x; const gemKey = st.tokens[1].y * n + st.tokens[1].x; const retireKey = st.park.retire.y * n + st.park.retire.x; const dryKeys = st.park.escape.dryKeys, rows = st.park.escape.rows; for (const k of [stationKey, gemKey, retireKey]) { checked++; // the errand key was actually FOUND and checked — assert.ok(dryKeys.indexOf(k) >= 0, // a counter, not merely "not present in rows" `seed ${seed}: errand cell ${k} is not named in dryKeys — the structural hold-out the header ` + 'claims is not the hold-out the board actually carries'); assert.ok(!rows.some(row => row.indexOf(k) >= 0), `seed ${seed}: errand cell ${k} is named in dryKeys but the flood still owns it in a row — the ` + 'name and the geometry have drifted apart, and a gate that only reads the name would stay green'); } assert.strictEqual(st.park.cell.seed, st.park.seed, `seed ${seed}: park.cell.seed (${st.park.cell.seed}) != park.seed (${st.park.seed}) — the board's ` + 'own play-cell disagrees with the board about which seed produced it'); } assert.strictEqual(checked, 3 * 64, `only ${checked} errand keys were checked across 64 seeds (expected 192) — the loop above skipped ` + 'seeds silently and the assertions inside it proved nothing on the ones it skipped'); assert.strictEqual(fallbacks, 0, `${fallbacks} of 64 seeds fell back to an unchecked candidate — _parkEscapeLayoutOk is rejecting ` + 'layouts this sweep does not otherwise account for, or 64 reseeds is not enough headroom'); // THE ACCEPTED-SEED INVARIANT, unit level. No seed in 1..64 actually forces the reseed loop past // t=0 (see the controls below for why: on this geometry the filter never naturally rejects), so // the loop above cannot exercise "the cell is re-derived from the ACCEPTED seed, not the // requested one" — call _parkEscapeAssemble directly with an 'accepted' seed unrelated to any // request and confirm neither field silently names a different seed. { const accepted = 424242; const st = E._parkEscapeAssemble(accepted); assert.strictEqual(st.park.seed, accepted, 'park.seed must be the ACCEPTED seed, not some other one'); assert.strictEqual(st.park.cell.seed, accepted, 'park.cell.seed must be the ACCEPTED seed too, or the board and its own play-cell would disagree ' + 'about which seed produced them the moment the loop ever takes t > 0'); } // ---- (2) THE FILTER CAN ACTUALLY REJECT. MEASURED: seeds 1..64 (and, separately, 1..20000 run // outside this suite) are ALL accepted at candidate t=0 — this geometry's own line-cut and // errand hold-out already make a spec-§8 violation unreachable by seed alone. So the only honest // way to prove the filter can fire at all is to hand it a layout that genuinely breaks each // condition and watch it say no. Every control asserts a PASSING baseline first, so the false // below is provably caused by the tamper and not by something already broken. const baseSeed = 1; const DIRS4 = [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]; { // CONTROL ① — no dry path to the finish. Seal the walker's spawn into a one-cell box. const st = E._parkEscapeAssemble(baseSeed); assert.strictEqual(E._parkEscapeLayoutOk(st), true, 'CONTROL baseline for condition (1) must pass before it is tampered with, or the failure below ' + 'proves nothing about the condition it is supposed to isolate'); const n = st.N, sx = st.pos[0].x, sy = st.pos[0].y; for (const dd of DIRS4) st.wall.add((sy + dd.y) * n + (sx + dd.x)); assert.strictEqual(E._parkEscapeLayoutOk(st), false, 'a walker sealed into a one-cell box has no dry path to the finish by construction, and the filter ' + 'let it through anyway — condition (1) is not actually being checked'); } { // CONTROL ③ — no dry cell to be shoved to. Box in the frontmost pre-finish row's own first cell. const st = E._parkEscapeAssemble(baseSeed); assert.strictEqual(E._parkEscapeLayoutOk(st), true, 'CONTROL baseline for condition (3) must pass first'); const rows = st.park.escape.rows; assert.ok(rows.length > 0, 'CONTROL: the flood schedule must actually have a frontmost row to box in'); const n = st.N, k = rows[rows.length - 1][0], x = k % n, y = (k / n) | 0; for (const dd of DIRS4) st.wall.add((y + dd.y) * n + (x + dd.x)); assert.strictEqual(E._parkEscapeLayoutOk(st), false, 'a cell with no un-walled neighbour at all has no dry cell to be shoved to when its own wave ' + 'arrives, and the filter let it through anyway — condition (3) is not actually being checked'); } { // CONTROL ② — the errand hold-out dropped from the GEOMETRY while dryKeys still claims it: the // exact regression the header warns a later edit could cause (dryKeys.length stays 3, but a row // owns the cell anyway). const st = E._parkEscapeAssemble(baseSeed); assert.strictEqual(E._parkEscapeLayoutOk(st), true, 'CONTROL baseline for condition (2) must pass first'); const gemKey = st.tokens[1].y * st.N + st.tokens[1].x; assert.ok(st.park.escape.dryKeys.indexOf(gemKey) >= 0, 'CONTROL: the contract gem must actually be named in dryKeys before the geometry is tampered with'); st.park.escape.rows[0].push(gemKey); // the flood now also owns the 'held out' cell assert.strictEqual(E._parkEscapeLayoutOk(st), false, 'the contract gem is still named in dryKeys but the flood owns it in a row — the structural ' + 'hold-out has been dropped from the geometry, and the filter let it through anyway: condition (2) ' + 'is not actually being checked, only the label is'); } }); /* ==================== ESCAPE-CLICK-SEAM (y51, 2026-07-27) ========================================== * THE APP-SIDE HALF OF THE HUMAN-ONLY VERB. The seven gates above test `parkEscapePull` itself. They * cannot see the wiring that decides WHEN to call it and on WHICH game object, and neither can the * module bar, the pairing bar or the mimic bar — the oracle never clicks, so every one of them runs * green over a click branch aimed at the wrong P or one that swallows clicks it should pass through. * That is the repository's recorded blind spot ("사람 전용 어포던스 사각지대"), and on 2026-07-27 it * shipped a live bug through a fully green suite. * * app.js needs the DOM to run, so this gate uses the C1-drawToken idiom every app.js gate here uses — * it reads app.js as SOURCE — but it does NOT stop at a regex. It CUTS the seam functions out of * the source and EXECUTES them, once against a spy engine (which P did the branch actually aim at?) * and once against the REAL engine on a REAL escape board (does the seam agree with the module?). * A regex could only pin the text; this pins the behaviour, which is what the blind spot needs. * * AND IT RUNS THE WHOLE ROUTE, NOT JUST THE PULL — the lesson of review round 1. This gate's first * revision pinned the listener's call textually, and a reviewer showed the hole: wrapping the shipped * pull branch in `if (false)` left the source byte-identical, killed the pull dead, and the gate went * green. Text is not reachability. So app.js now lifts the entire in-game route into * `_parkGameClick`, and leg (F) below CUTS AND RUNS THAT — which means "the pull is reachable past * the summon branch above it" and "the move read still receives the clicks that are not ours" are * EXECUTED assertions. The realistic recurrences the reviewer named — an early `return` above the * pull, or a new companion's-cell branch inserted before it — die in leg (F) too. */ test('ESCAPE-CLICK-SEAM: the pull click aims at the live board, and never swallows a click that is a move', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); // ---- cut a top-level function out of app.js by balanced braces (its body carries no brace in any // string or comment; if that ever stops being true this cut fails loudly rather than quietly). const cut = (name) => { const at = src.indexOf('function ' + name + '('); assert.ok(at >= 0, `ESCAPE-CLICK-SEAM cannot find ${name} in app.js — the pull's click seam has been renamed or ` + 'inlined, which means this gate stopped measuring the thing it claims to measure'); let depth = 0; for (let j = src.indexOf('{', at); j < src.length; j++) { if (src[j] === '{') depth++; else if (src[j] === '}' && --depth === 0) return src.slice(at, j + 1); } assert.fail(`ESCAPE-CLICK-SEAM could not brace-match ${name} out of app.js`); }; const liveP = new Function('return (' + cut('_parkLiveP') + ');')(); const mkClick = (eng) => new Function('E', '_parkLiveP', 'return (' + cut('_parkEscapeClick') + ');')(eng, liveP); // ---- (A) THE LIVE P. A hub TASK cell keeps its game one level deeper than a plain crossing cell. // A branch that always read run.park.game.P would drive a stale board while the player watched a // live one and nothing on screen would say so. const plainP = { tag: 'plain' }, taskP = { tag: 'task' }; assert.strictEqual(liveP({ park: { game: { P: plainP } } }), plainP, 'a plain crossing cell must resolve to run.park.game.P'); assert.strictEqual(liveP({ park: { task: { game: { P: taskP } }, game: { P: plainP } } }), taskP, 'a hub TASK cell must resolve to run.park.task.game.P — a pull aimed at run.park.game.P there ' + 'drives a board nobody is looking at, and every oracle-driven bar stays green while it happens'); // ---- (B) ROUTING, against a SPY engine: which clicks are ours, and which P we aimed at. const spy = { calls: [], parkEscapePull(P) { spy.calls.push(P); return true; } }; const click = mkClick(spy); const runTask = { park: { task: { game: { P: taskP } }, game: { P: plainP } } }; const stEsc = { park: { escape: {} }, pos: { 0: { x: 3, y: 3 }, 1: { x: 5, y: 4 } } }; assert.strictEqual(click(runTask, stEsc, 5, 4), true, "a click on the companion's own cell is the pull, and a pull the engine accepted must report a repaint"); assert.strictEqual(spy.calls.length, 1, 'the branch must call parkEscapePull exactly once for that click'); assert.strictEqual(spy.calls[0], taskP, 'the pull was aimed at the WRONG P — on a hub task cell the command must reach the task game'); // the three clicks that are NOT ours. Each must fall through (null) AND fire no command at all: // a branch that returned false here would consume the click and the walker would stop moving. const passes = [[3, 3, 'his own cell = stay'], [3, 4, 'a neighbour = that step'], [9, 9, 'far ground = inert'], [5, 3, 'the cell above him'], [4, 4, 'beside him']]; for (const [cx, cy, what] of passes) assert.strictEqual(click(runTask, stEsc, cx, cy), null, `${what}: the pull branch swallowed a click that belongs to the move read — the walker would ` + 'stop responding to half the board and no oracle-driven bar could ever see it'); assert.strictEqual(spy.calls.length, 1, 'the pull branch fired an engine command on a click that was not its own'); // a board with no escape fixture is never ours, however the click lands — the y29 summon branch // and every ordinary board must still receive their own clicks. const stStatue = { park: { statue: {} }, pos: { 0: { x: 3, y: 3 }, 1: { x: 5, y: 4 } } }; assert.strictEqual(click(runTask, stStatue, 5, 4), null, 'the pull branch claimed a click on a NON-escape board — it would shadow the y29 summon'); assert.strictEqual(spy.calls.length, 1, 'and it must not have called the engine on that board either'); // ---- (C) A REFUSED PULL IS CONSUMED, NOT PASSED ON. false, never null: a refused pull that fell // through would become a step into his cell, which is the collision the branch exists to prevent. const spyNo = { calls: [], parkEscapePull(P) { spyNo.calls.push(P); return false; } }; assert.strictEqual(mkClick(spyNo)(runTask, stEsc, 5, 4), false, 'a refused pull must report CONSUMED-and-inert (false), never not-mine (null)'); assert.strictEqual(spyNo.calls.length, 1, 'CONTROL: the refusing engine was actually consulted'); // ---- (D) AGAINST THE REAL ENGINE, ON A REAL BOARD. (B) proves the routing; this proves the seam // and the module agree about the same world. const st = E._parkEscapeBuild({ seed: 1 }); const P = E.parkStart(st); const run = { park: { game: { P } } }; const real = mkClick(E); const D = st.park.dyn.escape; const before = { x: st.pos[1].x, y: st.pos[1].y }; assert.strictEqual(D.mateStun, 0, 'CONTROL: he starts unfrozen, or the accept below proves nothing'); assert.ok(E._parkEscapeNear(st) <= E.PARK_ESCAPE_REACH && E._parkEscapePullDest(st) >= 0, 'CONTROL: seed 1 must open with a legal pull, or this leg is vacuous'); assert.strictEqual(real(run, st, before.x, before.y), true, 'the opening pull must be accepted'); assert.ok(st.pos[1].x !== before.x || st.pos[1].y !== before.y, 'CONTROL: an accepted pull must have actually moved him, or the refusal below proves nothing'); const after = { x: st.pos[1].x, y: st.pos[1].y }; assert.ok(D.mateStun > 0, 'CONTROL: the accepted pull must have frozen him'); assert.strictEqual(real(run, st, after.x, after.y), false, 'a second pull while he is frozen must be refused — CONSUMED and inert'); assert.deepStrictEqual({ x: st.pos[1].x, y: st.pos[1].y }, after, 'a refused pull must leave the world untouched'); assert.strictEqual(real(run, st, st.pos[0].x, st.pos[0].y), null, "even on a real escape board, a click on the WALKER's cell is 'stay' and must fall through"); // ---- (E) THE PATH FROM THE EVENT TO THE ROUTE, EXECUTED. There is no golden here any more, and // the reason is the lesson of review round 2. // // Round 1 pinned the listener's call with a `contains` regex; `if (false)` around the branch beat // it. Round 2 replaced that with a GOLDEN over the game-mode block; `return;` one line above that // block, and `if (false)` one enclosing brace outward, both beat THAT — each killing the entire // in-game route (no summon, no pull, no move) with the gate green. The weakness never went away, // it moved to whatever boundary the golden happened to draw, and it would move again to the next // one. A golden anchored at `if (run.park) {` would simply invite the same mutation at the // listener body. That recursion has no fixed point, because the question — "is this code reached" // — is not a question about text at any boundary. // // So the boundary is put where the recursion actually ENDS: the event handler itself. This cuts // the click listener's own callback out of app.js and RUNS it with a synthetic click, a stub // board rect and a spy `_parkGameClick`, and asserts the route is reached with the right cell. // Every wrapper, early return or inserted branch ANYWHERE between the event and the route now // fails, at any nesting depth, because nothing about the assertion is positional. What is left is // the registration line — and that has no enclosing brace inside app.js to hide one more level in, // so it is the one thing pinned as text. assert.ok(src.indexOf("board.addEventListener('click', e => {") >= 0, 'the click listener registration is gone or reshaped — nothing below can vouch for a handler ' + 'that is never attached'); { const at = src.indexOf("board.addEventListener('click', e => {"); // ---- AND THE REGISTRATION ITSELF RUNS AT TOP LEVEL. My round-2 claim that this line "has no // enclosing brace to hide in" was simply wrong, and a reviewer showed it: `if (false) { … }` // around the whole registration statement left every assertion below green while NO CLICK // ANYWHERE IN THE APP did anything — hub tiles, tutorial, park game, legacy board, all silent. // The fixed point is not textual, it is the PARSER: everything before the registration must be // a complete program on its own. If the statement has been nested inside any open block — an // `if`, a `function`, an IIFE, a `try` — that prefix is unbalanced and does not compile. This is // fail-safe by construction (a prefix that will not parse throws, which is RED, never a silent // pass) and it needs no comment or string stripping. ~5ms on the shipped file. try { new Function(src.slice(0, at)); } catch (err) { assert.fail('the click listener registration is no longer a TOP-LEVEL statement — everything ' + 'before it does not parse as a complete program, which means the registration now sits ' + 'inside an open block (an `if`, a function, an IIFE) and may never run at all. Under that ' + 'shape no click anywhere in the app does anything and every other assertion here still ' + 'passes. Parser said: ' + (err && err.message)); } let depth = 0, end = -1; for (let j = src.indexOf('{', at); j < src.length; j++) { if (src[j] === '{') depth++; else if (src[j] === '}' && --depth === 0) { end = j + 1; break; } } assert.ok(end > at, 'ESCAPE-CLICK-SEAM could not brace-match the click listener out of app.js'); const handlerSrc = src.slice(src.indexOf('e => {', at), end); // Only the identifiers on the PLAY path have to be supplied — the tutorial/hub/demo/report // branches are guarded by `psk === ...` and are never evaluated with stageKey() === 'play'. const seen = []; const G = { campaign: null, parkAnim: { mode: 'game' } }; const P = { st: { N: 13, park: { escape: {} }, pos: { 0: { x: 3, y: 3 }, 1: { x: 5, y: 4 } } } }; G.campaign = { park: { game: { P } } }; const handler = new Function('G', 'stageKey', 'board', '_parkGameClick', 'start', 'playMove', '_parkLiveP', 'return (' + handlerSrc + ');')( G, () => 'play', { getBoundingClientRect: () => ({ left: 0, top: 0, width: 130, height: 130 }) }, (run, st, px, py) => { seen.push({ run, st, px, py }); }, () => assert.fail('start() must not run in a live play click'), () => assert.fail('the legacy board path must not run on a park board'), liveP); handler({ clientX: 55, clientY: 45 }); // 130px / 13 cells = 10px per cell -> cell (5, 4) assert.strictEqual(seen.length, 1, 'a live park-game click never reached _parkGameClick. The route is present in app.js and ' + 'unreachable from the browser — no summon, no pull, no move — which is strictly worse than ' + 'the 2026-07-27 summon bug and is exactly what a source regex or a golden cannot see'); assert.strictEqual(seen[0].st, P.st, 'the route must be handed the LIVE board'); assert.strictEqual(seen[0].run, G.campaign, 'and the live run'); assert.deepStrictEqual([seen[0].px, seen[0].py], [5, 4], 'the clicked cell was mis-computed on the way in — the route would act on the wrong square'); handler({ clientX: 5, clientY: 5 }); // a second, different cell: the cell is READ, not fixed assert.deepStrictEqual([seen[1].px, seen[1].py], [0, 0], 'the handler hands the same cell to the route whatever was clicked'); // ---- THE TASK BOARD. A hub-launched cell keeps its game under run.park.task (campaign.js sets // pk.task), and THAT is the live path every hub-launched crossing cell takes — every crossing // tile the player opens from the hub arrives here. Deleting the round-2 golden dropped the only // cover on this read: collapsing `_parkLiveP(run).st` back to `run.park.game.P.st` made every // in-game click read the wrong board (or throw) with every gate green. The stub above has no // `.task`, so it never exercised the branch; this runs the SAME handler again against a // task-shaped run. const PTask = { st: { N: 13, park: { escape: {} }, pos: { 0: { x: 1, y: 1 }, 1: { x: 2, y: 2 } } } }; assert.notStrictEqual(PTask.st, P.st, 'CONTROL: the two boards must be distinguishable objects'); G.campaign = { park: { task: { game: { P: PTask } }, game: { P } } }; handler({ clientX: 55, clientY: 45 }); assert.strictEqual(seen.length, 3, 'the task-shaped click must still reach the route'); assert.strictEqual(seen[2].st, PTask.st, 'a hub TASK cell resolved to the CAPSTONE board. Every click would act on a board nobody is ' + 'looking at — and this is the live path for every hub-launched crossing cell'); assert.strictEqual(seen[2].run, G.campaign, 'and the run handed down must be the live one'); G.campaign = { park: { game: { P } } }; // leave the stub as leg (E) found it } // ---- (F) THE WHOLE ROUTE, CUT AND RUN. This is the leg that makes reachability an assertion. // _parkGameClick's summon branch now calls `_parkLiveP` (Minor 3 of the 2026-07-28 fix wave routed // both the summon and the listener's `st` read through the one named helper instead of re-spelling // the ternary) so the cut function needs it supplied, same as mkClick already supplies it above. const mkRoute = (eng, spy) => new Function('E', 'draw', 'parkGameInput', '_parkEscapeClick', '_parkLiveP', 'return (' + cut('_parkGameClick') + ');')(eng, spy.draw, spy.input, mkClick(eng), liveP); const mkSpy = () => { const s = { draws: 0, inputs: [] }; s.draw = () => { s.draws++; }; s.input = (m) => { s.inputs.push(m); }; return s; }; { // (F1) THE PULL IS REACHABLE — past the summon branch that sits textually above it. An // `if (false)` around the pull, an early return above it, or a new companion's-cell branch // inserted before it all land here, and none of them are visible to a regex. const spy = mkSpy(); const eng = { calls: [], parkEscapePull(P) { eng.calls.push(['pull', P]); return true; }, parkStatueSummon(P) { eng.calls.push(['summon', P]); return true; } }; const route = mkRoute(eng, spy); const st = { N: 13, park: { escape: {} }, pos: { 0: { x: 3, y: 3 }, 1: { x: 5, y: 4 } } }; assert.strictEqual(route(runTask, st, 5, 4), 'pull', 'a click on the companion of an ESCAPE board did not route to the pull — the branch is present ' + 'in the source but not reachable at runtime, which is exactly the 2026-07-27 summon bug shape'); assert.deepStrictEqual(eng.calls.map(c => c[0]), ['pull'], 'and only the pull may have fired'); assert.strictEqual(eng.calls[0][1], taskP, 'aimed at the live task board'); assert.strictEqual(spy.draws, 1, 'an accepted pull repaints'); assert.strictEqual(spy.inputs.length, 0, 'and it must never also spend a turn'); } { // (F2) THE MOVE READ STILL GETS ITS CLICKS. The route must not have been turned into a // companion-only handler by anything inserted above it. const spy = mkSpy(); const eng = { parkEscapePull: () => true, parkStatueSummon: () => true }; const route = mkRoute(eng, spy); const st = { N: 13, park: { escape: {} }, pos: { 0: { x: 3, y: 3 }, 1: { x: 5, y: 4 } } }; assert.strictEqual(route(runTask, st, 3, 3), 'stay', "the walker's own cell is 'stay'"); assert.strictEqual(route(runTask, st, 3, 2), 'move', 'a neighbour is that step'); assert.strictEqual(route(runTask, st, 2, 3), 'move', 'and so is the one on the other axis'); assert.strictEqual(route(runTask, st, 9, 9), 'inert', 'far ground stays inert'); assert.deepStrictEqual(spy.inputs, ['stay', 'U', 'L'], 'the route sent the wrong keys to the park resolver — the d-pad and the click read must agree'); } { // (F3) THE y29 SUMMON PATH IS UNCHANGED BY THE LIFT. Same condition, same "repaint only if the // engine accepted", same early exit before the pull is ever consulted. Its P read now goes // through `_parkLiveP` (2026-07-28 fix wave, Minor 3) instead of a re-spelled inline ternary, // and that is exactly why THIS assertion has to exist: F1 above already checks the PULL's // target (`eng.calls[0][1] === taskP`), but the summon spy used to discard its argument // entirely, so a summon aimed at the wrong board (the 2026-07-27 shape, now for THIS verb) // would have passed here unnoticed. It now records what it was called with. const spy = mkSpy(); const eng = { calls: [], summonP: null, parkEscapePull(P) { eng.calls.push('pull'); return true; }, parkStatueSummon(P) { eng.summonP = P; eng.calls.push('summon'); return true; } }; const st = { N: 13, park: { statue: {} }, pos: { 0: { x: 3, y: 3 }, 1: { x: 5, y: 4 } } }; assert.strictEqual(mkRoute(eng, spy)(runTask, st, 5, 4), 'summon'); assert.deepStrictEqual(eng.calls, ['summon'], 'the summon must fire and the pull must not'); assert.strictEqual(spy.draws, 1, 'an accepted summon repaints'); assert.strictEqual(eng.summonP, taskP, 'the summon was aimed at the capstone board on a hub TASK cell — aimed at run.park.game.P ' + 'instead, it would drive a board nobody is looking at while every gate here stayed green'); const spy2 = mkSpy(); const eng2 = { parkStatueSummon: () => false, parkEscapePull: () => { assert.fail('not the pull'); } }; assert.strictEqual(mkRoute(eng2, spy2)(runTask, st, 5, 4), 'summon', 'a REFUSED summon is still consumed — it must not fall through and become a step into him'); assert.strictEqual(spy2.draws, 0, 'and a refused summon must not repaint'); } { // (F3b) THE SAME COMPETING GEOMETRY, FOR y29. F3 above stands the companion two cells from // the walker — the exact geometry that made F1's and the old ordering legs vacuous, and a // reviewer showed it: moving the summon branch BELOW the move read left this gate green while // an adjacent-companion click on a real statue board would become a step INTO him. That // matters more since the lift, because y29's routing now lives in shared code and a y51 edit // can break y29's seam. So: him BESIDE the walker, accepting and refusing engines. const st = { N: 13, park: { statue: {} }, pos: { 0: { x: 3, y: 3 }, 1: { x: 4, y: 3 } } }; const spyA = mkSpy(); const engA = { calls: 0, parkStatueSummon() { engA.calls++; return true; }, parkEscapePull: () => { assert.fail('not an escape board'); } }; assert.strictEqual(mkRoute(engA, spyA)(runTask, st, 4, 3), 'summon', 'a click on a companion STANDING BESIDE the walker was consumed as a step — the y29 summon is ' + 'read after the move test, and the input collision that branch exists to prevent is back'); assert.strictEqual(engA.calls, 1, 'the summon command must have fired'); assert.deepStrictEqual(spyA.inputs, [], 'and the click must not ALSO have spent a turn'); const spyB = mkSpy(); const engB = { calls: 0, parkStatueSummon() { engB.calls++; return false; }, parkEscapePull: () => { assert.fail('not an escape board'); } }; assert.strictEqual(mkRoute(engB, spyB)(runTask, st, 4, 3), 'summon', 'a REFUSED summon fell through and became a step INTO him'); assert.strictEqual(engB.calls, 1, 'CONTROL: the refusing engine was actually consulted'); assert.deepStrictEqual(spyB.inputs, [], 'a refused summon spends no turn'); assert.strictEqual(spyB.draws, 0, 'and repaints nothing'); } { // (F4) THE CASE WHERE THE TWO READS COMPETE: the companion standing NEXT TO the walker, so // his cell is ALSO a legal step. This is the only geometry in which "pull before move" and // "the three-valued contract" have any teeth — with him two cells away the move read declines // the click anyway and both mutations pass unnoticed. (They did: lifting the route into // _parkGameClick moved the text the old regex legs pinned, and until this leg existed a pull // read AFTER the move test, and a `if (pulled)` that let a refusal fall through, were both // green. Reachability legs replaced ordering regexes; this replaces the rest.) const st = { N: 13, park: { escape: {} }, pos: { 0: { x: 3, y: 3 }, 1: { x: 4, y: 3 } } }; const spyA = mkSpy(); const engA = { calls: 0, parkEscapePull() { engA.calls++; return true; }, parkStatueSummon: () => { assert.fail('not a statue board'); } }; assert.strictEqual(mkRoute(engA, spyA)(runTask, st, 4, 3), 'pull', 'a click on a companion STANDING BESIDE the walker was consumed as a step — the pull is read ' + 'after the move test, so the verb is unreachable in exactly the geometry it is for'); assert.strictEqual(engA.calls, 1, 'the engine command must have fired'); assert.deepStrictEqual(spyA.inputs, [], 'and the click must not ALSO have spent a turn'); const spyB = mkSpy(); const engB = { calls: 0, parkEscapePull() { engB.calls++; return false; }, parkStatueSummon: () => { assert.fail('not a statue board'); } }; assert.strictEqual(mkRoute(engB, spyB)(runTask, st, 4, 3), 'pull', 'a REFUSED pull fell through to the move read and became a step INTO him — the three-valued ' + 'contract has collapsed to a boolean, which is the one input collision this branch exists for'); assert.strictEqual(engB.calls, 1, 'CONTROL: the refusing engine was actually consulted'); assert.deepStrictEqual(spyB.inputs, [], 'a refused pull spends no turn'); assert.strictEqual(spyB.draws, 0, 'and repaints nothing'); } // ---- (G) WHO CRACKED HIS HEART. Render-only arithmetic, so no oracle bar covers it either; the // one-sided version of this guard reads EVERY freeze as a pull the moment the beat stops // dominating shieldBeat, which is total and silent rather than partial. const byPull = new Function('return (' + cut('_parkEscapeFreezeByPull') + ');')(); const esc2 = { stun: 2 }; assert.strictEqual(byPull(5, { shieldBeat: 5 }, esc2), true, 'the pull beat itself is warm'); assert.strictEqual(byPull(6, { shieldBeat: 5 }, esc2), true, 'and so is the second frozen beat'); assert.strictEqual(byPull(7, { shieldBeat: 5 }, esc2), false, 'the window closes with the freeze'); assert.strictEqual(byPull(4, { shieldBeat: 5 }, esc2), false, 'a beat BEFORE the stamp is not a pull this freeze can be blamed on — without the lower bound ' + 'every freeze on the board reads warm and the water stops being tellable from the hand'); assert.strictEqual(byPull(0, { shieldBeat: -1 }, esc2), false, 'never pulled: never warm'); { // and against the REAL module: a pull reads warm, a freeze the SEA caused reads cold. const M = E.PARK_FIELD_MECHS.escape; const st = E._parkEscapeBuild({ seed: 1 }); const P = E.parkStart(st), D = st.park.dyn.escape, dyn = st.park.dyn; assert.strictEqual(E.parkEscapePull(P), true, 'CONTROL: the opening pull must be accepted'); assert.strictEqual(byPull(dyn.beat, D, st.park.escape), true, 'a real pull reads warm'); const st2 = E._parkEscapeBuild({ seed: 1 }); const P2 = E.parkStart(st2), D2 = st2.park.dyn.escape; const line = E._parkEscapeNext(st2); st2.pos[1] = { x: line[0] % st2.N, y: (line[0] / st2.N) | 0 }; // stand him where it drowns next st2.park.dyn.beat = E.PARK_ESCAPE_EVERY; M.tick(P2); assert.ok(D2.mateSwept > 0 && D2.mateStun > 0, 'CONTROL: the sea must actually have shoved and frozen him, or the cold read below is vacuous'); assert.strictEqual(byPull(st2.park.dyn.beat, D2, st2.park.escape), false, 'the SEA froze him and the render would have painted it as a toll he took for the walker'); } // ---- (H) THE TWO RENDER DECISIONS THAT ARE NOT CANVAS. I first deferred these as "canvas code // this suite cannot execute", and a reviewer pointed out that my own commit refuted me: the // DECISION need not live in the canvas, which is exactly why _parkEscapeFreezeByPull was lifted // in the round before. Both were one lift away from being runnable, so both are lifted and run. // Hard-wiring `looking` to true, or dropping the `looking &&` from the squint, were confirmed // ungated before this leg existed. { // (H1) THE SQUINT belongs to the doll that CAUGHT the step, not to both of them. const squint = new Function('return (' + cut('_parkEscapeSquint') + ');')(); const seenFx = [{ k: 'seen', x: 1, y: 1 }]; assert.strictEqual(squint(true, seenFx), true, 'the looking doll reacts to a step it caught'); assert.strictEqual(squint(false, seenFx), false, 'the doll with its EYES CLOSED squinted at a toll it had no part in — the two hues exist so a ' + 'player can read WHICH ONE bit him, and a shared reaction throws exactly that away'); assert.strictEqual(squint(true, []), false, 'nothing was caught, so nothing to squint at'); assert.strictEqual(squint(true, [{ k: 'swept', x: 1, y: 1 }]), false, 'the WATER taking the ground under him is not a doll catching him'); assert.strictEqual(squint(true, undefined), false, 'a board with no fx list must not throw'); } { // (H2) THE HALO'S SOURCE IS THE ENGINE'S PREDICATE, checked by RUNNING the painter's read // against a real board at beats that disagree — not by a regex about which name it types. const looks = new Function('E', 'return (' + cut('_parkEscapeLooks') + ');')(E); const st = E._parkEscapeBuild({ seed: 1 }); let agreed = 0, differed = 0; for (let b = 0; b < 12; b++) { st.park.dyn.beat = b; const got = looks(st); assert.strictEqual(got.a, E._parkEscapeGazeA(st), `beat ${b}: doll A's halo disagrees with the module's own gaze predicate — the second priced ` + 'danger on this board has two sources, and the drawn danger is not the billed one'); assert.strictEqual(got.b, E._parkEscapeGazeB(st), `beat ${b}: doll B's halo disagrees`); if (got.a === got.b) agreed++; else differed++; } assert.ok(differed > 0 && agreed > 0, 'CONTROL: over 12 beats the two dolls must both agree sometimes and DIFFER sometimes, or this ' + 'leg would pass just as well against a read that returned one flag for both of them'); // AND IT MUST BE THE ENGINE'S SOURCE, not a formula that merely agrees with it. A `ph >= sing` // re-derived off the BOARD gives the same answer on every shipped seed, so the assertions above // cannot tell the two apart. Force them apart: tamper the board's own clock (a local copy, no // constant is changed) and pick a beat where the two formulas must differ. Whoever the read // follows here is where the halo's price really comes from. const st2 = E._parkEscapeBuild({ seed: 1 }); st2.park.escape.a.sing = E.PARK_ESCAPE_SING + 1; st2.park.dyn.beat = E.PARK_ESCAPE_SING; const boardSays = (st2.park.dyn.beat % (st2.park.escape.a.sing + st2.park.escape.a.gaze)) >= st2.park.escape.a.sing; assert.notStrictEqual(boardSays, E._parkEscapeGazeA(st2), 'CONTROL: the tampered board and the module must actually disagree at this beat, or the ' + 'assertion below cannot tell a re-derivation from the real source'); assert.strictEqual(looks(st2).a, E._parkEscapeGazeA(st2), "doll A's halo followed the BOARD's clock rather than the module's gaze predicate. Today those " + 'agree, so nothing on screen would look wrong — until a per-board clock is tuned, at which ' + 'point the drawn danger and the billed danger part company in silence'); } // ---- (J) THE WIRING OF THOSE DECISIONS INTO THE PAINTER. (H) proves the decisions are right; // it says nothing about whether the painter still ASKS them. Hard-wiring at the call site — // `doll(..., true)`, `_parkEscapeSquint(true, st.fx)`, `freezeHue = PARK_HUES.companion` — left // (H) green, so the mutation the review literally named was still uncovered one line from where I // lifted it. A golden over those lines would only move the boundary again (the whole lesson of // this task), so the painter is RUN: cut it out of app.js and drive it against a recording 2d // context, with `_parkActor` and the two glyph helpers as SPIES. What reaches _parkActor's hue and // squint arguments IS what the player sees, so this is the wiring, not a description of it. { const hue = (name) => { const m = new RegExp('const ' + name + " = '(#[0-9a-fA-F]+)'").exec(src); assert.ok(m, `ESCAPE-CLICK-SEAM cannot read ${name} out of app.js`); return m[1]; }; const HUE_A = hue('PARK_ESCAPE_HUE_A'), HUE_B = hue('PARK_ESCAPE_HUE_B'); assert.notStrictEqual(HUE_A, HUE_B, 'the two dolls share one hue — the whole point of two clocks is that a player can see WHICH ' + 'one bit him'); // a 2d context that records nothing and refuses nothing: the painter's marks are not the subject // here, its QUESTIONS are. const noop = () => {}; const bx = new Proxy({}, { get: (t, k) => (k in t ? t[k] : noop), set: (t, k, v) => (t[k] = v, true) }); const actors = [], hearts = []; const paint = new Function('bx', 'E', '_pulseGlow', '_alpha', '_parkActor', 'PARK_HUES', 'PARK_ESCAPE_HUE_A', 'PARK_ESCAPE_HUE_B', '_parkEscapeCrackedHeart', '_parkEscapeSplash', '_parkEscapeFreezeByPull', '_parkEscapeSquint', '_parkEscapeLooks', 'return (' + cut('_paintParkEscape') + ');')( bx, E, () => 0.5, (h) => h, (c, cx, cy, r, h, ax, ay, squint) => actors.push({ hue: h, squint }), { companion: '#c85ce0' }, HUE_A, HUE_B, (cx, cy, s, h) => hearts.push(h), noop, new Function('return (' + cut('_parkEscapeFreezeByPull') + ');')(), new Function('return (' + cut('_parkEscapeSquint') + ');')(), new Function('E', 'return (' + cut('_parkEscapeLooks') + ');')(E)); // (J1) a beat where the two dolls DISAGREE, with a caught step on the board. const st = E._parkEscapeBuild({ seed: 1 }); st.park.dyn.beat = E.PARK_ESCAPE_SING; // A has just opened its eyes; B's phase differs assert.notStrictEqual(E._parkEscapeGazeA(st), E._parkEscapeGazeB(st), 'CONTROL: this leg needs a beat the two dolls disagree on, or a hard-wired `true` is invisible'); st.fx = [{ k: 'seen', x: 1, y: 1 }]; actors.length = 0; paint(st, 0, 0, 40); assert.strictEqual(actors.length, 2, 'the painter must draw exactly the two dolls'); const want = [E._parkEscapeGazeA(st), E._parkEscapeGazeB(st)]; const hues = [HUE_A, HUE_B]; for (let i = 0; i < 2; i++) { assert.strictEqual(actors[i].hue, want[i] ? hues[i] : '#7a6a55', `doll ${'AB'[i]} was painted ${want[i] ? 'cold while its eyes are OPEN' : 'lit while its eyes ' + 'are CLOSED'} — the halo the painter draws is not the gaze the engine bills`); assert.strictEqual(actors[i].squint > 0, want[i], `doll ${'AB'[i]} ${want[i] ? 'did not react to a step it caught' : 'squinted at a toll it had ' + 'no part in'} — the squint is wired past _parkEscapeSquint`); } // (J2) a beat NEITHER doll is looking on: nothing may be lit, and nothing may squint. st.park.dyn.beat = 0; assert.strictEqual(E._parkEscapeGazeA(st) || E._parkEscapeGazeB(st), false, 'CONTROL: beat 0 must be a beat both dolls sing through'); actors.length = 0; paint(st, 0, 0, 40); assert.deepStrictEqual(actors.map(a => a.hue), ['#7a6a55', '#7a6a55'], 'a doll is lit on a beat no eye is open — the rim promises a toll the engine will not charge'); assert.deepStrictEqual(actors.map(a => a.squint > 0), [false, false], 'and neither may squint'); // (J3) THE FREEZE HUE reaches the glyph: a freeze the SEA caused must not be painted magenta. const st3 = E._parkEscapeBuild({ seed: 1 }); const P3 = E.parkStart(st3), D3 = st3.park.dyn.escape; const line = E._parkEscapeNext(st3); st3.pos[1] = { x: line[0] % st3.N, y: (line[0] / st3.N) | 0 }; st3.park.dyn.beat = E.PARK_ESCAPE_EVERY; E.PARK_FIELD_MECHS.escape.tick(P3); assert.ok(D3.mateSwept > 0 && D3.mateStun > 0 && D3.shieldBeat < 0, 'CONTROL: the sea must have frozen him and no pull may have happened, or this proves nothing'); st3.fx = []; // the freeze glyph is the only heart in frame hearts.length = 0; paint(st3, 0, 0, 40); assert.strictEqual(hearts.length, 1, 'exactly one cracked heart — his freeze — should be drawn'); assert.notStrictEqual(hearts[0], '#c85ce0', 'a freeze the WATER caused was painted in the companion magenta the PULL owns — the player is ' + 'shown a toll he took for the walker when nobody paid anything'); } }); /* ---- GATE: CAMP-SEAT-ADMITS — a seated cell its own module rejects (Task 6, 2026-07-27) -------- * THE HOLE THIS FILLS. Every picker slot that names a field mechanic is seated on a PLAY CELL, and * every such module owns an `admits()` predicate that says whether that cell stages the scene the * module was built to measure. Nothing checked that the two agreed. The loud counter that looks like * it would — parkCrossFallbacks — is STRUCTURALLY PINNED AT 0 for any ship:false slot * (campaign.js:3672, `if (found < 0 && ship)`), by design: a preview asserts no transfer guarantee, * so the incongruence filter is skipped and its exhaustion is never counted. The consequence is that * a preview can be seated on a board its own module calls unplayable and every gate stays green. * MEASURED 2026-07-27 (seeds 1, 11, 23; 24 field slots per seed): true on 23 of 24, and y51 escape * was the one that was false — on all three seeds — because its module admission is 0/40. * * THE RECORDED SET IS EMPTY AGAIN (2026-07-28). y51 was UNSEATED: FIELD-REGISTRY-SURFACE refuses to * let a 0/40-admission mechanic be named by any slot at all, so the one entry this set ever carried * left the board with its tile. 23 field slots x 3 seeds, and every one of them is admitted. * * WHY AN EMPTY GOLDEN STILL HAS TEETH — the thing to check before believing this gate. The * assertion is `badBySeed[seed] deepEquals RECORDED`, so with RECORDED empty it reads "no seated * field slot is rejected by its own module", which is a claim that CAN fail — and it was MADE to * fail before being believed: with RECORDED already `[]`, the y51 slot row was put back into * PARK_CROSSINGS by hand and this gate went red on all three seeds (`[y51]` vs `[]`, 69/72 * admitted), then the row was taken back out and campaign.js sha256-compared to its saved copy. * 2026-07-28. What an empty golden * loses is only the OTHER direction — with no id recorded there is nothing left to celebrate * shrinking — and that direction is moot precisely because nothing is recorded. The three * non-vacuity guards below are what keep the empty case from going hollow, and all three still * bind: `inspected === fieldSlots.length * SEEDS.length` (69 = 23 x 3), `shippedSeen > 0`, * `previewsAdmitted > 0`. * * WHY A GOLDEN SET AND NOT `bad.length === 0` — the mechanism stays, even at zero entries. When a * known, documented, deliberate preview failure exists, a gate that simply demanded an empty set * would have to be committed RED, and a red gate is a gate someone turns off; a gate that exempted * the slot BY NAME could never fail FOR it again — the bar-that-cannot-fail this project has paid * for twice. The set-equality shape is what lets a future failure be recorded WITHOUT either. Seat * triage (spec 2026-07-22) says an unmeasurable cell may still ship=false/open=true and be played by * hand — this gate does not contradict that; it only insists the fact be written down in exactly one * place. (For y51 that place is now campaign.js's PARK_CROSSINGS note plus * docs/superpowers/plans/2026-07-27-y51-measurements.md, because the slot itself is gone.) */ test('CAMP-SEAT-ADMITS: the set of seated field slots their own module rejects is exactly the recorded one', () => { const SEEDS = [1, 11, 23]; // CAMP-CROSS-SWEEP's own seed convention // THE RECORDED SET. Slot ids whose seated play cell its own module's admits() currently refuses. // Shrinking it is the goal; growing it is a defect. Never add an id without a measurement. const RECORDED = []; // empty since 2026-07-28 — see the header // note on why an empty golden still bites. const fieldSlots = CAMP.PARK_CROSSINGS.filter(c => c.playMech && c.playMech.fieldMech).map(c => c.id); assert.ok(fieldSlots.length > 0, 'no slot names a field mechanic — this gate would be vacuous'); let inspected = 0, admitted = 0, previewsAdmitted = 0, shippedSeen = 0; const badBySeed = {}; for (const seed of SEEDS) { const bad = []; for (const cx of CAMP.parkCrossings(seed)) { const fv = cx.playMech && cx.playMech.fieldMech; if (!fv) continue; const m = E.PARK_FIELD_MECHS[fv]; assert.ok(m && typeof m.admits === 'function', `slot ${cx.slot} names field mechanic '${fv}' whose registry entry has no admits() — that is ` + 'the y32 registry blind spot: a missing admits key makes every sweep skip silently and report ' + 'a clean zero. Wire it before seating the slot.'); inspected++; if (cx.ship) shippedSeen++; if (m.admits(cx.playCell)) { admitted++; if (!cx.ship) previewsAdmitted++; } else bad.push(cx.slot); } badBySeed[seed] = bad.slice().sort(); } console.log(` [CAMP-SEAT-ADMITS] ${fieldSlots.length} field slots x ${SEEDS.length} seeds = ${inspected} ` + `seatings; admits(playCell) true on ${admitted}/${inspected} ` + `(${previewsAdmitted} of them PREVIEWS, ${shippedSeen} shipped seatings inspected); ` + SEEDS.map(s => `seed ${s}: [${badBySeed[s].join(',')}]`).join(' ')); // NON-VACUITY, both halves. The loop must have run, and it must have run on cells of BOTH kinds — // a check that only ever looked at shipped slots could not see the hole it exists to cover. assert.strictEqual(inspected, fieldSlots.length * SEEDS.length, 'the sweep did not inspect every field slot on every seed — the count below is then a partial read'); assert.ok(shippedSeen > 0, 'no SHIPPED seating was inspected — the control half of this gate is missing'); assert.ok(previewsAdmitted > 0, 'not one PREVIEW slot passed — then this gate cannot tell "previews are exempt from admission" ' + 'from "previews happen to be admitted", and the recorded set below means nothing'); for (const seed of SEEDS) { assert.deepStrictEqual(badBySeed[seed], RECORDED.slice().sort(), `seed ${seed}: the seated field slots their own module REJECTS are [${badBySeed[seed].join(',')}], ` + `but the recorded set is [${RECORDED.join(',')}]. If a slot was ADDED to the failures, its picker ` + 'seat rides a board its module calls unplayable and parkCrossFallbacks will never say so — fix ' + 'the slot, do not extend the set without a measurement beside it. If a slot LEFT the failures, ' + 'its module now admits its seated cell: delete the id from RECORDED (and re-measure its ship bar ' + '— an admitting preview is a promotion candidate).'); } }); /* ==== Y53 BEACON (plan 2026-07-27) — the rotating-cone hybrid. Four gates: the clock, the yard, * the toll, the admission. The clock and the cone are PURE reads of a public beat, so they can be * pinned by poking dyn.beat directly; the toll and the judge are driven through real parkStep runs * because that is the only path a player takes. */ test('Y53-BEACON-CLOCK: the beam holds a quadrant for a span, turn then stare, and comes home in a period', () => { const st = E._parkBeaconBuild(E._parkBeaconCell(7)); const B = st.park.beacon; const f0 = E._parkBeaconFacing(st); const SPAN = E.PARK_BEACON_SPAN, AT = E.PARK_BEACON_STARE_AT; assert.strictEqual(st.park.dyn.beat, 0, 'a fresh board starts on beat 0'); assert.strictEqual(E._parkBeaconTurning(st), true, 'beat 0 of a span is the head-turn'); assert.strictEqual(E._parkBeaconStaring(st), AT === 0); assert.strictEqual(E._parkBeaconTillStare(st), AT); // walk the whole span: exactly one turn beat, exactly one stare beat, one facing throughout let turns = 0, stares = 0; for (let b = 0; b < SPAN; b++) { st.park.dyn.beat = b; if (E._parkBeaconTurning(st)) turns++; if (E._parkBeaconStaring(st)) stares++; assert.strictEqual(E._parkBeaconFacing(st), f0, 'the facing holds across its own span'); } assert.strictEqual(turns, 1, 'one head-turn per span'); assert.strictEqual(stares, 1, 'one stare per span — the single billing beat'); st.park.dyn.beat = AT; assert.strictEqual(E._parkBeaconStaring(st), true, 'the stare sits where STARE_AT says'); assert.strictEqual(E._parkBeaconTillStare(st), 0); st.park.dyn.beat = SPAN; assert.strictEqual(E._parkBeaconFacing(st), (f0 + B.spin + 4) % 4, 'the next span has rotated by spin'); assert.strictEqual(E._parkBeaconNextFace(st), (f0 + 2 * B.spin + 8) % 4, 'and the preview is one further'); st.park.dyn.beat = E.PARK_BEACON_PERIOD; assert.strictEqual(E._parkBeaconFacing(st), f0, 'a full period returns home'); assert.strictEqual(st.park.dyn.beacon.rotations, 0, 'the clock reads advance nothing'); // the reads are total: a board with no beacon fixture must not throw (other boards call nothing, // but the painter and the gates both probe defensively). assert.strictEqual(E._parkBeaconFacing({ park: {} }), 0); }); test('Y53-BEACON-LAYOUT: seed-pure yard, a walled tower, four cones and the dark spokes, five distinct anchors', () => { for (const seed of [1, 7, 23]) { const a = E._parkBeaconBuild(E._parkBeaconCell(seed)); const b = E._parkBeaconBuild(E._parkBeaconCell(seed)); assert.strictEqual(JSON.stringify(a.park.beacon), JSON.stringify(b.park.beacon), 'seed ' + seed + ': the layout is a pure function of the seed (C1)'); const n = a.N, B = a.park.beacon; assert.ok(a.wall.has(B.towerKey), 'the tower cell is a wall'); assert.strictEqual(B.towerKey, ((n - 1) >> 1) * n + ((n - 1) >> 1), 'and it stands at the center'); const cone = E._parkBeaconCone(a, E._parkBeaconFacing(a)); assert.ok(cone.size > 0 && cone.size < a.park.walkway.size, 'the lit quadrant is a proper part of the yard — never all of it, never none'); assert.ok(!cone.has(B.towerKey)); for (const k of cone) assert.ok(!a.wall.has(k), 'walls are never lit'); // THE FOUR CONES NEVER OVERLAP, AND THEY NEVER COVER: the strict inequality leaves four dark // diagonal spokes running from the tower to the corners, which is where a caution mind stands. const counts = new Map(); for (let f = 0; f < 4; f++) for (const k of E._parkBeaconCone(a, f)) counts.set(k, (counts.get(k) || 0) + 1); for (const [, c] of counts) assert.strictEqual(c, 1, 'no cell is lit by two facings at once'); assert.ok(counts.size < a.park.walkway.size, 'the lit cells are a proper part of the yard'); const dark = [...a.park.walkway].filter(k => !counts.has(k)); assert.ok(dark.length >= 4, 'the dark spokes exist — somewhere to stand while the beam passes'); // every dark cell is on a diagonal of the tower (|dx| === |dy|), tower excluded. const cx = B.towerKey % n, cy = (B.towerKey / n) | 0; for (const k of dark) { const dx = Math.abs((k % n) - cx), dy = Math.abs(((k / n) | 0) - cy); assert.strictEqual(dx, dy, 'a cell no facing lights is a diagonal cell'); } // five anchors: spawn, station, retire, finish, contract gem — distinct and on the walkway. const key = (p) => p.y * n + p.x; const ids = [key(a.park.spawn), key(a.park.companionSpawn), key(a.park.retire), B.finishKey, key(a.park.clusters[1])]; assert.strictEqual(new Set(ids).size, ids.length, 'seed ' + seed + ': the anchors are distinct'); for (const k of ids) assert.ok(!a.wall.has(k), 'the anchors are on the walkway'); // and the finish is reachable around the tower, ignoring the clock (walls only). const dist = new Array(n * n).fill(Infinity); const q = [ids[0]]; dist[ids[0]] = 0; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const d of [{ x: 0, y: -1 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 1, y: 0 }]) { const nx = x + d.x, ny = y + d.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (a.wall.has(nk) || dist[nk] <= dist[k] + 1) continue; dist[nk] = dist[k] + 1; q.push(nk); } } assert.ok(isFinite(dist[B.finishKey]), 'seed ' + seed + ': the finish is reachable around the tower'); assert.ok(isFinite(dist[ids[4]]), 'and so is his contract gem'); } }); test('Y53-BEACON-TOLL: the stare bills a lit step once; the turn, the dark and staying still are free', () => { const st = E._parkBeaconBuild(E._parkBeaconCell(7)); const P = E.parkStart(st); const D = st.park.dyn.beacon; const M = E.PARK_FIELD_MECHS.beacon; const key = (p) => p.y * st.N + p.x; const litStep = () => { const cone = E._parkBeaconCone(st, E._parkBeaconFacing(st)); return [...cone][0]; }; // ① a step with BOTH ends dark, on the stare beat: free. st.park.dyn.beat = E.PARK_BEACON_STARE_AT; assert.strictEqual(E._parkBeaconStaring(st), true); const cone = E._parkBeaconCone(st, E._parkBeaconFacing(st)); const darkA = [...st.park.walkway].find(k => !cone.has(k)); const before0 = P.hearts; M.onLeave(P, { fromKey: darkA, toKey: darkA + 0, from: { x: 0, y: 0 }, to: { x: 0, y: 0 } }); assert.strictEqual(P.hearts, before0, 'a step that never touches the light is free'); assert.strictEqual(D.spotted.length, 0); // ② the SAME step out of a lit cell, on the stare beat: one heart, one confession. const lk = litStep(); const before1 = P.hearts; M.onLeave(P, { fromKey: lk, toKey: darkA, from: { x: lk % st.N, y: (lk / st.N) | 0 }, to: { x: darkA % st.N, y: (darkA / st.N) | 0 } }); assert.strictEqual(P.hearts, before1 - 1, 'leaving the light under the stare costs exactly one heart'); assert.strictEqual(D.spotted.length, 1, 'and it is confessed once'); assert.strictEqual(D.spotted[0].beat, st.park.dyn.beat, 'on the beat it happened'); // ③ the same lit step on the head-TURN beat: free (the telegraph never bills). st.park.dyn.beat = 0; assert.strictEqual(E._parkBeaconTurning(st), true); const before2 = P.hearts; M.onLeave(P, { fromKey: lk, toKey: darkA, from: { x: lk % st.N, y: (lk / st.N) | 0 }, to: { x: darkA % st.N, y: (darkA / st.N) | 0 } }); assert.strictEqual(P.hearts, before2, 'the head-turn is a telegraph, not a toll'); assert.strictEqual(D.spotted.length, 1); // ④ staying still is innocent even standing in the light under the stare. st.park.dyn.beat = E.PARK_BEACON_STARE_AT; const before3 = P.hearts; M.onLeave(P, { fromKey: lk, toKey: lk, from: { x: 0, y: 0 }, to: { x: 0, y: 0 } }); assert.strictEqual(P.hearts, before3, "'stay' is always innocent — freezing is the caution answer"); assert.strictEqual(D.spotted.length, 1); assert.ok(key(st.pos[0]) >= 0); }); test('Y53-BEACON-SHEPHERD: the light takes an unheld companion; a friend at his shoulder is the exemption', () => { const M = E.PARK_FIELD_MECHS.beacon; const setup = (adjacent) => { const st = E._parkBeaconBuild(E._parkBeaconCell(7)); const P = E.parkStart(st); const D = st.park.dyn.beacon; // put him in the lit quadrant of the facing we are about to declare PREVIOUS const face = E._parkBeaconFacing(st); const cone = [...E._parkBeaconCone(st, face)]; const k = cone[(cone.length / 2) | 0]; st.pos[1] = { x: k % st.N, y: (k / st.N) | 0 }; st.pos[0] = adjacent ? { x: st.pos[1].x, y: st.pos[1].y } : { x: st.park.spawn.x, y: st.park.spawn.y }; if (adjacent) { // one cell away, inside the board const nx = st.pos[1].x + 1 < st.N - 1 ? st.pos[1].x + 1 : st.pos[1].x - 1; st.pos[0] = { x: nx, y: st.pos[1].y }; } D.starePrev = true; D.facePrev = face; D.matePrev = { x: st.pos[1].x, y: st.pos[1].y }; M.tick(P, { mvKey: 'stay' }); return D; }; const alone = setup(false); assert.strictEqual(alone.mateCaught, 1, 'standing in the light with nobody near, he is taken'); assert.strictEqual(alone.mateStun, E.PARK_BEACON_STUN, 'and frozen for exactly STUN beats'); assert.strictEqual(alone.holds.length, 0, 'no hold is claimed when nobody was there'); const held = setup(true); assert.strictEqual(held.mateCaught, 0, 'a friend at his shoulder is the whole exemption'); assert.strictEqual(held.holds.length, 1, 'and the act is confessed'); assert.strictEqual(held.mateStun, 0); }); test('Y53-BEACON-ADMIT: measured seeds admit, and the reject histogram stays clean on them', () => { // seeds 16 and 20 are two of the 11 that admit over 1..40 (measured 2026-07-27: 3,4,16,19,20,25, // 28,30,32,33,37; the full sweep and its histogram live in the harness beside the plan, not in // this gate's runtime budget). NOTE the module SHIP seed (7) is deliberately not one of them — // admission and the order-recovery bar are different predicates over different questions, and a // cell can be readable without being one the campaign would seat. const before = E.parkBeaconWhys(); for (const seed of [16, 20]) { assert.strictEqual(E._parkBeaconAdmissible(E._parkBeaconCell(seed)), true, 'seed ' + seed + ' admits'); } const after = E.parkBeaconWhys(); for (const k of Object.keys(after)) assert.strictEqual(after[k] - before[k], 0, 'reject reason ' + k + ' must not fire on a seed that admits'); // and the module bar is DERIVED, never asserted. assert.strictEqual(E.PARK_BEACON_SHIPPABLE, E._parkBeaconRecovers(E._parkBeaconCell(E.PARK_BEACON_SHIP_SEED)), 'the module pin must BE the predicate, not a copy of what it once returned'); }); /* ==== Y53-BEACON-SHIP-GATE (2026-07-27) — y53 GOES LIVE, and this gate is what makes the word * mean something. Two bars, two DIFFERENT BOARDS (the y29 confusion, named again so nobody * re-conflates them): * MODULE bar E.PARK_BEACON_SHIPPABLE — calibrated 6/6 blind order recovery on the module's own * shipped-seed cell, read at PARK_CAL_TURNS (the skip the product's readout uses). * PAIRING bar CAMP.PARK_Y53_SHIPPABLE — the crossing's own board: faithful 6/6 AND the surface * mimic recovering NOTHING. * Both are derived calls, never literals, and the slot flag must equal the pairing measurement. * NON-VACUITY: the mimic leg must have PROBED (probes > 0) — a short-circuiting recovery that never * reached the imitator would report "0 leaks" having tested nothing. * WHAT THIS CELL SHIPS WITH, stated rather than rounded up: pairing 22 of 24 seeds and 142 of 144 * crossing runs. The two shortfalls are the same persona on seeds 9 and 18 and both recover at * skip 0 — evidence inside the calibration span, not a misreading. Misreads are ZERO everywhere. */ test('Y53-BEACON-SHIP-GATE: both bars are derived, both hold, and the slot flag equals the pairing measurement', () => { const slot = CAMP.PARK_CROSSINGS.find(c => c.id === 'y53'); assert.ok(slot, 'the y53 slot vanished from the registry'); assert.strictEqual(slot.ship, CAMP.PARK_Y53_SHIPPABLE, 'the y53 slot flag and its PAIRING measurement disagree — ship:true is earned by the measurement ' + 'or it is not earned at all (derive-never-assert)'); assert.strictEqual(slot.ship, true, 'y53 ships as of 2026-07-27'); assert.ok(!slot.open, 'a measured crossing is not a marked preview — `open` must be gone, not false'); assert.strictEqual(slot.kind, 'm3', 'y53 is measured on C-vs-N. The kind was chosen by measurement, not by analogy: at m1 the ' + 'imitator leaks 10 and at m2 it leaks 25, because goal-vs-safety and goal-vs-care are the two ' + 'pairs a gem-greedy copier expresses by accident on this yard.'); assert.strictEqual(E.PARK_BEACON_SHIPPABLE, true, 'the MODULE bar must hold too, and it is derived'); assert.strictEqual(E._parkBeaconRecovers(E._parkBeaconCell(E.PARK_BEACON_SHIP_SEED)), E.PARK_BEACON_SHIPPABLE, 're-running the module predicate must reproduce the pin — a pin that drifted from its predicate ' + 'is a literal wearing a derivation'); assert.strictEqual(CAMP._parkY53Recovers(), CAMP.PARK_Y53_SHIPPABLE, 're-running the pairing predicate must reproduce the pin'); // the pairing bar, RECOMPUTED here over seeds 1..8 rather than trusted from the pin let pair = 0; for (let s = 1; s <= 8; s++) if (CAMP._parkY53Recovers(s)) pair++; assert.strictEqual(pair, 8, `crossing pairing recovery is ${pair}/8 over seeds 1..8 — it was 8/8 ` + 'when y53 shipped, so a drop here means the boards moved under the claim'); // the imitator control, reported SEPARATELY from the verdict and with its non-vacuity clause const mim = { probes: 0, expressed: 0, leaks: 0 }; for (let s = 1; s <= 8; s++) { const r = CAMP._parkY53MimicLeaks(s); mim.probes += r.probes; mim.expressed += r.expressed; mim.leaks += r.leaks; } assert.ok(mim.probes > 0, 'the mimic leg never probed — "0 leaks" off a loop that never ran'); assert.strictEqual(mim.leaks, 0, `the surface imitator recovered the pair on ${mim.leaks} probes — ` + 'a cell a copier can read is not measuring what it claims to measure'); console.log(` [Y53-BEACON-SHIP-GATE] module pin true (seed ${E.PARK_BEACON_SHIP_SEED}), ` + `pairing ${pair}/8, mimic probes ${mim.probes} expressed ${mim.expressed} leaks ${mim.leaks}`); }); /* ==== Y52 BURST (plan 2026-07-27) — the water-balloon yard. Four gates: the clock and the cross, * the toll that rides the STEP, the bubble only a body can pop, and the two ship bars. */ test('Y52-BURST-CLOCK: fuses are a public countdown; the cross stops at walls; the yard is seed-pure', () => { for (const seed of [1, 8, 16]) { const a = E._parkBurstBuild(E._parkBurstCell(seed)); const b = E._parkBurstBuild(E._parkBurstCell(seed)); assert.strictEqual(JSON.stringify(a.park.burst), JSON.stringify(b.park.burst), 'seed ' + seed + ': the layout is a pure function of the seed (C1)'); assert.ok(E._parkBurstLayoutOk(a), 'seed ' + seed + ': the builder accepts its own board'); const B = a.park.burst; assert.ok(B.balloons.length >= 3, 'the balloons were actually seated'); for (const o of B.balloons) { assert.ok(a.wall.has(o.y * a.N + o.x), 'a balloon is a wall'); assert.ok(o.phase >= 0 && o.phase < E.PARK_BURST_PERIOD, 'its fuse lives inside the period'); const arms = E._parkBurstArms(a, o); assert.ok(arms.has(o.y * a.N + o.x), 'the cross includes the balloon itself'); for (const k of arms) { if (k === o.y * a.N + o.x) continue; assert.ok(!a.wall.has(k), 'the water never lands inside a wall — the arm stops there'); const dx = Math.abs((k % a.N) - o.x), dy = Math.abs(((k / a.N) | 0) - o.y); assert.ok((dx === 0 || dy === 0) && dx + dy <= E.PARK_BURST_ARM, 'the cross is axis-aligned and no longer than ARM'); } } // the fuse counts down and reaches zero exactly on the bursting beat for (const o of B.balloons) { a.park.dyn.beat = o.phase; assert.strictEqual(E._parkBurstFuse(a, o), 0, 'the fuse is spent on the beat it bursts'); assert.ok(E._parkBurstDue(a, o.phase).some(q => q.x === o.x && q.y === o.y)); a.park.dyn.beat = (o.phase + 1) % E.PARK_BURST_PERIOD; assert.strictEqual(E._parkBurstFuse(a, o), E.PARK_BURST_PERIOD - 1, 'and starts over'); } } }); test('Y52-BURST-TOLL: the water bills a STEP across the cross; crouching and dry ground are free', () => { const st = E._parkBurstBuild(E._parkBurstCell(8)); const P = E.parkStart(st); const D = st.park.dyn.burst, M = E.PARK_FIELD_MECHS.burst; const o = st.park.burst.balloons[0]; st.park.dyn.beat = o.phase; const hot = [...E._parkBurstHot(st, st.park.dyn.beat)]; assert.ok(hot.length > 0, 'the beat under test is a bursting beat'); const dry = [...st.park.walkway].find(k => !E._parkBurstHot(st, st.park.dyn.beat).has(k)); const wet = hot.find(k => !st.wall.has(k)); const at = (k) => ({ x: k % st.N, y: (k / st.N) | 0 }); // ① dry to dry: free let h = P.hearts; M.onLeave(P, { fromKey: dry, toKey: dry, from: at(dry), to: at(dry) }); assert.strictEqual(P.hearts, h, 'a step that never touches the water is free'); // ② crouching in the water: free h = P.hearts; M.onLeave(P, { fromKey: wet, toKey: wet, from: at(wet), to: at(wet) }); assert.strictEqual(P.hearts, h, "'stay' is innocent even standing in the cross — crouch and it passes"); assert.strictEqual(D.singed.length, 0); // ③ stepping out of the water on the bursting beat: one heart, one confession h = P.hearts; M.onLeave(P, { fromKey: wet, toKey: dry, from: at(wet), to: at(dry) }); assert.strictEqual(P.hearts, h - 1, 'a step across the bursting cross costs exactly one heart'); assert.strictEqual(D.singed.length, 1, 'and it is confessed once, with its beat'); assert.strictEqual(D.singed[0].beat, st.park.dyn.beat); // ④ the same step on a quiet beat: free st.park.dyn.beat = (o.phase + 1) % E.PARK_BURST_PERIOD; if (E._parkBurstHot(st, st.park.dyn.beat).size === 0) { h = P.hearts; M.onLeave(P, { fromKey: wet, toKey: dry, from: at(wet), to: at(dry) }); assert.strictEqual(P.hearts, h, 'no burst, no bill'); assert.strictEqual(D.singed.length, 1); } }); test('Y52-BURST-BUBBLE: he cannot move until a body pops it, and popping spends the step not the ground', () => { const st = E._parkBurstBuild(E._parkBurstCell(8)); const P = E.parkStart(st); const D = st.park.dyn.burst, M = E.PARK_FIELD_MECHS.burst; assert.strictEqual(D.bubbled, true, 'he starts inside one'); // his whole domain is frozen; the walker's is untouched (y3's grammar — a bubble is not a wall) const somewhere = [...st.park.walkway][0]; assert.strictEqual(M.legalMask(P, somewhere, 'mate'), true, 'every cell is shut to him'); assert.strictEqual(M.legalMask(P, somewhere, 'me'), false, 'and none of them to the walker'); assert.strictEqual(M.legalMask(P, somewhere, 'route'), false, 'the route metric still prices the yard'); // his cell is the one additive lane, and only for the walker const his = E._parkKey(st, st.pos[1]); assert.strictEqual(M.legalAdd(P, his, 'me'), true, 'the bubble is enterable — that is what legalAdd is for'); assert.strictEqual(M.legalAdd(P, his, 'mate'), false); assert.strictEqual(M.legalAdd(P, somewhere, 'me'), false, 'and nothing else is opened'); // walking in pops it and puts the walker back where he came from const from = { x: st.pos[0].x, y: st.pos[0].y }; M.onEnter(P, { mvKey: 'U', fromKey: E._parkKey(st, from), toKey: his, from, to: { ...st.pos[1] } }); assert.strictEqual(D.bubbled, false, 'the bubble is gone'); assert.strictEqual(D.rescues, 1, 'the deed is counted'); assert.strictEqual(D.freeBeat, st.park.dyn.beat, 'and stamped with the beat it happened on'); assert.deepStrictEqual(st.pos[0], from, 'the step was spent on the deed, not on the ground'); assert.strictEqual(M.legalAdd(P, his, 'me'), false, 'a popped bubble is not a door'); }); test('Y52-BURST-SHIP-GATE: both bars are derived, both hold, and the slot flag equals the pairing measurement', () => { const slot = CAMP.PARK_CROSSINGS.find(c => c.id === 'y52'); assert.ok(slot, 'the y52 slot vanished from the registry'); assert.strictEqual(slot.ship, CAMP.PARK_Y52_SHIPPABLE, 'the y52 slot flag and its PAIRING measurement disagree — ship:true is earned or it is not earned'); assert.strictEqual(slot.ship, true, 'y52 ships as of 2026-07-27'); assert.ok(!slot.open, 'a measured crossing is not a marked preview — `open` must be gone, not false'); assert.strictEqual(slot.kind, 'm3', 'y52 is measured on C-vs-N, chosen by measurement: m1 and m2 both pair-recover 0/24 here and ' + 'leak on 72 of 144 imitator probes.'); assert.strictEqual(E.PARK_BURST_SHIPPABLE, true, 'the MODULE bar must hold too, and it is derived'); assert.strictEqual(E._parkBurstRecovers(E._parkBurstCell(E.PARK_BURST_SHIP_SEED)), E.PARK_BURST_SHIPPABLE, 're-running the module predicate must reproduce the pin'); assert.strictEqual(CAMP._parkY52Recovers(), CAMP.PARK_Y52_SHIPPABLE, 're-running the pairing predicate must reproduce the pin'); let pair = 0; for (let s = 1; s <= 8; s++) if (CAMP._parkY52Recovers(s)) pair++; assert.strictEqual(pair, 8, `crossing pairing recovery is ${pair}/8 over seeds 1..8 — it was 8/8 ` + '(24/24 over 1..24) when y52 shipped'); const mim = { probes: 0, expressed: 0, leaks: 0 }; for (let s = 1; s <= 8; s++) { const r = CAMP._parkY52MimicLeaks(s); mim.probes += r.probes; mim.expressed += r.expressed; mim.leaks += r.leaks; } assert.ok(mim.probes > 0, 'the mimic leg never probed — "0 leaks" off a loop that never ran'); assert.strictEqual(mim.leaks, 0, `the surface imitator recovered the pair on ${mim.leaks} probes`); console.log(` [Y52-BURST-SHIP-GATE] module pin true (seed ${E.PARK_BURST_SHIP_SEED}), ` + `pairing ${pair}/8, mimic probes ${mim.probes} expressed ${mim.expressed} leaks ${mim.leaks}`); }); /* Y52-DEMO-LEGIBLE (2026-08-01) — the demonstration must TEACH the ruler this seat is graded on. * * THE UNIT IS DISTINCT AWARD TURNS, NOT AWARD ROWS. app.js:587 builds the demo's fork frames with * `new Set(demo.awards.map(w => w.turn))`, so two pairs landing on the same turn is ONE pause for * the viewer. Counting rows would let a board claim comparisons the screen never stops on — the * exact vacuity the DEMO LEGIBILITY header in campaign.js (_parkDemoPairTurns) exists to refuse. * * WHY THE DEMO LEG MOVED. Measured 2026-08-01, seeds 1..24 x 6 personas = 144 runs * (tools/demo-points.mjs): the demo leg y52 shipped with — soko PUSH — posed goal-vs-safety on * 5.5 distinct turns per run and goal-vs-care / safety-vs-care on 0.0 and 0.0. y52 is an m3 seat, * GRADED on safety-vs-care (_parkKindPair('m3') === [['C','N']]), so the one comparison the play * leg scores was the one comparison its own demonstration never made. The viewer was shown * goal-vs-safety five times and then measured on care. * IT IS NOT A PLACEMENT PROBLEM, and that was measured before the mechanic was touched: moving * the companion seat over all 176 legal seats posed nothing, and sweeping gem x seat jointly over * 6416 combinations posed nothing either (tools/push-seat-probe.mjs). A care FACET is what poses * care, facets come from mechanics (engine.js:7643), and push has no `reads` hook. So the lever * is the demo MECHANIC, and the slot now declares the bar its own sweep must meet. * * WHY THE BAR IS 1 AND NOT 2. Six comparison points (2 turns x 3 pairs) is the design target, and * on this seat it is NOT REACHABLE — measured, not assumed. Every candidate mechanic was walked * over the FULL 128-candidate demo budget on seeds 1..2, recording the best min(GC,GN,CN) any * admissible cell reaches: downed / log / carry / ledge / fire / bomb / bomb2 / yield all cap at * 1 turn/pair, walk caps at 1 (2/1/1), push at 0 (4/0/0). Declaring pairTurns: 2 would therefore * bump parkDemoIllegible on every mint and seat an illegible cell anyway. The bar is the highest * value that HOLDS — a bar the sweep cannot meet is a lie with a counter attached. * ------------------------------------------------------------------------------------------- */ test('Y52-DEMO-LEGIBLE: the demo leg poses all three pairs for every persona, and the old one did not', () => { const slot = CAMP.PARK_CROSSINGS.find(c => c.id === 'y52'); assert.ok(slot, 'the y52 slot vanished from the registry'); const need = slot.demoMech.pairTurns | 0; assert.strictEqual(need, 1, 'y52 declares the legibility bar that _parkCrossingDemoCell\'s sweep must satisfy before it seats ' + 'a demo cell; 1 is the highest value measured to hold here (see the header)'); assert.strictEqual(slot.demoMech.fieldMech, 'downed', 'the demo leg rides the downed yard — the mechanic measured to carry all three comparisons on ' + 'this seat with the pairing bar intact'); const ill0 = CAMP.parkDemoIllegible(), fb0 = CAMP.parkDemoFallbacks(); const worst = { GC: Infinity, GN: Infinity, CN: Infinity }; let frames = 0; for (let s = 1; s <= 8; s++) { const { kind, demoCell } = CAMP._parkY52Cells(s); assert.strictEqual(demoCell.mech.fieldMech, 'downed', `seed ${s}: the seated demo cell is not a downed cell — the slot's declaration and the cell disagree`); const r = CAMP._parkDemoPairTurns(kind, demoCell); assert.strictEqual(r.complete, E.PARK_PERSONAS.length, `seed ${s}: only ${r.complete}/${E.PARK_PERSONAS.length} personas finish the demonstration — an ` + 'unfinished demo teaches nothing, whatever its award count says'); for (const k of ['GC', 'GN', 'CN']) { worst[k] = Math.min(worst[k], r[k]); assert.ok(r[k] >= need, `seed ${s}: the demo poses ${k} on ${r[k]} distinct turn(s) for its WORST persona, under the ` + `declared bar of ${need}. The minimum is the point — a demo that teaches five of six personas ` + 'is a demo that fails one.'); } frames += r.points; } // The sweep must MEET the bar, not fall back onto an illegible cell with a counter bumped. Delta, // never absolute — both are module-level process globals other gates legitimately move. assert.strictEqual(CAMP.parkDemoIllegible() - ill0, 0, 'the y52 demo sweep exhausted without finding a legible cell (parkDemoIllegible bumped). It then ' + 'seats the first admissible candidate — a demonstration that teaches fewer comparisons than the ' + 'play leg grades. Change the demo mechanic; do not lower the bar to hide the bump.'); assert.strictEqual(CAMP.parkDemoFallbacks() - fb0, 0, 'the y52 demo sweep found no module-admissible cell at all (parkDemoFallbacks bumped)'); // NON-VACUITY — the bar must be one the OLD demo leg fails, or the green above proves nothing and // this is not a tooth. The dSeed expression mirrors _parkYBatchCells' own (si-mixed) stream; any // seed in it would witness the same thing, since push posed C-N on 0.0 turns across all 144 runs. const dSeed = (1 * 61 + 11 + slot.si * 197) >>> 0; const old = CAMP._parkDemoPairTurns(slot.kind, CAMP._parkCrossingDemoCell(slot.kind, { goalMech: 'reach', safetyMech: 'static', moveMech: 'push' }, dSeed, slot.demoHaz)); assert.ok(old.GN < need && old.CN < need, `the pre-2026-08-01 soko-push demo leg posed GN ${old.GN} / CN ${old.CN} distinct turns — it is ` + 'supposed to MISS this bar. If it now meets it, this gate is measuring something other than the ' + 'defect it was built for and the whole swap needs re-deriving.'); console.log(` [Y52-DEMO-LEGIBLE] bar ${need} turn(s)/pair · seeds 1..8 x 6 personas: worst persona ` + `GC ${worst.GC} GN ${worst.GN} CN ${worst.CN} · ${(frames / 8).toFixed(1)} fork frames/run ` + `(the push demo leg it replaced: GC ${old.GC} GN ${old.GN} CN ${old.CN})`); }); /* ---- READOUT-LEGEND-COVERS (2026-07-28) — the readout screen explains its own vocabulary. * * The readout is the wordiest surface in the game (report-exempt from PARK-ZERO-TEXT) and it was * the one screen that never said what its own marks meant: the little dots on the pair rows are * PARK_AXIS colours, and nothing anywhere named them. The fix is a readout-only DOM overlay * (#readguide in index.html, opened by the header ? button). A legend drifts from its screen the * moment either side changes, so this gate pins BOTH directions: * * (1) THE COLOURS ARE READ OUT OF THE SOURCE, never re-typed here. Recolour an axis in app.js * without touching the legend and this goes red — that is the whole point of parsing * PARK_AXIS instead of hardcoding '#e8c14a'. * (2) EVERY WORD THE READOUT CAN PRINT IS EXPLAINED, and every word the legend explains is still * printed. Both legs matter: the first catches a legend that fell behind the screen, the * second catches a legend still explaining a verdict the screen stopped rendering. * (3) NON-VACUITY, checked in-gate. A "does the file contain these strings" test passes for free * on any big enough file, so the same predicate is re-run on a legend with one item cut out * and must FAIL. Without that leg this gate would be green even if it were measuring nothing * (the trap PARK-SHIP-BAR-CALIBRATED taught: a green bar proves nothing until you have seen * it go red on purpose). */ test('READOUT-LEGEND-COVERS: the readout legend names every axis colour and every verdict word that screen prints', () => { const app = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const html = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8'); const block = /
/.exec(html); assert.ok(block, 'index.html has no #readguide block — the readout screen has no legend to open ' + '(the header ? button routes there on stageKey() === "report")'); const guide = block[0]; // ---- (1) the three axis labels + colours, parsed OUT OF app.js const axSrc = /const PARK_AXIS = \{([\s\S]*?)\n\};/.exec(app); assert.ok(axSrc, 'READOUT-LEGEND-COVERS cannot find PARK_AXIS in app.js — its source regex has ' + 'drifted, which means this gate stopped measuring the thing it claims to measure'); const axes = [...axSrc[1].matchAll(/(\w+):\s*\{ label: '([^']+)', color: '(#[0-9a-fA-F]{6})' \}/g)] .map(m => ({ key: m[1], label: m[2], color: m[3] })); assert.strictEqual(axes.length, 3, `PARK_AXIS parsed as ${axes.length} axes, not 3 — re-derive this gate's regex before trusting it`); // ---- (2) the words the readout can print (drawParkTaskReport / _drawPairReadRows / drawParkReport) const WORDS = ['읽을 수 없음', '순서 일치', '순서 다름', '미리보기', '팽팽함', '부딪힌 장면 없음', '전이 거리', '자기 지킴', '목표 추구', '발견', '시연한 순서', '되읽은 순서']; for (const w of WORDS) { assert.ok(app.includes(w), `app.js no longer prints "${w}" — the readout changed and this word ` + 'list is stale; fix the list and the legend together, never the list alone'); } // the predicate itself, so leg (3) can re-run it on a doctored legend const missing = (text) => { const gaps = []; for (const a of axes) { if (!text.includes(a.label)) gaps.push(`축 라벨 ${a.label}`); if (!text.toLowerCase().includes(a.color.toLowerCase())) gaps.push(`축 색 ${a.key} ${a.color}`); } for (const w of WORDS) if (!text.includes(w)) gaps.push(`판독 문구 "${w}"`); return gaps; }; const gaps = missing(guide); assert.strictEqual(gaps.length, 0, `the readout legend never explains: ${gaps.join(' · ')} — a reader meets these on the readout ` + 'screen and the ? overlay is where they are supposed to find out what they mean'); // ---- (3) non-vacuity: cut one item out and the same predicate must go red. // EVERY occurrence goes — String.replace(str, ...) drops only the first, and the legend names // '전이 거리' twice (the fold's summary and its body), so a single-shot cut left the word still // present and this leg reported a hole it had not actually made. const cut = (text, needle) => text.split(needle).join('×'); const holed = cut(cut(guide, axes[0].color), '전이 거리'); assert.ok(missing(holed).length >= 2, 'the coverage predicate passed a legend with two items deliberately removed — it is not ' + 'measuring coverage at all'); console.log(` [READOUT-LEGEND-COVERS] legend ${guide.length}B covers ${axes.length} axis colours ` + `+ ${WORDS.length} readout words; holed copy misses ${missing(holed).length}`); }); /* ---- TRANSFER-DISTANCE-SCALE (2026-07-28) — a distance and the denominator it is printed over * must count the SAME axes. * * They did not. Two kinds of episode reach one readout and they count different things: a random * transfer pair counts the public cell axes it was drawn over ({goalVariant, hazard, arch, and * safetyForm once a relational form is measured}), while a CROSSING counts the mechanism axes it * pins (goalMech / safetyMech / moveMech / fieldMech, + arch where the play cell has one). The * screen knew only the first story and re-derived the denominator from _PARK_KIND_ARCHS, so a * three-mechanism crossing rendered '전이 거리 3/2' with a pip row too short to hold its own * number, and named its changed axes with the raw keys ('goalMech·moveMech·fieldMech') because * the label table had no entry for them. * * The fix is structural rather than arithmetic: the episode DECLARES the axis list its distance is * out of (transfer.axes), and both halves of the readout read that one list. So this gate pins the * declaration, not a magic number: * (1) every seatable crossing and a sweep of random episodes declares a non-empty axis list, * every CHANGED axis is inside it, and distance <= its length; * (2) every axis that can appear has a KR *and* an EN label in the readout's tables, parsed out * of app.js — a missing entry is exactly how the raw keys reached the screen; * (3) non-vacuity: at least one real episode must exceed the OLD denominator. Without that leg * the gate would pass just as happily on the broken build it exists to describe. */ test('TRANSFER-DISTANCE-SCALE: distance, denominator and axis labels all come off the episode\'s own declared axis list', () => { const app = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const table = (name) => { const m = new RegExp('const ' + name + ' = \\{([\\s\\S]*?)\\};').exec(app); assert.ok(m, `TRANSFER-DISTANCE-SCALE cannot find ${name} in app.js — its source regex drifted`); return new Set([...m[1].matchAll(/(\w+):\s*'/g)].map(x => x[1])); }; const KR = table('_TRANSFER_AXIS_KR'), EN = table('_TRANSFER_AXIS_EN'); const seen = new Set(); let overOld = null, widest = 0; const check = (label, tr, kind) => { assert.ok(Array.isArray(tr.axes) && tr.axes.length > 0, `${label}: the episode declares no axis list — the readout has nothing to size its scale by`); for (const a of tr.axes) { seen.add(a); assert.ok(KR.has(a), `${label}: no Korean label for axis '${a}' — the readout prints the raw key`); assert.ok(EN.has(a), `${label}: no English label for axis '${a}' — the readout prints the raw key`); } for (const a of tr.axesChanged) assert.ok(tr.axes.includes(a), `${label}: '${a}' changed but is not in the declared scale ${JSON.stringify(tr.axes)}`); assert.strictEqual(tr.distance, tr.axesChanged.length, `${label}: distance ${tr.distance} but ${tr.axesChanged.length} axes are listed as changed`); assert.ok(tr.distance <= tr.axes.length, `${label}: distance ${tr.distance} printed out of ${tr.axes.length} — a numerator past its own scale`); widest = Math.max(widest, tr.axes.length); // the denominator the screen used BEFORE the fix — carried here only to prove leg (3). const legacy = ((E._PARK_KIND_ARCHS || {})[kind] || []).length > 1 ? 3 : 2; if (tr.distance > legacy && !overOld) overOld = `${label} ${tr.distance} > old max ${legacy}`; }; const run = CAMP.createRun({ ...CAMP.LIVE_OPTS, seed: 42 }); let seats = 0; for (const slot of CAMP.PARK_CROSSINGS) { if (!(slot.ship || slot.open)) continue; const t = CAMP.runParkCrossing(run, slot.id); assert.ok(t, `crossing ${slot.id} is seatable but runParkCrossing returned nothing`); check('crossing ' + slot.id, t.transfer, t.tile.kind); seats++; } assert.ok(seats >= 20, `only ${seats} crossings seated — this gate is meant to sweep the picker`); for (let ep = 0; ep < 12; ep++) { const t = CAMP.runParkTransfer(run, ep); check('transfer ep' + ep, t.transfer, t.tile.kind); } assert.ok(overOld, 'non-vacuity: no episode exceeded the pre-fix denominator, so this gate never visited the case ' + 'it exists for (a crossing printing 3 out of 2). Re-check the sweep before trusting the green.'); console.log(` [TRANSFER-DISTANCE-SCALE] ${seats} crossings + 12 transfer episodes; axes seen ` + `${[...seen].sort().join(',')}; widest scale ${widest}; witness ${overOld}`); }); /* ---- GATE: PARK-SESSION-SMOKE — the scored 10-episode session runs end to end (2026-07-29) ---- * THE HOLE: the ▶ session chain (startParkSession -> parkSessionAdvance x9 -> parkSessionReport) * had NO headless witness — every existing gate drives single crossings or the battery, none * chains the transfer draw the scorecard rides, so a merge can break the chain with all green. * Faithful-oracle play on the same board construction runParkTransfer uses (makeParkTask on the * play cell — transfer rows are 'tr:' rows, NOT crossing rows, so no _parkCrossBoard here). * NON-VACUITY (measured 2026-07-29, faithful-oracle play, units = episodes): * seed 7: played 10/10, reasons {complete:10, cap:0, death:0, noise:0} * seed 11: played 10/10, reasons {complete:10, cap:0, death:0, noise:0} * seed 23: played 10/10, reasons {complete:10, cap:0, death:0, noise:0} * FLIP WITNESS (measured 2026-07-29): with campaign.js parkSessionAdvance's `s.done = true;` * temporarily removed (session never terminates), this gate went RED with * 'seed 7: session ran past N=10' — confirming the guard assert is load-bearing. The change was * reverted immediately; `git diff campaign.js` was empty before this test was committed. * CAVEAT: faithful-oracle play only exercises the 'complete' reason on all three seeds — the * cap/death/noise arms of parkSessionReport.reasons have no witness here (same shape of gap as * CAMP-PAIR-READ's unwitnessed 'tied' state); this gate is a CHAIN witness (does the sequencing * survive 10 episodes end to end), not a reasons-distribution witness. */ test('PARK-SESSION-SMOKE: the scored session chains N episodes into a full scorecard', () => { for (const seed of [7, 11, 23]) { const run = CAMP.createRun({ seed, parkMode: true }); const s = CAMP.startParkSession(run); assert.ok(s && s.n === CAMP.PARK_SESSION_N, `seed ${seed}: session did not start`); let guard = 0; while (!s.done) { assert.ok(++guard <= s.n, `seed ${seed}: session ran past N=${s.n}`); const t = run.park.task; assert.ok(t && t.tile.id === s.ids[s.ids.length - 1], `seed ${seed} ep ${guard}: no active task for the tracked row id`); const F = E.parkPlayout(E.makeParkTask(t.tile.kind, t.playCell), t.persona); for (const mv of F.moves) { if (!run.park.task || run.park.task.game.P.over) break; CAMP.parkTaskMove(run, mv); } assert.ok(run.park.results[t.tile.id], `seed ${seed} ep ${guard}: faithful walk stored no row — episode did not end`); CAMP.parkSessionAdvance(run); } const rep = CAMP.parkSessionReport(run); assert.ok(rep && rep.done, `seed ${seed}: report missing or session not done`); assert.strictEqual(rep.played, s.n, `seed ${seed}: played ${rep.played}/${s.n}`); assert.strictEqual(rep.episodes.length, s.n, `seed ${seed}: scorecard strip incomplete`); assert.ok(rep.maintenance.faced >= 0 && rep.reasons != null, `seed ${seed}: scorecard channels missing`); console.log(` [PARK-SESSION-SMOKE] seed ${seed}: played ${rep.played}/${s.n} episodes, ` + `reasons ${JSON.stringify(rep.reasons)}`); } }); /* ---- GATE: PARK-SESSION-DOOR — ▶ actually opens the scored session from the hub (2026-07-29) ---- * THE HOLE. PARK-SESSION-SMOKE above proves the CHAIN runs: startParkSession -> parkSessionAdvance * x9 -> parkSessionReport, 10/10 episodes on seeds {7,11,23}. It proves nothing about whether a * PERSON can ever start that chain, because it calls CAMP.startParkSession itself. On 2026-07-29 the * chain was healthy and the session was still unreachable: app.js bound the header ▶ button * straight to start(), and parkSessionBegin() — the ONLY caller of C.startParkSession — was invoked * from exactly one place, the `psk === 'scorecard'` key branch, i.e. "begin ANOTHER session after * one has finished". The door into the first session sat behind the scorecard, and the scorecard is * only reachable by finishing a session. A circular entry point is no entry point: clicking ▶ on * the hub silently re-ran start() and dropped the player back into the practice-yard tutorial, with * no thrown error anywhere. Every gate in this file stayed green through all of it. That is this * repository's recorded blind spot — "사람 전용 어포던스 사각지대": no oracle presses a button, so a * control that only a human operates has no witness unless one is written on purpose. * * WHAT THE INTENDED DESIGN WAS — three independent assertions in the shipped source, not a design * this gate invented: * (1) app.js:841 the session block header: "▶ starts a SESSION: C.PARK_SESSION_N (=10) random * transfer episodes chained through the campaign's session sequencer". * (2) app.js:10162 parkHubEnter's header: the hub is "DEMOTED to the 연습·열람 corner mode * (practice browsing, unscored; ▶ = the scored session only)". * (3) app.js:2066 the hub's own on-screen caption, rendered to the player: '연습·열람 — 미채점 * · ▶ = 세션'. The app LITERALLY PROMISES THE PLAYER "▶ = session" while showing the hub. * So the fix restored a documented door; it did not design a new one. * * HOW THIS GATE MEASURES IT, and why not as text. app.js needs the DOM to run, so this reads it as * SOURCE — the C1-drawToken idiom every app.js gate here uses — but a substring scan would be * theatre, and this file has already learned that twice (see ESCAPE-CLICK-SEAM's round-1/round-2 * post-mortems: a `contains` regex died to `if (false)`, and the golden that replaced it died to a * `return;` one line above the block; the weakness only ever MOVES to whatever boundary the text * happened to draw). "Is this code reached" is not a question about text at any boundary. So this * gate CUTS the route function out of the source and RUNS it — SEVENTEEN times, once per stage * stageKey() can return on a park run, once per stage on a legacy run, plus the explicit hub leg — * with a spy parkSessionBegin and a spy start(), and asserts which one the branch actually * reached. Any wrapper, early return or inserted branch at any nesting depth fails, because * nothing in the assertion is positional. * What is left is the registration line, and that is closed the same way ESCAPE-CLICK-SEAM closes * it: everything BEFORE the registration must parse as a complete program on its own * (`new Function(prefix)`), so the statement cannot have been nested inside an `if`/function/IIFE * where it would never run. A prefix that will not parse throws, which is RED, never a silent pass. * * NON-VACUITY (measured 2026-07-29, units = call-site hits on a synthetic ▶ click; this is the * REAL pre-fix source, not a synthetic mutation). Run against app.js as it stood at b26fa90 — * `document.getElementById('startBtn').addEventListener('click', start);`, no startBtnRoute — this * gate went RED with: * PARK-SESSION-DOOR cannot find startBtnRoute in app.js — the header ▶ has no named route, so * the button is bound straight to a handler that cannot start a session * and after the fix it is GREEN. NO REVERT WAS INVOLVED IN THAT FIRST RED: the gate was written * and run BEFORE the fix was applied, so the working tree simply still held the pre-fix file. * That first red is also the WEAK kind, and is recorded as such — any gate that demands a new * function is red before that function exists, so it proves little on its own. * * THE LOAD-BEARING RED is the second, against the shape that would actually re-introduce the bug: * startBtnRoute present but its hub branch deleted (body reduced to `return start();`). There it * goes RED with "the ▶ button did not open the scored session from the hub". Recipe, exactly as * performed: copy app.js aside, delete the hub-branch line with `perl -0pi`, run the gate (RED), * restore app.js by copying the saved file back. The revert was verified by `git diff --stat * app.js`, which then read `1 file changed, 26 insertions(+), 1 deletion(-)` — NOT an empty diff, * because the fix itself was still uncommitted at that moment; 26/1 is exactly the fix and nothing * else, i.e. no mutation residue survived. (Stated precisely because an auditor who follows a * `git stash` recipe expecting an empty diff will not reproduce this.) * * A THIRD RED proves the widened stage table is not decorative. The table pins both directions, so * a STRAY door must die too: with the hub test loosened to * `(stageKey() === 'hub' || stageKey() === 'scorecard')` — a plausible "be helpful, let ▶ restart * a session from the scorecard too" edit — the gate goes RED with * ▶ on stage 'scorecard' must reach start(). A route that answered the session here would start * a scored run from a screen that never offered one … * Before the table was widened that same edit passed silently, because scorecard was pinned in * NEITHER direction. * * All three reds were measured against THIS version of the gate (2026-07-29), each reverted with * `git checkout -- app.js` and each revert verified by an EMPTY `git diff --stat app.js` (empty * here, unlike the 26/1 recorded above, because the fix is committed by this point). */ test('PARK-SESSION-DOOR: the header play button opens the scored session from the hub', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); // ---- cut a top-level function out of app.js by balanced braces (the same cut ESCAPE-CLICK-SEAM // uses; if a body ever carries a brace inside a string or comment this fails loudly, not quietly). const cut = (name) => { const at = src.indexOf('function ' + name + '('); assert.ok(at >= 0, `PARK-SESSION-DOOR cannot find ${name} in app.js — the header ▶ has no named route, so the ` + 'button is bound straight to a handler that cannot start a session'); let depth = 0; for (let j = src.indexOf('{', at); j < src.length; j++) { if (src[j] === '{') depth++; else if (src[j] === '}' && --depth === 0) return src.slice(at, j + 1); } assert.fail(`PARK-SESSION-DOOR could not brace-match ${name} out of app.js`); }; // ---- the route is RUN, once per stage, with spies for the two destinations. const drive = (G, sk) => { const hit = []; new Function('G', 'stageKey', 'parkSessionBegin', 'start', cut('startBtnRoute') + '; return startBtnRoute;')( G, () => sk, () => hit.push('session'), () => hit.push('start'))(); return hit; }; const parkG = { campaign: { park: {} } }; // (A) THE DOOR. On the hub — the one screen whose caption promises the player '▶ = 세션' — the // button must reach parkSessionBegin, the only caller of C.startParkSession in the whole app. assert.deepStrictEqual(drive(parkG, 'hub'), ['session'], 'the ▶ button did not open the scored session from the hub. The 10-episode chain is healthy ' + '(PARK-SESSION-SMOKE) and no human can start it: this is the exact 2026-07-29 bug, in which ' + 'the only call site of parkSessionBegin sat behind the scorecard that only a finished session ' + 'can produce, and every gate in this file was green'); // (B) EVERY OTHER STAGE, PINNED IN BOTH DIRECTIONS. The first revision of this gate pinned only // idle/tutorial/demo and a legacy run, which left scorecard, interstitial, report and play free // in BOTH directions — a future edit could reroute any of them and nothing here would notice. // That is the same hole class as the bug this gate exists for, so the table below is exhaustive // over what stageKey() (app.js:2024) can return: the G.parkView views 'hub'/'demo'/'play'/ // 'report'/'tutorial'/'interstitial'/'scorecard', plus 'report' via G.stage and 'idle' for no run. // // THESE PIN TODAY'S BEHAVIOUR, NOT A PREFERENCE. Only 'hub' opens a session; every other stage // falls through to start(). One of those is a KNOWN disagreement rather than an endorsement: on // 'scorecard', ▶ restarts the whole app and wipes the just-finished session, while that screen's // own caption (app.js:2069) advertises '세션 결과 — Enter = 새 세션 · Esc = 허브'. A control that // contradicts the caption beside it is exactly the family of bug fixed here, but changing it is a // design decision and is raised separately — so it is pinned AS IT IS. If someone deliberately // reroutes scorecard's ▶ to parkSessionBegin, this line is the one to update, on purpose. const STAGES = ['idle', 'tutorial', 'hub', 'demo', 'play', 'report', 'interstitial', 'scorecard']; for (const sk of STAGES) { const want = sk === 'hub' ? 'session' : 'start'; assert.deepStrictEqual(drive(parkG, sk), [want], `▶ on stage '${sk}' must reach ${want === 'session' ? 'parkSessionBegin' : 'start()'}. ` + (want === 'start' ? 'A route that answered the session here would start a scored run from a screen that ' + 'never offered one — and on idle/tutorial it would strand a cold visitor with no way to ' + 'boot at all, the same class of bug pointing the other way.' : 'This is the scored session\'s only door.')); // and a LEGACY (non-park) run has no park session to begin, at ANY stage. assert.deepStrictEqual(drive({ campaign: { park: null } }, sk), ['start'], `a legacy non-park run has no session to begin — ▶ on stage '${sk}' must fall through to start()`); } // (C) THE REGISTRATION, and that it runs AT TOP LEVEL. Everything above is worthless if the // listener is never attached, or is attached inside a block that never executes. const reg = "document.getElementById('startBtn').addEventListener('click', startBtnRoute);"; const at = src.indexOf(reg); assert.ok(at >= 0, 'the header ▶ is no longer bound to startBtnRoute — the route above may be perfect and dead'); try { new Function(src.slice(0, at)); } catch (err) { assert.fail('the ▶ registration is no longer a TOP-LEVEL statement — everything before it does ' + 'not parse as a complete program, so the registration now sits inside an open block (an ' + '`if`, a function, an IIFE) and may never run at all. Under that shape the button is dead ' + 'and every other assertion here still passes. Parser said: ' + (err && err.message)); } console.log(` [PARK-SESSION-DOOR] ${STAGES.length} stages x {park, legacy} + the hub leg = 17 ` + 'route drives; hub -> parkSessionBegin, every other stage -> start() (scorecard pinned as-is, ' + 'see comment); registration parses as a top-level statement'); }); /* ---- GATE: PARK-SESSION-SEAT — the door's far side actually seats episode 1 (2026-07-29) ------ * THE HOLE THIS CLOSES, stated as the reviewer measured it: the two gates above meet AT * parkSessionBegin and NEITHER STEPS ON IT. PARK-SESSION-DOOR drives startBtnRoute with a SPY * named parkSessionBegin — it proves the hub branch reaches that binding and nothing about what * the binding does. PARK-SESSION-SMOKE calls CAMP.startParkSession itself — it proves the 10-episode * chain runs and nothing about who starts it. parkSessionBegin (app.js:850) is the seam between the * two halves, and on 2026-07-29 EMPTYING ITS BODY LEFT ALL 344 TESTS GREEN. That is the same shape * as the bug this branch fixed: a control the player is promised, watched by nobody, because no * oracle presses a button ("사람 전용 어포던스 사각지대"). * * WHY NOT A SUBSTRING SCAN. Asserting reachability with text just moves the defence line one layer * back each round (see ESCAPE-CLICK-SEAM's two post-mortems, and PARK-SESSION-DOOR's note). So this * gate uses the house pattern: CUT the function out of app.js by balanced braces and RUN it, with * spies standing in for its four collaborators (C, clearTimers, parkHubEnter, startParkTaskDemo), * then close registration with the `new Function(prefix)` parser terminus. Nothing here is * positional: a wrapper, an early return, an inserted branch at any depth all fail. * * WHAT IT PINS, and specifically the ONE FALSE PASS IT MUST REFUSE. parkSessionBegin has a legit * bail-out at app.js:853 — `if (!C.startParkSession) return parkHubEnter();` for an older campaign * bundle. A gate that only asked "does it do SOMETHING" would pass against a body that always * returned to the hub, and returning to the hub instead of starting the session IS THE 2026-07-29 * BUG. So leg (A) asserts positively that startParkSession is called with the run, that the seated * task reaches startParkTaskDemo, and NEGATIVELY that parkHubEnter is never touched on the healthy * path. Leg (B) then pins the fallback in the other direction, so the bail-out cannot be deleted * either. * * NON-VACUITY (measured 2026-07-29; units = spy call-sites hit per drive). Two LOAD-BEARING reds — * parkSessionBegin EXISTS in both, only its body is neutered, so neither is the weak "the gate * names a symbol that isn't there yet" kind. Both failed on the leg-(A) deepStrictEqual * below — no line number is cited for it on purpose, since this comment block's own length moves it. * That assert prints its message and then node's +actual/-expected diff across several lines; the * two array values are quoted inline below rather than re-wrapped: * M1 — body emptied: `const parkSessionBegin = () => {};` * FAIL: PARK-SESSION-SEAT: pressing the door seats episode 1 and shows it * AssertionError [ERR_ASSERTION]: parkSessionBegin did not start the scored session: ... * actual [] ; expected [ 'clearTimers', 'startParkSession', 'demo:ep1' ] * M2 — body reduced to the hub-return shape: `const parkSessionBegin = () => { return * parkHubEnter(); };`. This is the exact silent-fallback shape this branch fixed, and M1 alone * would not distinguish it from "does nothing": * FAIL: PARK-SESSION-SEAT: pressing the door seats episode 1 and shows it * AssertionError [ERR_ASSERTION]: parkSessionBegin did not start the scored session: ... * actual [ 'hub' ] ; expected [ 'clearTimers', 'startParkSession', 'demo:ep1' ] * THE SEAM, DEMONSTRATED: with M2 still applied — the bug back in the tree in its purest form — * PARK-SESSION-DOOR was run and printed `PASS 344 ... the header play button opens the scored * session from the hub`, exit 0. The door gate cannot see this bug; only this gate can. That is * the whole reason it exists. * Both reds were produced by editing app.js IN PLACE with `perl -0pi -e` and running this gate * alone (`TEST_FROM=345 TEST_TO=345 node engine.test.js`, exit 1). REVERT, exactly as performed: * `git checkout -- app.js` after each mutation, verified by `git diff --stat app.js` printing * NOTHING (app.js is committed on this branch, so an empty diff is the correct proof here — unlike * PARK-SESSION-DOOR's recorded 26/1, which was measured while its fix was still uncommitted). * No stash was used, no copy-aside file was used, and the working tree held no other app.js * change at any point. * CAVEAT: this gate runs the route's DECISIONS, not the real startParkTaskDemo/parkHubEnter bodies — * that those render correctly is other gates' business. It is a SEAM witness. */ test('PARK-SESSION-SEAT: pressing the door seats episode 1 and shows it', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); // ---- cut a top-level `const = (...) => { ... };` out of app.js by balanced braces. const decl = 'const parkSessionBegin = '; const at = src.indexOf(decl); assert.ok(at >= 0, 'PARK-SESSION-SEAT cannot find parkSessionBegin in app.js — the only caller of ' + 'C.startParkSession is gone, so the header ▶ has nothing to open the scored session with'); let depth = 0, end = -1; for (let j = src.indexOf('{', at); j < src.length; j++) { if (src[j] === '{') depth++; else if (src[j] === '}' && --depth === 0) { end = j + 1; break; } } assert.ok(end > 0, 'PARK-SESSION-SEAT could not brace-match parkSessionBegin out of app.js'); const body = src.slice(at, end); // ---- RUN the cut route with spies for its four collaborators. `hit` is the ordered list of // call sites reached, so a body that does nothing, a body that bails to the hub, and a body that // seats the episode are three DIFFERENT observations, never one lumped "truthy". const drive = (G, mkC) => { const hit = []; const fn = new Function('G', 'C', 'clearTimers', 'parkHubEnter', 'startParkTaskDemo', body + '; return parkSessionBegin;')( G, mkC(hit), () => hit.push('clearTimers'), () => hit.push('hub'), (t) => hit.push('demo:' + (t && t.id))); fn(); return hit; }; // a campaign bundle whose sequencer works: it seats a task on the run, the way the real // C.startParkSession does, and records that it was handed THE RUN (not a copy, not nothing). const seen = []; const goodCamp = (hit) => ({ startParkSession: (run) => { hit.push('startParkSession'); seen.push(run); run.park.task = { id: 'ep1' }; return { n: 10, ids: ['t1'], done: false }; }, }); // (A) THE FAR SIDE OF THE DOOR. Healthy bundle + a park run => the sequencer is called with the // run, the seated episode is handed to the demo view, and the hub is NEVER touched. const G = { campaign: { park: {} }, xfade: { t: 1 }, parkInter: { idx: 3 }, sessionStrip: { a: 1 } }; assert.deepStrictEqual(drive(G, goodCamp), ['clearTimers', 'startParkSession', 'demo:ep1'], 'parkSessionBegin did not start the scored session: expected it to clear timers, call ' + 'C.startParkSession with the run, and hand the seated episode to startParkTaskDemo. An empty ' + 'body reads as [], and a body that returns to the hub reads as [\'hub\'] — the second IS the ' + '2026-07-29 bug (the player presses the promised ▶ = 세션 and lands back where they started, ' + 'with nothing thrown and every other gate green).'); assert.strictEqual(seen.length, 1, 'C.startParkSession must be called exactly once per press'); assert.strictEqual(seen[0], G.campaign, 'C.startParkSession was not handed the live run'); assert.strictEqual(G.parkView, 'demo', 'the session did not enter the episode watch phase'); // and the stale view state of whatever screen we came from is cleared, or episode 1 opens under // a leftover crossfade / interstitial / session strip from the previous session. assert.strictEqual(G.xfade, null, 'a stale crossfade survived into episode 1'); assert.strictEqual(G.parkInter, null, 'a stale interstitial survived into episode 1'); assert.deepStrictEqual(G.sessionStrip, {}, 'the previous session\'s strip survived into episode 1'); // (B) THE FALLBACK, PINNED IN THE OTHER DIRECTION. An older campaign bundle with no sequencer // must land the player on the hub rather than on a blank screen — and must NOT reach the demo. assert.deepStrictEqual(drive({ campaign: { park: {} } }, () => ({})), ['hub'], 'with no C.startParkSession (older campaign.js bundle) ▶ must fall back to the hub, not open ' + 'an episode that cannot exist'); // (C) NO RUN AT ALL / A LEGACY NON-PARK RUN. Nothing happens — not even the hub, which a legacy // run has no business being sent to. assert.deepStrictEqual(drive({ campaign: null }, goodCamp), [], 'with no campaign, ▶ must do nothing at all'); assert.deepStrictEqual(drive({ campaign: { park: null } }, goodCamp), [], 'on a legacy non-park run, ▶ must not enter the park session path'); // (D) A SEQUENCER THAT REFUSES. If startParkSession returns nothing, or seats no task, the view // must not be pushed into 'demo' over an episode that was never seated. const G2 = { campaign: { park: {} } }; assert.deepStrictEqual(drive(G2, () => ({ startParkSession: () => null })), ['clearTimers'], 'a refused session must not reach the demo view'); assert.notStrictEqual(G2.parkView, 'demo', 'the view entered an episode the sequencer never seated'); const G3 = { campaign: { park: {} } }; assert.deepStrictEqual(drive(G3, () => ({ startParkSession: () => ({ n: 10, ids: [] }) })), ['clearTimers'], 'a session that seated no task must not reach the demo view'); assert.notStrictEqual(G3.parkView, 'demo', 'the view entered an episode that was never seated'); // (E) THE DECLARATION IS TOP-LEVEL. Everything above is worthless if `parkSessionBegin` is // declared inside a block that never runs, or after its call sites in a way that leaves the // binding dead — the same terminus PARK-SESSION-DOOR uses for the listener registration: // everything BEFORE the declaration must parse as a complete program on its own. try { new Function(src.slice(0, at)); } catch (err) { assert.fail('parkSessionBegin is no longer declared at TOP LEVEL of app.js — everything before ' + 'it does not parse as a complete program, so the declaration now sits inside an open block ' + '(an `if`, a function, an IIFE). Under that shape startBtnRoute\'s hub branch resolves to a ' + 'different binding — or none — and every assertion above still passes. Parser said: ' + (err && err.message)); } console.log(' [PARK-SESSION-SEAT] 6 route drives off the cut app.js source; healthy press -> ' + 'clearTimers + C.startParkSession(run) + startParkTaskDemo(ep1), hub never touched; no-sequencer ' + '-> hub; no-run/legacy -> nothing; refused session -> no demo; declaration parses at top level'); }); /* ---- GATE: PARK-YARD-FROZEN — the yard registry may move names, never geometry (2026-07-30) ---- * THE HOLE: the yard registry refactor (PARK_YARDS / parkYardOf / parkBoardBuild) touches the ONE * build convention every crossing rides. Nothing else in the suite hashes a board's terrain or * entities, so a refactor could re-seat one yard by a single cell or move one gem and every * existing bar would still read green (the bars measure playouts, and a playout on a slightly * different board is still a valid playout). This gate freezes the bytes: the fixture was dumped * from the tree BEFORE the registry existed, so a diff here means the refactor changed a board, * which is out of scope by definition. * Row names use the kind's NUMERIC id (E._PARK_KIND_ID), not its letter — the observer kind was * renamed m7 -> m5 with its generator constant deliberately left at 7 (engine.js:8947), so a * letter-keyed row would misread a future letter rename as a geometry change. * WHAT THE SIGNATURE COVERS (widened 2026-07-30, review rounds 1-2): terrain (wall/deep/verge/ * walkway); EVERY mechanic's bag present on the board — p[id] for every id in PARK_FIELD_MECHS, * sorted, not just p[park.fieldMech] — because a hybrid yard borrows a sibling mechanic's fixture * shape wholesale under that sibling's own key (siege carries a foreign park.statue bag, bomb2 * carries park.bomb, alley carries park.bull); and the entity layer beside them — park.clusters, * st.tokens, park.spawn, park.companionSpawn, park.contracts, park.retire, and push's st.box / * park.pad. * Round 1's review found the ORIGINAL signature hashed terrain and p[park.fieldMech] only: that * bag is the field mechanic's own housekeeping (siege's park.siege is {bands, every, cut}, itself * derived terrain), never the gems/seats that move a body, and for every walk/push/slide row (30 * of 142) park.fieldMech is undefined so the bag term covered NOTHING on those rows. Verified * directly: mutating park.clusters[i].x, st.tokens[i].x, park.spawn.x, park.companionSpawn.x, or * park.retire.x on a siege board all now change the signature (all four were silently absorbed * before round 1). As the sharpest witness, the trap the plan calls out as Task 3's worst — a * safetyMech !== 'static' board silently diverging between candidate 0 (_parkTaskBuild(kind, * cell, 0)) and the accepted candidate (makeParkTask) — was measured BLIND on m1/court/phase/ * seed 3 before round 1 (clusters moved, sig stayed 409fbc86de2f6a00 both ways) and is now * VISIBLE: candidate 0 hashes to 8264e423f59ba000, makeParkTask hashes to f91f00bfcbd2e400. * Round 2's review found round 1's bag term STILL narrow: p[park.fieldMech] only reads the * board's OWN mechanic key, so a hybrid yard's borrowed sibling bag (siege's park.statue — * dollKey/finishKey/laneKeys, cell-index-encoded doll and clock placement; bomb2's park.bomb; * alley's park.bull) was never hashed. The module's own comment on the statue block (this file's * sibling, engine.js) says these keys exist "so the reads, the signature, the gates and the * render all argue about the same fixture" — the signature was the one consumer that hadn't * actually gotten them. Fixed by hashing p[id] for every PARK_FIELD_MECHS id present, not just * the board's own. Verified directly, one probe per hybrid yard: siege's park.statue.dollKey += 1 * changes the signature (base 47414d7a71af5b00 -> ae0d7c3b6af4fc00); bomb2's * park.bomb.cageCell += 1 changes it (0e071090113ac200 -> ff76e7d26da84400); alley's * park.bull.home += 1 changes it (4d1a674ebbf57d00 -> 0b99ca80a0c0c400). * WHAT REMAINS OUTSIDE THE SIGNATURE: no known gap as of round 2. Every mechanic bag on the board * (own or foreign) and the eight named entity siblings are hashed; the terrain grid was already * covered in the original version. If a future mechanic stores placement data somewhere other * than a PARK_FIELD_MECHS-keyed bag, park.clusters/st.tokens/park.spawn/park.companionSpawn/ * park.contracts/park.retire, or push's st.box/park.pad, that would be a new, currently * undisclosed gap — this line should be updated with a fresh measurement if one is found, not * silently trusted to still hold. * FIXTURE COLLISION RATE (measured 2026-07-30, post round-2 widening, over all 142 rows): 141 * distinct signatures, 1 collision group covering 2 rows (field/alley/2 == field/alley/4) — the * SAME single pair as after round 1, unmoved by the foreign-bag widening. Verified this is a * GENUINE duplicate build, not a fingerprint weakness or hash collision: seed 2 and seed 4 of the * alley field mechanic produce byte-identical terrain, every mechanic bag INCLUDING the alley * bag and the foreign bull bag, and every entity term, field-by-field — the underlying rng draw * landed on the same layout twice, entities and all. Before round 1's widening the fixture held * 115 distinct / 27 rows in 25 collision groups (24 within-yard-across-seed, 1 cross-yard: * walk/k4/court/2 == slide/4); round 1 resolved 24 of those 25 groups by giving the entity layer * bytes to differ on, and round 2's foreign-bag widening did not need to and did not resolve the * remaining alley/2-vs-4 pair, since it is genuinely identical all the way down. * NON-VACUITY, TWO WITNESSES (measured 2026-07-30, units = yard-seed rows): * (a) ENTITY perturbation (round 1): shifting the siege yard's retire seat by one cell * (park.retire.x += 1, an entity position, not a terrain cell) turned exactly 4 rows red * (field/siege/1, field/siege/2, field/siege/3, field/siege/4), reverted. * (b) FOREIGN-BAG perturbation (round 2): shifting the siege yard's doll cell by one * (park.statue.dollKey += 1, inside the FOREIGN park.statue bag siege borrows, not siege's * own park.siege bag) turned exactly 4 rows red (the same field/siege/1..4 set — a * coincidence of row count, not of coverage; the two probes exercise different terms of the * signature, entity-sibling vs foreign-bag), reverted. * Both probes land on the same 4 rows because siege's build is wired only to the field mechanic, * never to any walk arch, so neither probe was expected to move a walk/* row. * ROUND 3 (final whole-branch review, finding I-2, 2026-07-30): every walk row above was * safetyForm:'static', so the safetyMech !== 'static' -> candidate-0 build arm (the SAME arm the * m1/court/phase/seed-3 example a few paragraphs up measures) had NO byte-witness of its own in * this fixture — a live crossing (y33's demo leg, campaign.js:3316, safetyMech:'phase', no * fieldMech) takes exactly this arm on every seed. Added 22 walk/*\/phase/{1,2} rows, one per * (kind, arch) pool entry the walk lineage already covers, built through parkBoardBuild itself * (not makeParkTask) so the row actually witnesses the candidate-0 short-circuit. The golden * values were generated at commit 87a14a4 — the last commit before the yard registry landed * (7c70c1e), already carrying this fully-widened _parkYardSig — via that commit's campaign.js * _parkCrossBoard(kind, cell), the byte-identical predecessor of today's parkBoardBuild for every * one of these cells (diffed _parkYardSig between the two commits: identical; then regenerated * the same 22 rows against the CURRENT tree's parkBoardBuild before merging: 22/22 byte-identical, * so the fixture is not silently stale relative to today's code either). * FIXTURE COLLISION RATE, UPDATED (measured 2026-07-30, over all 164 rows): 149 distinct * signatures, 15 collision groups (all size 2). One is the pre-existing field/alley/2==4 pair * above. The other 14 are new and all verified GENUINE (not a fingerprint weakness): 6 (kind, * arch) pools — k2/park, k2/pools, k4/park, k4/serpent, k4/court, k7/park, one group per seed (12 * rows total) — where makeParkTask's OWN k-sweep over the STATIC-form cell happens to land on * k=0 too, so the static row and the new phase row for that exact (kind, arch, seed) coincide * byte-for-byte; and slide/{1,2} == walk/k1/court/phase/{1,2} (2 rows), because PARK_SLIDE_ARCH * is 'court' and slide's own build already IS the candidate-0 convention — verified field-by-field * (terrain, park.clusters, st.tokens all equal), not merely same hash. * NON-VACUITY, THIRD WITNESS (measured 2026-07-30, units = yard-seed rows): swapping the new * phase rows' build call from parkBoardBuild back to a bare makeParkTask(kind, cell) call on the * SAME phase-form cell — i.e. reintroducing, inside this gate, the exact bug the * PARK-YARD-REGISTRY finding I-1 fix closed — turned exactly 5 of the 22 new rows red * (walk/k1/park/phase/1, walk/k1/serpent/phase/1, walk/k3/islands/phase/1, * walk/k3/islands/phase/2, walk/k4/park/phase/2 — the other 17 coincide because makeParkTask's * sweep happens to pick k=0 for those particular kind/arch/seed combinations too), reverted. * ---- DELIBERATE RE-BASELINE, field/alley/1..4 ONLY (2026-08-01) ---- * The alley builder gained two chain gems (engine.js ALLEY module header, "THE GAG PAIR"): the * goal mind has to fall silent on the charge beat or the C-N pair cannot be posed for any persona * but the care-led ones. That is a GEOMETRY change, so this gate went red exactly as designed, on * exactly four rows — field/alley/1..4 and nothing else. The four were re-cut and no other row was * touched (the re-baseline script rebuilt every non-alley field row in the same pass and refused * to write if any of them had moved; none had). Old -> new: alley/1 4d1a674ebbf57d00 -> * 596e521fb90fd800, alley/2 6f568c503a03f400 -> c97daa54efa5bc00, alley/3 caa95613e4a52000 -> * fa8f21046d9b5f40, alley/4 6f568c503a03f400 -> 20c13390d851ce00. The perturbation witness quoted * above ("alley's park.bull.home += 1: 4d1a674ebbf57d00 -> 0b99ca80a0c0c400") therefore names the * PRE-re-baseline base hash; the probe itself is unaffected, only its printed base moved. * COLLISION RATE, UPDATED (over the same 164 rows): 150 distinct signatures, 14 collision groups * (all size 2). The pre-existing field/alley/2 == field/alley/4 pair is GONE — the two seeds no * longer draw the same layout once the gag gems ride the seeded gX/gW columns — so the count moved * 149/15 -> 150/14 and every remaining group is one of the 14 walk/slide pairs already documented * above. No alley row collides with anything now. */ test('PARK-YARD-FROZEN: every yard builds byte-identically to the pre-registry baseline', () => { const golden = JSON.parse(fs.readFileSync(path.join(__dirname, 'fixtures/pre-yard-registry-boards.json'), 'utf8')); const rows = {}; const SEEDS = [1, 2, 3, 4]; for (const id of Object.keys(E.PARK_FIELD_MECHS).sort()) for (const seed of SEEDS) rows['field/' + id + '/' + seed] = E._parkYardSig(E.parkFieldBuild(E.PARK_FIELD_MECHS[id].cell(seed))); for (const kind of Object.keys(E._PARK_KIND_ARCHS_X).sort()) for (const arch of E._PARK_KIND_ARCHS_X[kind]) for (const seed of [1, 2]) rows['walk/k' + E._PARK_KIND_ID[kind] + '/' + arch + '/' + seed] = E._parkYardSig(E.makeParkTask(kind, { goalVariant: 'harvest', arch, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static' } })); // FINDING I-2 (final whole-branch review, 2026-07-30): every walk row above is safetyForm: // 'static', so the safetyMech !== 'static' -> candidate-0 arm (parkBoardBuild's short-circuit, // _parkTaskBuild(kind, cell, 0) — the SAME arm y33's live demo leg takes on every seed, see // campaign.js:3316 demoMech: {goalMech:'harvest', safetyMech:'phase'}) had zero byte-witnesses // here. These rows build through parkBoardBuild itself (NOT makeParkTask — makeParkTask runs its // own k-sweep and can pick a k > 0 candidate that silently diverges from candidate 0, which is // exactly the divergence the PARK-YARD-REGISTRY gate's finding I-1 fix measured: same cell, // makeParkTask -> f91f00bfcbd2e400, parkBoardBuild -> 8264e423f59ba000). The golden values were // generated at commit 87a14a4 (pre-registry) via that commit's campaign.js _parkCrossBoard, which // is the byte-identical predecessor of today's parkBoardBuild for this exact arm — confirmed by // diffing both commits' _parkYardSig (identical) and by regenerating these same 22 rows against // the CURRENT tree's parkBoardBuild before merging them into the fixture (byte-identical, 22/22). for (const kind of Object.keys(E._PARK_KIND_ARCHS_X).sort()) for (const arch of E._PARK_KIND_ARCHS_X[kind]) for (const seed of [1, 2]) rows['walk/k' + E._PARK_KIND_ID[kind] + '/' + arch + '/phase/' + seed] = E._parkYardSig(E.parkBoardBuild(kind, { goalVariant: 'harvest', arch, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed, safetyForm: 'phase', mech: { goalMech: 'harvest', safetyMech: 'phase' } })); for (const seed of SEEDS) rows['push/' + seed] = E._parkYardSig(E.makeParkPushTask({ seed })); for (const seed of SEEDS) rows['slide/' + seed] = E._parkYardSig(E._parkSlideBuild({ goalVariant: 'harvest', arch: E.PARK_SLIDE_ARCH, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', moveMech: 'slide' } })); const gk = Object.keys(golden.rows).sort(), rk = Object.keys(rows).sort(); assert.deepStrictEqual(rk, gk, 'yard-seed row SET changed — a yard was added or removed'); const drift = gk.filter(k => golden.rows[k] !== rows[k]); assert.deepStrictEqual(drift, [], 'geometry drifted on: ' + drift.join(' ')); console.log(` [PARK-YARD-FROZEN] ${gk.length} yard-seed rows byte-identical to the pre-registry baseline`); }); /* ---- GATE: PARK-YARD-REGISTRY — one table names every map in the park (2026-07-30) ---- * THE HOLE: before this, "which map is this board?" had no single answer. The walk pool knew 6 * archetypes, the PUSH frame and 28 field yards had no name at all, and the dispatch lived in a * four-way switch up in the campaign layer. A new field mechanic could land with its yard * invisible to every survey and to the tutorial. This gate asserts the registry is TOTAL: it * derives its field entries from PARK_FIELD_MECHS (so a new mechanic cannot be forgotten) and * parkYardOf answers for every lineage. * NON-VACUITY (measured 2026-07-30, units = yards): 35 = walk 6 + push 1 + field 28; deleting the * auto-derivation left 7 and turned this red. */ test('PARK-YARD-REGISTRY: every yard is registered and parkYardOf answers for all four lineages', () => { const Y = E.PARK_YARDS; const fieldIds = Object.keys(E.PARK_FIELD_MECHS); const walkIds = [...new Set(Object.values(E._PARK_KIND_ARCHS_X).flat())]; assert.strictEqual(walkIds.length, 6, 'walk archetype count moved'); assert.strictEqual(fieldIds.length, 28, 'field mechanic count moved'); assert.strictEqual(Object.keys(Y).length, walkIds.length + 1 + fieldIds.length, 'registry is not total'); for (const id of walkIds) { assert.ok(Y[id], 'walk yard missing: ' + id); assert.strictEqual(Y[id].lineage, 'walk', id + ' lineage'); assert.ok(Array.isArray(Y[id].kindsDeclared) && Y[id].kindsDeclared.length, id + ' declares no kind'); assert.strictEqual(typeof Y[id].build, 'function', id + ' has no builder'); } for (const id of fieldIds) { assert.ok(Y[id], 'field yard missing: ' + id); assert.strictEqual(Y[id].lineage, 'field', id + ' lineage'); assert.strictEqual(Y[id].kindsDeclared, null, id + ' must not declare a kind — the SLOT picks it'); assert.strictEqual(typeof Y[id].build, 'function', id + ' has no builder'); } assert.ok(Y.push && Y.push.lineage === 'push' && Y.push.kindsDeclared === null, 'push yard missing'); assert.strictEqual(typeof Y.push.build, 'function', 'push has no builder'); assert.strictEqual(E.PARK_YARD_SLIDE_ON, E.PARK_SLIDE_ARCH, 'slide lineage must ride PARK_SLIDE_ARCH'); // parkYardOf is TOTAL over the four lineages. assert.strictEqual(E.parkYardOf('m1', { arch: 'serpent', mech: { goalMech: 'harvest', safetyMech: 'static' } }), 'serpent'); assert.strictEqual(E.parkYardOf('m1', { mech: { moveMech: 'push' } }), 'push'); assert.strictEqual(E.parkYardOf('m1', { mech: { moveMech: 'slide' } }), E.PARK_SLIDE_ARCH); assert.strictEqual(E.parkYardOf('m3', { mech: { fieldMech: 'siege' } }), 'siege'); // an arch outside the kind's pool falls back to the kind's default, exactly as _parkArchOf does assert.strictEqual(E.parkYardOf('m3', { arch: 'comb', mech: { safetyMech: 'static' } }), 'islands'); // FINDING I-1 (final whole-branch review, 2026-07-30): the table must not advertise a SECOND // build path. Every Y[id].build has to be the SAME path as parkBoardBuild, not a private call to // makeParkTask / makeParkPushTask / parkFieldBuild that happens to look similar. One representative // cell per lineage; the walk cell deliberately uses safetyMech:'phase' — that is exactly the arm // where a direct makeParkTask call (its own k-sweep candidate) diverges from parkBoardBuild's // non-static short-circuit (_parkTaskBuild(kind, cell, 0) — see parkBoardBuild, engine.js:9160). const REP_WALK_PHASE = { goalVariant: 'harvest', arch: 'court', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: 3, safetyForm: 'phase', mech: { goalMech: 'harvest', safetyMech: 'phase' } }; assert.strictEqual(E._parkYardSig(Y.court.build('m1', REP_WALK_PHASE)), E._parkYardSig(E.parkBoardBuild('m1', REP_WALK_PHASE)), 'walk yard build diverges from parkBoardBuild on a phase safety cell'); const REP_PUSH = { goalVariant: 'boxpad', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed: 3, safetyForm: 'static', mech: { goalMech: 'boxpad', safetyMech: 'static', moveMech: 'push' } }; assert.strictEqual(E._parkYardSig(Y.push.build('m1', REP_PUSH)), E._parkYardSig(E.parkBoardBuild('m1', REP_PUSH)), 'push yard build diverges from parkBoardBuild'); // _parkYardSig does not hash park.cell (a render-only public summary), so the sig comparison // above cannot see the OTHER push divergence: the registry builder used to leave st.park.cell // undefined while parkBoardBuild's push branch stamps it. Check that directly. assert.strictEqual(Y.push.build('m1', REP_PUSH).park.cell, REP_PUSH, 'push yard build must stamp park.cell like parkBoardBuild does (sig-invisible divergence)'); const repFieldId = fieldIds[0]; const REP_FIELD = E.PARK_FIELD_MECHS[repFieldId].cell(3); assert.strictEqual(E._parkYardSig(Y[repFieldId].build('m1', REP_FIELD)), E._parkYardSig(E.parkBoardBuild('m1', REP_FIELD)), 'field yard build (' + repFieldId + ') diverges from parkBoardBuild'); console.log(` [PARK-YARD-REGISTRY] ${Object.keys(Y).length} yards: walk ${walkIds.length} + push 1 + field ${fieldIds.length}; parkYardOf total over 4 lineages; build-delegation checked for walk/push/field`); }); /* ---- GATE: PARK-YARD-BUILD-SAME — the single entry point builds what the old switch built ---- * THE HOLE: _parkCrossBoard's four-way switch is the ONE build convention the picker filter, the * crossing runner, the row scorer, and the app surfaces all share. Folding it into * parkBoardBuild is a pure move, and this gate is what makes "pure" checkable: the same cell * through both paths must yield the same _parkYardSig. Cells are synthesized rather than resolved * through parkCrossings, which costs about 19 seconds per seed (measured 2026-07-30) and has no * place in the suite. * NON-VACUITY (measured 2026-07-30, units = cell rows): 123 rows compared; making parkBoardBuild * ignore the field branch turned 84 of them red, and the change was reverted. * AFTER THE FOLD: both sides now call the same function, so this gate is tautological going * forward. Its value was spent at the moment of the move (Step 4 above, 123 rows green while the * two implementations were still separate). It stays as a tripwire: if anyone re-opens a second * build path in the campaign layer, this is where the divergence surfaces. */ test('PARK-YARD-BUILD-SAME: parkBoardBuild reproduces the four-way crossing build exactly', () => { const CAMP = require('./campaign.js'); const rows = []; const cmp = (label, kind, cell) => { const a = E._parkYardSig(CAMP._parkCrossBoard(kind, cell)); const b = E._parkYardSig(E.parkBoardBuild(kind, cell)); rows.push(label); assert.strictEqual(b, a, 'build diverged on ' + label); }; for (const seed of [1, 2, 3]) { for (const id of Object.keys(E.PARK_FIELD_MECHS)) cmp('field/' + id + '/' + seed, 'm1', E.PARK_FIELD_MECHS[id].cell(seed)); cmp('push/' + seed, 'm1', { goalVariant: 'boxpad', hazard: { kind: 'meadow', damage: 1, d: 2 }, seed, safetyForm: 'static', mech: { goalMech: 'boxpad', safetyMech: 'static', moveMech: 'push' } }); cmp('slide/' + seed, 'm1', { goalVariant: 'harvest', arch: E.PARK_SLIDE_ARCH, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static', moveMech: 'slide' } }); for (const kind of Object.keys(E._PARK_KIND_ARCHS_X)) for (const arch of E._PARK_KIND_ARCHS_X[kind]) cmp('walk/' + kind + '/' + arch + '/' + seed, kind, { goalVariant: 'harvest', arch, hazard: { kind: 'meadow', damage: 1, d: 2 }, seed, safetyForm: 'static', mech: { goalMech: 'harvest', safetyMech: 'static' } }); } console.log(` [PARK-YARD-BUILD-SAME] ${rows.length} cell rows identical through both paths`); }); /* ============ THE CROSSING SESSION (design 2026-07-31) ============================================ * The scored TRANSFER session draws generic boards and has never read PARK_CROSSINGS; the designed * cells get their own ledger instead of being mixed in, because mixing would turn its scores from a * function of transfer distance into a function of cell difficulty and make every figure ever * recorded incomparable. These gates pin the two properties that separation depends on: the new * session touches only its own slot, and it never repeats a board inside one run. * =============================================================================================== */ // SEED 7 IS THE WITNESS, not an arbitrary pick. The first implementation drew the line-up with a // modular stride, and seed 7 is where that degenerated: 16 live slots, stride 8, an orbit of two, // y16/y52 alternating for a whole session. Seed 42 draws stride 13 — coprime with 16 — and shows // no repeat at all, so a gate written on 42 would have passed the very bug it exists to hold down. test('PARK-XSESSION-SEPARATE: the crossing session seats live crossings on its OWN ledger and never repeats a board', () => { const run = CAMP.createRun({ ...CAMP.LIVE_OPTS, seed: 7 }); const live = CAMP._parkLiveCrossings(run); assert.ok(live.length >= 8, `only ${live.length} live crossings — the session cannot fill ${CAMP.PARK_XSESSION_N} episodes`); for (const c of live) assert.ok(c.ship, `${c.id} is not shipped — a preview cannot carry a scorecard`); const s = CAMP.startParkCrossingSession(run); assert.ok(s && s.crossing === true, 'the crossing session must mark itself as one'); assert.strictEqual(run.park.session, undefined, 'the crossing session wrote to run.park.session — that is the TRANSFER ledger and it must stay untouched'); let guard = 0; while (guard++ < 40) { const t = run.park.task; if (!t) break; const P = t.game.P; while (!P.over) CAMP.parkTaskMove(run, E.parkOracleMove(P, t.persona)); if (!CAMP.parkCrossingSessionAdvance(run)) break; } assert.strictEqual(s.done, true, 'the session did not end'); assert.strictEqual(s.ids.length, s.n, `session ran ${s.ids.length} episodes, expected ${s.n}`); // NO REPEATS. A modular stride was the first implementation and it is exactly what this pins // against: with 16 live slots a stride of 8 walks an orbit of two, and seed 7 drew y16/y52 // alternating for a whole session — a scorecard's worth of numbers about two boards. assert.strictEqual(new Set(s.ids).size, s.ids.length, `the line-up repeats a board: ${s.ids.join(' ')} — a seeded PERMUTATION, not a stride`); assert.strictEqual(run.park.session, undefined, 'the transfer ledger was written to during the run'); const rep = CAMP.parkCrossingSessionReport(run); assert.ok(rep && rep.crossing === true, 'the crossing scorecard must carry its own mark — the two rulers are never summed, so every ' + 'surface that prints one has to be able to say which it used'); const rows = s.ids.filter(id => run.park.results[id]).length; assert.strictEqual(rows, s.ids.length, `${rows}/${s.ids.length} episodes stored a row`); assert.strictEqual(CAMP.parkSessionReport(run), null, 'parkSessionReport read something — the transfer scorecard must be empty when only the crossing session ran'); console.log(` [PARK-XSESSION-SEPARATE] ${s.ids.length} episodes, ${new Set(s.ids).size} distinct: ` + `${s.ids.map(i => i.replace('cx:', '')).join(' ')} · reasons ${JSON.stringify(rep.reasons)}`); }); test('PARK-XSESSION-SEEDED: the line-up is deterministic per seed and differs between seeds', () => { const order = (seed) => CAMP._parkXSessionOrder(CAMP.createRun({ ...CAMP.LIVE_OPTS, seed })) .map(c => c.id).join(' '); const a1 = order(7), a2 = order(7); assert.strictEqual(a1, a2, 'the same seed drew two different line-ups — the draw is not seed-pure'); assert.notStrictEqual(order(7), order(42), 'two seeds drew the SAME line-up — the seed does not reach the draw'); }); /* ---- GATE: PARK-XSESSION-DOOR — the crossing session has a way in (2026-07-31) -------------- * PARK-SESSION-DOOR exists because a scored session was once reachable only from the scorecard * that finishing one produces: the chain was healthy, every gate was green, and no human could * start it. A SECOND session now exists, so it needs the same guard — plus one the first did not: * the two must never be able to open each other's ledger, because their scores are different * rulers and a scorecard that silently switched rulers would be worse than no scorecard. * Source assertions (app.js needs the DOM to run), the C1 drawToken idiom. * ------------------------------------------------------------------------------------------- */ test('PARK-XSESSION-DOOR: the hub has a key that opens the CROSSING session, and it stays on its own ledger', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); // (A) THE DOOR EXISTS and it is a HUB key. The header ▶ keeps its documented promise (the // transfer session, pinned by PARK-SESSION-DOOR), so the crossing ledger needs its own opener. assert.ok(/e\.key === 'c' \|\| e\.key === 'C'/.test(src), "no 'c'/'C' key handler in app.js — the crossing session has no door, which is exactly the " + 'shape of the 2026-07-29 bug PARK-SESSION-DOOR was written for'); assert.ok(/parkSessionBegin\(true\)/.test(src), 'nothing calls parkSessionBegin(true) — the crossing branch is unreachable'); // (B) THE CAPTION PROMISES IT. The hub caption is the only place a player is told what the // keys do; a door nobody is told about is not a door. (The 2026-07-29 bug was found BY the // caption disagreeing with the code, so the caption is load-bearing evidence here.) const cap = src.match(/sk === 'hub'\) s = '([^']*)'/); assert.ok(cap, 'the hub caption moved — PARK-XSESSION-DOOR cannot read what the player is promised'); assert.ok(cap[1].indexOf('C =') >= 0, `the hub caption does not mention the C key: ${cap[1]}`); // (C) THE LEDGERS DO NOT CROSS. Every reader must go through the two helpers, so a future edit // cannot quietly point one session's screen at the other's rows. assert.ok(/const _sessLedger = \(run\) =>/.test(src) && /const _sessReport = \(run\) =>/.test(src), 'the ledger selectors are gone — each screen would have to decide for itself which session it ' + 'is showing, and one of them will eventually decide wrong'); // The selectors themselves MUST read the slots — that is what they are for — so cut their block // out before scanning. Everything left is a screen, and no screen may name a slot. const selAt = src.indexOf('const _sessLedger = '); assert.ok(selAt > 0, 'the ledger selector block is gone'); const MARK = 'END LEDGER SELECTORS'; const selEnd = src.indexOf(MARK, selAt); assert.ok(selEnd > selAt, `the '${MARK}' marker is gone — without it this gate cannot tell the selectors (which MUST ` + 'name the slots) from the screens (which must not)'); // strip comments: this scans CODE, and three of app.js's prose lines mention the slots by name. const code = (src.slice(0, selAt) + src.slice(src.indexOf('*/', selEnd) + 2)) .replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, ''); const screens = code.split('\n'); const stray = (needle, allowed) => screens.filter(l => l.indexOf(needle) >= 0 && l.indexOf(allowed) < 0); const strayLedger = stray('run.park.session', '_sessLedger'); assert.deepStrictEqual(strayLedger.map(l => l.trim()), [], 'a screen reads run.park.session directly instead of the selectors — with two sessions live ' + 'that is a scorecard showing the other ruler:\n ' + strayLedger.join('\n ')); const strayReport = stray('C.parkSessionReport', '_sessReport'); assert.deepStrictEqual(strayReport.map(l => l.trim()), [], 'a screen calls C.parkSessionReport directly instead of _sessReport(run):\n ' + strayReport.join('\n ')); }); /* ---- GATE: PARK-XSESSION-NAMED — a scorecard must say which ruler it used (2026-07-31) ------- * parkCrossingSessionReport's contract ends: "The returned row carries `crossing: true` so every * surface that prints it has to say which ruler it used." Until now no surface did. Both scorecard * screens read whichever report `_sessReport` handed them and printed the same fixed title, so a * crossing session and a transfer session produced two visually identical scorecards carrying * numbers that are NOT comparable — discovery/maintenance over transfer distance in one, over * designed-cell difficulty in the other. That is the failure the split ledgers exist to prevent, * arriving one layer later: the campaign kept the rows apart and the view put them back together. * * This gate pins the view's half. `_sessRulerName` is the single sentence both surfaces print, and * `_sessOtherReport` is the second reader that lets ONE screen show both rows side by side without * a combined figure. Source assertions for the screens (app.js needs the DOM), plus a behavioural * clause for the part that can actually run: the two readers must be callable on the same run * without either disturbing the other's ledger. * ------------------------------------------------------------------------------------------- */ test('PARK-XSESSION-NAMED: both scorecards name their ruler, and the two readers do not disturb each other', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); // (A) THE SENTENCE EXISTS, IN ONE PLACE. Two surfaces printing their own wording drift apart. assert.ok(/const _sessRulerName = \(crossing\) =>/.test(src), 'the ruler-name helper is gone — each scorecard would word it itself and they will disagree'); assert.ok(/const _sessOtherReport = \(run\) =>/.test(src), 'the second reader is gone — the scorecard cannot show the other ruler beside its own'); // (B) BOTH SURFACES USE IT. Counting the call sites is what stops one screen quietly reverting // to a fixed title: the board scorecard and the hud panel are two, and both must ask. const uses = (src.match(/_sessRulerName\(/g) || []).length; assert.ok(uses >= 3, `_sessRulerName is called ${uses}x — expected at least 3 (board title, board's other-ruler ` + 'line, hud panel title); a surface that stopped asking is printing an unlabelled scorecard'); // (C) THE OLD UNLABELLED TITLE IS GONE. This is the exact string that made the two scorecards // indistinguishable, and it is the red witness for this gate: put it back and (C) fails. assert.ok(src.indexOf('세션 결과 / session scorecard') < 0, 'the fixed unlabelled scorecard title is back — a crossing scorecard and a transfer scorecard ' + 'again render identically while carrying numbers measured with different rulers'); // (D) NO COMBINED FIGURE. The whole point of two rulers is that adding them answers nothing. assert.ok(/합산하지 않습니다/.test(src), 'the "not summed" caveat is gone from the side-by-side row — a reader seeing two rates stacked ' + 'will average them, which is precisely the reading the split ledgers exist to prevent'); // (E) BEHAVIOURAL: the two readers coexist. parkCrossingSessionReport aggregates by TEMPORARILY // swapping pk.session, so a botched restore would leave the transfer ledger holding crossing // rows — and the side-by-side screen calls both readers back to back on one run, which is // exactly the sequence that would expose it. const run = CAMP.createRun({ ...CAMP.LIVE_OPTS, seed: 7 }); CAMP.startParkCrossingSession(run); let guard = 0; while (guard++ < 40) { const t = run.park.task; if (!t) break; const P = t.game.P; while (!P.over) CAMP.parkTaskMove(run, E.parkOracleMove(P, t.persona)); if (!CAMP.parkCrossingSessionAdvance(run)) break; } const before = run.park.session; const x1 = CAMP.parkCrossingSessionReport(run); const t1 = CAMP.parkSessionReport(run); // the other reader, immediately after const x2 = CAMP.parkCrossingSessionReport(run); // and again, to catch a one-shot corruption assert.strictEqual(run.park.session, before, 'the transfer ledger slot changed identity after reading the crossing report — the temporary ' + 'swap in parkCrossingSessionReport did not restore it'); assert.ok(x1 && x1.crossing === true, 'the crossing report lost its ruler tag'); assert.strictEqual(t1, null, 'the transfer reader returned a report on a run that never started a transfer session — it is ' + 'reading the crossing rows, which is the ruler-swap this whole split exists to prevent'); assert.strictEqual(x2.played, x1.played, 'reading the crossing report twice gave different episode counts — the first read mutated it'); console.log(` [PARK-XSESSION-NAMED] crossing ${x1.played}/${x1.n} · transfer reader null · ` + `_sessRulerName call sites ${uses}`); }); /* ---- GATE: Y55-M2-SEAT — the roster's last kind hole, and the seat that closes it (2026-07-31) -- * m2 was never a missing grammar: the scored transfer session draws m2 boards on 109 of 400 * episodes. What was missing was a CROSSING SEAT, and the separation map read that as three * uncovered cells (m2 x GC/GN/CN) no repair could close. A `double` seat cannot close it — every * walk->walk m2 double leaks the surface mimic on half its probes, because two generic walk boards * from one arch pool share a surface (measured: plans/2026-07-31-m2-gap-findings.md). Putting a * FIELD on the play leg is what buys the anti-mimic back, and `fire` is the mechanic whose control * is non-vacuous — three others read 0 leaks on ZERO probes, which is not a clean leg, it is an * unrun one (the y26 rule). * ---------------------------------------------------------------------------------------------- */ test('Y55-M2-SEAT: the m2 seat exists, ships on a derived pin, and shares y17\'s yard on the other ruler', () => { const cx = CAMP.PARK_CROSSINGS.find(c => c.id === 'y55'); assert.ok(cx, 'the y55 slot is gone — the roster has no m2 seat and the m2 x {GC,GN,CN} cells reopen'); assert.strictEqual(cx.kind, 'm2', 'y55 exists to seat kind m2; any other kind leaves the hole open'); assert.ok(!cx.open, 'a measured crossing is not a preview'); assert.strictEqual(cx.ship, true, 'y55 ships'); assert.strictEqual(CAMP.PARK_Y55_SHIPPABLE, true, 'y55.ship is not backed by its own pairing measurement — derive, never assert'); // THE SEAT IS THE CLAIM. Two axes change across this crossing (goalMech + fieldMech); one would // put it at y12's distance-1, which is the shape that never carried a promotion. assert.strictEqual(cx.demoMech.goalMech, 'deliver', 'the demo leg is deliver — reach is y17\'s'); assert.strictEqual(cx.playMech.fieldMech, 'fire', 'the play leg rides the fire yard'); assert.notStrictEqual(cx.demoMech.goalMech, cx.playMech.goalMech, 'goalMech must differ too, or the crossing changes one axis and reads at distance 1'); // ONE YARD, TWO RULERS — and they must stay different rulers. const y17 = CAMP.PARK_CROSSINGS.find(c => c.id === 'y17'); assert.ok(y17 && y17.playMech.fieldMech === 'fire', 'y17 must still ride the same yard'); assert.notStrictEqual(cx.kind, y17.kind, 'y55 and y17 sit on the SAME fire yard; if they ever share a kind the second seat measures ' + 'nothing the first did not, and the m2 coverage claim becomes false while both stay green'); // THE m2 COVERAGE CLAIM ITSELF. This is what the seat is for; assert it rather than trust it. const shippedM2 = CAMP.PARK_CROSSINGS.filter(c => c.ship && c.kind === 'm2'); assert.ok(shippedM2.length >= 1, 'no shipped m2 slot — the separation map\'s three m2 coverage cells are uncovered again'); }); /* ---- GATE: PARK-SHIP-SEED-ROBUST — the ship pin reads ONE seed; a shipped cell must hold ---- * DISCOVERED 2026-07-31 while sweeping preview yards for a promotable ruler. * `_parkYBatchRecovers(id, seed, fallbackSeed)` evaluates exactly ONE seed — the slot's designated * PARK_Y*_SHIP_SEED. That is the whole of `PARK_Y*_SHIPPABLE`, and therefore the whole of what any * derive-never-assert gate checks. So a cell that happens to be clean on its pin seed and muddy * everywhere else passes every existing gate. * * That is not hypothetical. y54 (shifter) reads PARK_Y54_SHIPPABLE === true on seed 11 while only * 5 of 12 seeds actually recover — on the other 7 the blind order comes back UNDECIDED, because the * board poses C-N and G-N fully but G-C only 48/72. * TO BE PRECISE ABOUT THE HOLE: flipping that row to ship:true does not pass silently — the roster * COUNT pins (CAMP-SHIP-UNMOVED, CAMP-CROSS-SWEEP, CROSS-DEMO-SEAM) all fire on a roster change. * But those are bookkeeping: they say "a number moved, say so", and the fix is to bump them, which * this session did four times in one afternoon for legitimate seats. NOTHING measured whether the * cell HOLDS. The slot comment ("preview until it holds") recorded a HUMAN judgement that no machine * was making, and a bookkeeping pin is not a substitute for it. This gate makes the judgement. * * MEASURED FLOOR (seeds 1..12, this gate's own range): every LIVE cell today is 12/12 except y53 at * 11/12; the pin-says-true preview is 5/12. 10 separates them with a seed of headroom and is not a * number anyone can drift past quietly — raising a cell to ship now costs a real measurement. * ------------------------------------------------------------------------------------------- */ test('PARK-SHIP-SEED-ROBUST: a shipped cell recovers on most seeds, not just its pin seed', () => { const FLOOR = 10, N = 12; const rows = []; for (const key of Object.keys(CAMP)) { const m = /^_park(Y\d+)Recovers$/.exec(key); if (!m) continue; const id = m[1].toLowerCase(); const slot = CAMP.PARK_CROSSINGS.find(c => c.id === id); if (!slot || !slot.ship) continue; // previews are allowed to be muddy let ok = 0; for (let seed = 1; seed <= N; seed++) if (CAMP[key](seed)) ok++; rows.push({ id, ok }); } assert.ok(rows.length >= 8, `only ${rows.length} shipped cells carry a batch recover fn — the discovery loop stopped finding ` + 'them, so this gate would pass by inspecting nothing'); const weak = rows.filter(r => r.ok < FLOOR); assert.deepStrictEqual(weak, [], 'a SHIPPED cell recovers on too few seeds — its ship pin reads one lucky seed while the scored ' + 'rotation will draw many:\n ' + weak.map(r => `${r.id}: ${r.ok}/${N}`).join('\n ')); // AND the converse, so the floor cannot be met by a cell that simply never mints: every row must // have been exercised. for (const r of rows) assert.ok(r.ok > 0, `${r.id} recovered on NO seed — the pin is measuring something else`); console.log(` [PARK-SHIP-SEED-ROBUST] ${rows.length} shipped cells, floor ${FLOOR}/${N}: ` + rows.map(r => `${r.id} ${r.ok}`).join(' · ')); }); /* ---- GATE: Y56-CARRY-CARE-SEAT — the C-N ruler on a yard that already ships (2026-07-31) ----- * This session proved a law across three preview cells (y12/y25/y50, see * plans/2026-07-31-three-cells-one-law.md): the device that poses one pair BLOCKS another, because * _parkAwardsFor awards a pair only when EVERY order consistent with the observed move agrees on * it — so the turn's decision must fall BETWEEN those two minds, which means the mind above them * must have nothing left to prove at that beat. Twenty-odd measurements could not give any of the * three such a beat by geometry alone. * * The law then selected this seat. Sweeping the SHIPPED modules under m3 (den = C-N), only `carry` * cleared a NON-VACUOUS control: fire and ledge read 0 leaks on ZERO probes — an unrun control, not * a clean one (the y26 rule) — and downed leaks half. carry reads 0 leaks on 144 PROBES. * ---------------------------------------------------------------------------------------------- */ test('Y56-CARRY-CARE-SEAT: the C-N seat exists, ships on a derived pin, and never shares y16\'s ruler', () => { const cx = CAMP.PARK_CROSSINGS.find(c => c.id === 'y56'); assert.ok(cx, 'the y56 slot is gone — the roster loses its second C-N-ruled field seat'); assert.strictEqual(cx.kind, 'm3', 'y56 exists to read the CARE pair; any other kind is a different cell'); assert.ok(!cx.open, 'a measured crossing is not a preview'); assert.strictEqual(cx.ship, true, 'y56 ships'); assert.strictEqual(CAMP.PARK_Y56_SHIPPABLE, true, 'y56.ship is not backed by its own pairing measurement — derive, never assert'); // TWO AXES CHANGE, as with y55. One would put it at y12's distance-1, the shape that has never // carried a promotion. assert.strictEqual(cx.playMech.fieldMech, 'carry', 'the play leg rides the carry yard'); const y16 = CAMP.PARK_CROSSINGS.find(c => c.id === 'y16'); assert.ok(y16 && y16.playMech.fieldMech === 'carry', 'y16 must still ride the same yard'); assert.notStrictEqual(cx.demoMech.goalMech, y16.demoMech.goalMech, 'the two seats must not share a demo verb, or the second replays the first'); // The play goal is NOT an axis for a FIELD leg: _parkCrossingPlayCell mints the module's own cell, // so `carry` dictates the verb and both seats declare what it mints. Asserting a difference there // would pin a value neither slot controls (an earlier draft of this gate did exactly that and // passed while the two legs minted the SAME goal). What actually separates them is the demo leg // and the ruler, asserted above and below. assert.strictEqual(cx.playMech.goalMech, y16.playMech.goalMech, 'both carry seats must DECLARE the goal the module mints — a divergent declaration is a lie ' + 'about what the play leg does'); // Written as the INTENT rather than as the two module names. The earlier form spelled the answer // out — `cx.demoMech.moveMech == null && y16.demoMech.moveMech === 'slide'` — and so it failed on // 2026-08-01 for a reason that had nothing to do with the two seats sharing a ruler: y16's demo // leg moved off ice onto `ledge` for legibility, and a gate that names one module cannot tell // "these two collided" from "one of them changed". What this clause protects is that the two // carry seats do not demonstrate on the SAME module; say that, and it survives either seat moving. const demoModule = (m) => m.fieldMech || m.moveMech || 'walk'; assert.notStrictEqual(demoModule(cx.demoMech), demoModule(y16.demoMech), `both carry seats demo on '${demoModule(cx.demoMech)}' — the demo legs must differ in MODULE, ` + 'not only in verb, or the second seat replays the first and measures nothing new'); // ONE YARD, TWO RULERS — and they must stay different rulers. Same clause as Y55-M2-SEAT: the day // they match, the second seat measures nothing the first did not while both stay green. assert.notStrictEqual(cx.kind, y16.kind, 'y56 and y16 sit on the SAME carry yard; if they ever share a kind the second seat measures ' + 'nothing the first did not, and its coverage claim becomes false while both stay green'); // THE HARD PAIR IS THE POINT. A C-N seat that stopped posing C-N would be the y12 disease. assert.deepStrictEqual(E._parkKindPair('m3'), [['C', 'N']], 'm3 is no longer the C-N ruler — this seat was measured against that den specifically'); }); /* ---- GATE: X5-M4-STANDING-GUARD — the lone m4 carrier must keep CARRYING, not just DECLARING ---- * The separation map flagged m4 x {GC,GN,CN} as covered-but-thin: three cells resting on x5 alone. * Two sweeps on 2026-07-31 settled whether a SECOND m4 seat could exist, and the answer shapes this * gate. Under kind m4 only `fire` clears a non-vacuous bar (0 leaks / 36 probes; 3 seeds x 6 personas * x 2 dens, stable across 3 demo verbs and 2 si values). `carry` and `yield` read 0 leaks on ZERO * probes — not a clean control, an unrun one (the y26 rule). `downed` leaks half. `toll` never * expresses C-N at all (recovery 0/18, every episode undetermined). So the only second m4 seat on * offer would be a THIRD seat on the fire yard that y17 and y55 already share — trading a thin cell * for a fat module, and diluting what "a live game" means on the hub. Not taken. The measurement is * kept in plans/2026-07-31-m4-m5-coverage-findings.md should that trade ever start to look good. * * What IS taken is teeth. Today m4 coverage is DECLARED by x5's `kind` field, and nothing asserts x5 * still EXPRESSES both den pairs. Degrade the board until it poses only G-C and the coverage table * goes on reading "m4 x GN covered" while it is not — silently, which is exactly the failure mode * that matters when three cells rest on one slot. This gate makes that failure loud. * ---------------------------------------------------------------------------------------------- */ test('X5-M4-STANDING-GUARD: the lone m4 seat still poses BOTH den pairs in every persona', () => { const cx = CAMP.PARK_CROSSINGS.find(c => c.id === 'x5'); assert.ok(cx, 'the x5 slot is gone — m4 has no shipped carrier at all'); assert.strictEqual(cx.kind, 'm4', 'x5 is the roster\'s only m4 seat; another kind empties three cells'); assert.strictEqual(cx.ship, true, 'x5 must ship — a preview does not cover a roster cell'); assert.ok(CAMP.PARK_CROSSINGS.filter(c => c.ship && c.kind === 'm4').length >= 1, 'no shipped m4 slot — the m4 x {GC,GN,CN} cells are uncovered'); // THE TEETH. Declaring kind m4 is cheap; POSING both den pairs in every persona is the coverage. const dens = E._parkKindPair('m4'); assert.deepStrictEqual(dens, [['G', 'C'], ['G', 'N']], 'm4 den pairs moved — the floors below were measured against G-C and G-N specifically'); let episodes = 0, complete = 0; const posed = {}; for (let seed = 1; seed <= 8; seed++) { const dSeed = (seed * 61 + 11 + cx.si * 197) >>> 0; // MUST match parkCrossings' derivation const pSeed = (seed * 89 + 23 + cx.si * 211) >>> 0; const dc = CAMP._parkCrossingDemoCell(cx.kind, cx.demoMech, dSeed, cx.mechanic); const pl = CAMP._parkCrossingPlayCell(cx.kind, dc, cx.playMech, pSeed, cx.mechanic, cx.playHaz, true); if (!pl || pl.filtered !== true) continue; const board = () => CAMP._parkCrossBoard(cx.kind, pl.cell); for (const persona of E.PARK_PERSONAS) { episodes++; const P = E.parkPlayout(board(), persona); if (P.reason === 'complete') complete++; for (const q of dens) { if (E.parkPairExpressed(board(), P.moves, q) > 0) { posed[q.join('')] = (posed[q.join('')] || 0) + 1; } } } } // Measured 2026-07-31, seeds 1..8 x 6 personas: 48 episodes, 48 complete, G-C 48, G-N 48. // C-N sits at 36/48 on that same run and is deliberately NOT asserted — it is not an m4 den pair, // and pinning it would pin a number the kind does not depend on. It is also the RED WITNESS for // this gate: swap either floor below to ['C','N'] and the strict 48 fails at 36, which is how we // know the floor distinguishes "posed in every persona" from "posed in most of them". assert.strictEqual(episodes, 48, 'x5 stopped minting 8 filtered seeds — the floors below shift under it'); assert.strictEqual(complete, 48, 'a persona no longer finishes x5 alive; its faithful leg is broken'); assert.strictEqual(posed['GC'], 48, 'x5 no longer poses G-C in every persona — the m4 x GC cell is hollow while the roster claims it'); assert.strictEqual(posed['GN'], 48, 'x5 no longer poses G-N in every persona — the m4 x GN cell is hollow while the roster claims it'); }); /* ---- GATE: M5-HAS-NO-CROSSING-SEAT — and cannot, by construction ------------------------------ * m5 is the observer kind and the one kind with an EMPTY den: `_PARK_TASK_NEED.m5.den === []`. * Every crossing's anti-mimic control walks `_parkKindPair(kind)`, so under m5 that loop runs zero * times and the surface-mimic bar reads "0 leaks" having probed NOTHING. By the y26 rule that is not * a clean control but an unrun one, and the ship bar would be passing vacuously on its hardest * clause. m5 therefore belongs to the TRANSFER layer (where it is drawn and scored normally) and can * never take a crossing seat until it declares a den. Asserting this keeps a future session from * spending a week rediscovering it, and turns RED the day someone gives m5 a den — at which point * the seat becomes possible and this gate should be replaced, not deleted. * ---------------------------------------------------------------------------------------------- */ test('M5-HAS-NO-CROSSING-SEAT: m5 declares no den, so its anti-mimic control cannot run', () => { assert.deepStrictEqual(E._parkKindPair('m5'), [], 'm5 now declares a den — the vacuity argument below no longer holds and a crossing seat may be ' + 'possible; re-measure the anti-mimic bar and replace this gate rather than deleting it'); assert.ok(!CAMP.PARK_CROSSINGS.some(c => c.ship && c.kind === 'm5'), 'a shipped m5 crossing exists, but m5 has no den pair — its surface-mimic control probes nothing, ' + 'so that slot ships on a vacuous anti-mimic clause'); }); /* ============ Y58 THE FLOWING ROAD — road 필드 모듈 게이트 (2026-08-03) ============ */ test('Y58-ROAD-BOARD: 5차선 도로가 서고, 4대가 서로 다른 칸에 있고, dyn 멤버가 전부 build 시점에 있다', () => { // 커버리지를 1..24 로 넓힌다(2026-08-05): office-hours 측정이 1..24 범위였고, 1..8 만 // 돌면 L2 의 NPC 수정이 시드 9..24 에서 조용히 재발할 수 있다. // // 차선 수 단언이 3 -> 5 로 바뀌었다(L3, 2026-08-05). 계획은 이 게이트의 "교통 NPC 는 // 둘이다" 줄만 위험하다고 봤는데, 실제로 먼저 깨진 것은 차선 수였다. 분리대가 열을 먹던 // 구조에서는 5칸 중 3칸만 차선이었고 이제 다섯 전부가 주행칸이다. 몸 개수 단언(네 몸)은 // 그대로 둔다 — xx 액터는 road.npc 가 아니라 road.hazard 로 따로 살고, 다섯 몸은 // Y58-ROAD-FIVE-BODIES 가 잰다. for (let seed = 1; seed <= 24; seed++) { const st = E._parkRoadBuild(E._parkRoadCell(seed)); const park = st.park; assert.ok(park.road, `seed ${seed}: park.road 가 있어야 한다`); assert.strictEqual(park.road.lanes.length, 5, `seed ${seed}: 차선은 정확히 다섯이다`); assert.strictEqual(park.road.npc.length, 2, `seed ${seed}: 교통 NPC 는 둘이다`); // 스폰은 상하 중앙 — m4.early 가 조기 갈등을 요구하므로 밀릴 여지가 위아래로 있어야 한다. assert.strictEqual(st.pos[0].y, Math.floor(st.N / 2), `seed ${seed}: 워커는 상하 중앙에서 시작한다`); // 네 몸이 서로 다른 칸에 있다 (워커·동료·NPC 둘). const seen = new Set([`${st.pos[0].x},${st.pos[0].y}`, `${st.pos[1].x},${st.pos[1].y}`]); for (const n of park.road.npc) seen.add(`${n.x},${n.y}`); assert.strictEqual(seen.size, 4, `seed ${seed}: 네 몸이 겹치지 않는다`); // 네 몸 전부가 보드 안(0 <= x,y < N)에 있다. wall 검사만으로는 이걸 못 잡는다 — y*N+x 가 // 음수(예: y=-1)여도 Set.has() 는 그냥 false 를 돌려줄 뿐 던지지 않으므로, 몸이 보드 밖으로 // 나가도 "벽이 아니다"라는 통과가 조용히 나온다. 그래서 좌표 범위를 직접 잰다. const allBodies = [st.pos[0], st.pos[1], ...park.road.npc]; for (let i = 0; i < allBodies.length; i++) { const b = allBodies[i]; assert.ok(b.x >= 0 && b.x < st.N, `seed ${seed}: body ${i} 의 x=${b.x} 가 보드 밖이다`); assert.ok(b.y >= 0 && b.y < st.N, `seed ${seed}: body ${i} 의 y=${b.y} 가 보드 밖이다`); } // 추월 토큰 셋이 차량 앞칸에 있다 (+ Task 6 이 넷째를 더한다 — 동료의 계약 표적, // park.chain 밖의 gtype 3. 아래 for 루프는 그대로 처음 셋만 겨눈다: 추월 대상은 // 여전히 정확히 셋이다). Fix round 2(2026-08-04): chainAnyOf 는 더 안 쓴다 — 세 다리 // 순서(park.chain=[1,2,0], NPC 둘 먼저 동료 자신은 맨 뒤 — 이유는 Y58-ROAD-CHAIN // 게이트 주석과 engine.js 의 park.chain 주석 참고)로 바뀌었다. assert.strictEqual(st.tokens.length, 4, `seed ${seed}: 추월 대상 셋(동료 1 + NPC 2) + 동료의 계약 표적 하나`); assert.deepStrictEqual(park.chain, [1, 2, 0], `seed ${seed}: 추월 대상은 여전히 gtype 0..2 셋뿐이고 세 다리 순서로 선언된다`); const bodies = [st.pos[1], ...park.road.npc]; for (let i = 0; i < 3; i++) { assert.strictEqual(st.tokens[i].x, bodies[i].x, `token ${i} 는 제 차량과 같은 차선`); assert.strictEqual(st.tokens[i].y, bodies[i].y - 1, `token ${i} 는 제 차량 바로 앞`); // 좌표 범위를 먼저 잰다 — 음수 y 는 y*N+x 를 음수 키로 만들어 wall 이 절대 갖고 있지 // 않은 값이 되므로, wall 검사만 있으면 보드 밖으로 나간 토큰도 "벽 아님"으로 조용히 // 통과한다(브리프가 경고한 route mask 의 Infinity 함정이 여기서 다시 새는 지점). assert.ok(st.tokens[i].x >= 0 && st.tokens[i].x < st.N, `token ${i} 의 x=${st.tokens[i].x} 가 보드 밖이다`); assert.ok(st.tokens[i].y >= 0 && st.tokens[i].y < st.N, `token ${i} 의 y=${st.tokens[i].y} 가 보드 밖이다`); assert.ok(!st.wall.has(st.tokens[i].y * st.N + st.tokens[i].x), `token ${i} 의 칸이 벽이면 그 목적지는 영원히 도달 불가다`); } // 넷째 토큰(동료의 계약 표적)도 보드 안·벽 아님·동료의 홈 차선이어야 한다. const goal = st.tokens[3]; assert.strictEqual(goal.gtype, 3, `seed ${seed}: 계약 표적의 gtype 은 3 이다`); assert.ok(park.road.lanes.indexOf(goal.x) >= 0, `seed ${seed}: 계약 표적은 차선 위에 있다`); assert.ok(!st.wall.has(goal.y * st.N + goal.x), `seed ${seed}: 계약 표적이 벽이면 영원히 도달 불가다`); assert.strictEqual(park.contracts.length, 1, `seed ${seed}: 계약은 정확히 하나다`); assert.strictEqual(park.contracts[0].gem, 3, `seed ${seed}: 계약은 지워지지 않는 넷째 토큰을 가리킨다`); // needPairs 는 필수다 — 없으면 P.posed 를 아무도 갱신하지 않아 Task 6 의 측정이 // 기하와 무관하게 0 이 된다 (engine.js:7220 "그 보드에서는 죽은 필드다"). assert.strictEqual(park.needPairs, true, `seed ${seed}: needPairs 없이는 쌍을 못 센다`); // _parkDeepClone 규칙: 모든 dyn 멤버가 build 시점에 존재해야 포크가 안전하다. const d = park.dyn.road; assert.ok(d, `seed ${seed}: dyn.road`); assert.strictEqual(d.scroll, 0); assert.ok(d.passed instanceof Set); assert.strictEqual(d.rearended, 0); assert.strictEqual(d.yields, 0); } }); test('Y58-ROAD-SEED-PURE: build 는 seed 만의 함수다 (C1) — 같은 cell 은 두 번 불러도 같은 보드다', () => { const sig = (st) => JSON.stringify({ N: st.N, wall: [...st.wall].sort((a, b) => a - b), pos: st.pos, road: st.park.road, tokens: st.tokens.map(t => ({ x: t.x, y: t.y, alive: t.alive })), }); for (let seed = 1; seed <= 8; seed++) { const a = sig(E._parkRoadBuild(E._parkRoadCell(seed))); const b = sig(E._parkRoadBuild(E._parkRoadCell(seed))); assert.strictEqual(a, b, `seed ${seed}: 같은 cell 은 같은 보드를 낳는다`); } // build 의 인자 개수가 1 이어야 한다 — persona 를 받을 자리가 없다는 구조적 보장. assert.strictEqual(E._parkRoadBuild.length, 1, '_parkRoadBuild 는 cell 하나만 받는다 (persona 매개변수 금지 — C1)'); }); test('Y58-ROAD-YARD: road 를 등록하면 PARK_YARDS 에 마당이 저절로 생긴다 (레지스트리에 손으로 쓰지 않는다)', () => { const yards = E.PARK_YARDS; assert.ok(yards.road, 'PARK_YARDS.road — 필드 마당은 PARK_FIELD_MECHS 에서 파생된다'); assert.strictEqual(yards.road.lineage, 'field'); assert.strictEqual(yards.road.kindsDeclared, null, '필드 마당은 kind 를 구속하지 않는다 — 슬롯이 kind 를 고른다'); }); test('Y58-ROAD-DRIFT: ↑ 는 제 행을 지키고, 그 외 모든 수는 한 칸 밀린다. ↓ 는 두 칸이다', () => { const mk = () => E.parkStart(E._parkRoadBuild(E._parkRoadCell(3))); // ↑ — 앞으로 한 칸 간 뒤 드리프트 없음 => 순 -1 // 이동 수 문자열은 'up'/'down'이 아니라 엔진 관용구 'U'/'D'다(_PARK_MOVES, engine.js:6983) — // 브리프 원문의 'up'/'down'을 그대로 쓰면 parkStep이 이를 미등록 수로 보고 noise 로 씹어 // 걸음이 전혀 안 나간다(대상 대신 위치가 그대로 남는 게 첫 증상이었다). let P = mk(); let y0 = P.st.pos[0].y; E.parkStep(P, 'U'); assert.strictEqual(P.st.pos[0].y, y0 - 1, '↑ 는 한 칸 전진한다 (드리프트 면제)'); // stay — 안 움직인 뒤 드리프트 => 순 +1 P = mk(); y0 = P.st.pos[0].y; E.parkStep(P, 'stay'); assert.strictEqual(P.st.pos[0].y, y0 + 1, 'stay 는 한 칸 밀린다 — 이 판에서 정지는 공짜가 아니다'); // ↓ — 한 칸 내려간 뒤 드리프트 => 순 +2 P = mk(); y0 = P.st.pos[0].y; E.parkStep(P, 'D'); assert.strictEqual(P.st.pos[0].y, y0 + 2, '↓ 는 두 칸 밀린다 (급브레이크 값)'); }); test('Y58-ROAD-BOARD-12: 보드는 12x12 이고 차선 다섯이 x=3..7 로 연속이다', () => { for (let seed = 1; seed <= 24; seed++) { const st = E._parkRoadBuild(E._parkRoadCell(seed)); assert.strictEqual(st.N, 12, `seed ${seed}: 보드는 12x12 다 (엔진은 정사각만 쓴다)`); assert.deepStrictEqual(st.park.road.lanes, [3, 4, 5, 6, 7], `seed ${seed}: 차선 다섯이 연속이어야 한다 — 분리대가 칸을 안 먹는다`); for (const lx of [3, 4, 5, 6, 7]) for (let y = 0; y < 12; y++) { assert.ok(!st.wall.has(y * 12 + lx), `seed ${seed}: 차선 (${lx},${y}) 은 벽이 아니다`); } for (const wx of [0, 1, 2, 8, 9, 10, 11]) for (let y = 0; y < 12; y++) { assert.ok(st.wall.has(y * 12 + wx), `seed ${seed}: 갓길 (${wx},${y}) 은 벽이다`); } } }); test('Y58-ROAD-EDGE-DASH-SOLID: 모서리 실선 여부는 (시드, 모서리, 행, scroll) 의 순수 함수다', () => { for (const seed of [3, 7, 14]) { const stA = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))).st; const stB = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))).st; for (const xLo of [3, 4, 5, 6]) for (let y = 0; y < 12; y++) { assert.strictEqual(E._parkRoadSolidEdge(stA, xLo, y), E._parkRoadSolidEdge(stB, xLo, y), `seed ${seed}: 같은 시드·같은 scroll 은 같은 답을 내야 한다 (C1)`); } // 공허성: 전부 실선이거나 전부 파선이면 이 장치는 아무것도 안 한다. let solid = 0, total = 0; for (const xLo of [3, 4, 5, 6]) for (let y = 0; y < 12; y++) { total++; if (E._parkRoadSolidEdge(stA, xLo, y)) solid++; } assert.ok(solid > 0 && solid < total, `seed ${seed}: 실선 ${solid}/${total} — 전부이거나 전무면 실선/파선 구분이 공허하다`); } }); test('Y58-ROAD-SOLID-BINDS-BOTH: 실선은 워커와 동료를 둘 다 막는다 (사용자 결정 2026-08-05)', () => { let checked = 0; for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); const st = P.st, n = st.N; const me = E._parkMaskOf(P, 'me'), mate = E._parkMaskOf(P, 'mate'); assert.ok(me, `seed ${seed}: 워커 도메인 마스크가 있어야 한다`); assert.ok(mate, `seed ${seed}: 동료 도메인 마스크가 있어야 한다 — 실선은 사람을 안 가린다`); // 워커의 현재 행에서, 그가 실선 너머로 가려는 칸은 그에게 불법이어야 한다. const wy = st.pos[0].y, wx = st.pos[0].x; for (const dir of [-1, 1]) { const tx = wx + dir; if (tx < 3 || tx > 7) continue; const xLo = Math.min(wx, tx); if (!E._parkRoadSolidEdge(st, xLo, wy)) continue; assert.ok(me(wy * n + tx), `seed ${seed}: 실선 너머 (${tx},${wy}) 는 워커에게 불법이어야 한다`); checked++; } // 동료 쪽도 같은 규칙이다. const my = st.pos[1].y, mx = st.pos[1].x; for (const dir of [-1, 1]) { const tx = mx + dir; if (tx < 3 || tx > 7) continue; const xLo = Math.min(mx, tx); if (!E._parkRoadSolidEdge(st, xLo, my)) continue; assert.ok(mate(my * n + tx), `seed ${seed}: 실선 너머 (${tx},${my}) 는 동료에게도 불법이어야 한다`); checked++; } } assert.ok(checked >= 8, `실선 너머 이동을 ${checked} 번밖에 못 봤다 — 표본이 모자라 이 게이트가 공허하다`); }); test('Y58-ROAD-GATE-CLOSES-WALKER-ONLY: 차단 박자의 전방 폐쇄는 동료에게 안 걸린다', () => { // 위 SOLID-BINDS-BOTH 의 짝이다. 실선은 양쪽, 차단 박자는 워커 전용 — // 이 비대칭이 실제로 서 있는지 잰다. 둘을 같이 안 재면 "동료도 구속한다" 를 // 구현하다 실수로 동료를 얼려도 아무도 모른다. let sawGate = 0; for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let t = 0; t < 30 && !P.over; t++) { if (E._parkRoadGateBeat(P.st)) { sawGate++; const st = P.st, n = st.N; const mate = E._parkMaskOf(P, 'mate'); const frontY = st.pos[0].y - 1; if (frontY >= 0 && mate) { // 워커 전방 행의 칸이, 동료에게는 (실선 때문이 아닌 한) 열려 있어야 한다. const tx = st.pos[1].x; const solidBlocked = false; // 세로 이동엔 모서리 실선이 안 걸린다 if (!solidBlocked && st.pos[1].y - 1 === frontY) { assert.ok(!mate(frontY * n + tx), `seed ${seed}: 차단 박자에 동료 전방 (${tx},${frontY}) 이 막혔다 — ` + `그 폐쇄는 워커 전용이어야 한다`); } } break; } E.parkStep(P, 'stay'); } } assert.ok(sawGate >= 20, `차단 박자를 ${sawGate} 시드에서만 봤다 — 표본이 모자라다`); }); test('Y58-ROAD-HAZARD-SEED-PURE: xx 액터의 궤적은 시드의 순수 함수다 — 예측 불가는 사람에게만', () => { for (let seed = 1; seed <= 24; seed++) { const trace = (s) => { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(s))); const out = []; for (let t = 0; t < 30 && !P.over; t++) { const h = P.st.park.road.hazard; out.push(`${h.x},${h.y}`); E.parkStep(P, 'stay'); } return out.join('|'); }; assert.strictEqual(trace(seed), trace(seed), `seed ${seed}: 같은 시드는 같은 궤적을 내야 한다 (C1, Y58-ROAD-SEED-PURE 의 형제)`); } }); test('Y58-ROAD-HAZARD-CHANGES-LANES: xx 액터는 실제로 차선을 바꾼다 — 안 바꾸면 공허하다', () => { let movers = 0; for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); const x0 = P.st.park.road.hazard.x; let changed = false; for (let t = 0; t < 30 && !P.over; t++) { E.parkStep(P, 'stay'); if (P.st.park.road.hazard.x !== x0) changed = true; } if (changed) movers++; } assert.ok(movers >= 20, `24 시드 중 ${movers} 개에서만 xx 액터가 차선을 바꿨다 — 안 바꾸면 다른 NPC 와 같아서 ` + `조심(C)에 아무것도 안 더한다`); }); test('Y58-ROAD-FIVE-BODIES: 다섯 몸이 서로 다른 칸에 선다', () => { for (let seed = 1; seed <= 24; seed++) { const st = E._parkRoadBuild(E._parkRoadCell(seed)); const h = st.park.road.hazard; const seen = new Set([`${st.pos[0].x},${st.pos[0].y}`, `${st.pos[1].x},${st.pos[1].y}`, `${h.x},${h.y}`]); for (const nn of st.park.road.npc) seen.add(`${nn.x},${nn.y}`); assert.strictEqual(seen.size, 5, `seed ${seed}: 워커·동료·NPC 둘·xx 액터가 서로 다른 칸이어야 한다`); } }); test('Y58-ROAD-POCKET-IS-A-LANE: 포켓은 분리대 유령칸이 아니라 실재하는 주행 차선 칸이다', () => { for (let seed = 1; seed <= 24; seed++) { const st = E._parkRoadBuild(E._parkRoadCell(seed)); const px = st.park.road.pocketX; assert.ok(Number.isInteger(px), `seed ${seed}: pocketX 가 정수여야 한다 (얻은 값 ${px})`); assert.ok(st.park.road.lanes.indexOf(px) > -1, `seed ${seed}: pocketX=${px} 가 차선 목록 ${JSON.stringify(st.park.road.lanes)} 안에 있어야 한다 — ` + `동료를 한 칸 건너(±2) 차선에 두면 두 차선의 중점이 실재하는 주행칸이 된다`); // 워커와 동료는 정확히 두 차선 떨어져 있다. assert.strictEqual(Math.abs(st.pos[0].x - st.pos[1].x), 2, `seed ${seed}: 워커와 동료는 두 차선 간격이어야 중점이 차선 칸이 된다`); } }); test('Y58-ROAD-POCKET-RESPECTS-MASK: 실선이 막으면 포켓은 정직하게 null 이다', () => { let sawNull = 0, sawReal = 0; for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let t = 0; t < 40 && !P.over; t++) { if (E._parkRoadGateBeat(P.st)) { const pk = E._parkRoadPocket(P.st); if (!pk) { sawNull++; } else { sawReal++; const st = P.st, n = st.N; const me = E._parkMaskOf(P, 'me'), mate = E._parkMaskOf(P, 'mate'); const key = pk.y * n + pk.x; assert.ok(!me || !me(key), `seed ${seed} turn ${t}: non-null 포켓 (${pk.x},${pk.y}) 이 워커에게 불법이다 — 거짓말이다`); assert.ok(!mate || !mate(key), `seed ${seed} turn ${t}: non-null 포켓 (${pk.x},${pk.y}) 이 동료에게 불법이다 — ` + `실선이 동료를 막으면 포켓은 null 이어야 한다`); } } E.parkStep(P, 'stay'); } } assert.ok(sawReal > 0, `non-null 포켓을 한 번도 못 봤다 — 실선이 포켓을 전부 죽였다면 그건 설계 실패다`); assert.ok(sawNull + sawReal >= 20, `차단 박자 표본이 ${sawNull + sawReal} 개뿐이다`); }); test('Y58-ROAD-DRIFT-FLOOR: 맨 아래 행에서 드리프트는 제자리다 — 보드 밖으로 나가지 않는다', () => { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(3))); const n = P.st.N; for (let i = 0; i < n * 3 && !P.over; i++) E.parkStep(P, 'stay'); assert.ok(P.st.pos[0].y <= n - 1, '행이 보드 안에 있다'); assert.ok(P.st.pos[0].y >= 0, '행이 음수가 아니다'); }); test('Y58-ROAD-NPC-ALIVE: 교통은 게임 내내 흐른다 — NPC 가 바닥에 얼어붙지 않는다', () => { for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); let lastMove = 0; let prev = P.st.park.road.npc.map(n => `${n.x},${n.y}`).join('|'); for (let t = 1; t <= 40 && !P.over; t++) { E.parkStep(P, 'stay'); const now = P.st.park.road.npc.map(n => `${n.x},${n.y}`).join('|'); if (now !== prev) lastMove = t; prev = now; } assert.ok(lastMove >= 30, `seed ${seed}: NPC 가 턴 ${lastMove} 이후로 안 움직인다. 재생성 전에는 24/24 가 턴 5 에서 ` + `멈췄고, 40턴 중 35턴(87.5%)이 교통 정지였다 — 그 결함이 남아 있다`); } }); test('Y58-ROAD-RECYCLE-SEED-PURE: 재생성 간격은 시드의 순수 함수다', () => { for (let seed = 1; seed <= 24; seed++) { const a = E._parkRoadBuild(E._parkRoadCell(seed)).park.road.recycleGap; const b = E._parkRoadBuild(E._parkRoadCell(seed)).park.road.recycleGap; assert.ok(Array.isArray(a) && a.length === 2, `seed ${seed}: recycleGap 은 길이 2 배열이다`); assert.deepStrictEqual(a, b, `seed ${seed}: 같은 시드는 같은 간격을 내야 한다 (C1)`); for (const v of a) assert.ok(Number.isInteger(v) && v >= 1, `seed ${seed}: 간격은 1 이상 정수다`); } }); test('Y58-ROAD-NPC-DISJOINT-LANES: 두 교통 NPC 는 절대 같은 칸에 겹치지 않는다 (전 구간)', () => { for (let seed = 1; seed <= 24; seed++) { const st0 = E._parkRoadBuild(E._parkRoadCell(seed)); assert.notStrictEqual(st0.park.road.npc[0].x, st0.park.road.npc[1].x, `seed ${seed}: 두 NPC 가 같은 차선에서 출발하면 결국 같은 칸에 겹친다 — 빌드가 막아야 한다`); const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let t = 0; t < 40 && !P.over; t++) { const [a, b] = P.st.park.road.npc; assert.ok(!(a.x === b.x && a.y === b.y), `seed ${seed}, turn ${t}: 두 NPC 가 (${a.x},${a.y}) 에서 겹쳤다. 겹치면 추월 토큰 둘 ` + `(체인 다리 둘)도 같은 칸에 서서 체인이 퇴화한다`); E.parkStep(P, 'stay'); } } }); test('Y58-ROAD-REAREND: 밀려서 드럼을 밟은 것은 별도 장부에 청구된다 (보편 deep-entry 와 안 섞인다)', () => { // 브리프 원문은 'stay'만 반복해 시드를 뒤진다. 그런데 이 판(N=7, 스폰 행 midY=3, 주기 6)에서는 // 'stay'만으로는 절대 드럼을 못 밟는다 — 실측(24 시드 × 40 스텝, 전부 0)으로 확인했다: // 매 'stay' 걸음마다 (행+scroll)이 정확히 +2 씩만 바뀌므로 홀수(3→5→7→9…) 를 벗어나지 // 못하는데 주기 6 은 짝수라 0 mod 6 에 닿을 수 없다. 게다가 세 걸음 만에 바닥(행 6)에 // 붙잡히면 그 뒤로는 (행+scroll) 자체가 안 바뀐다(막혀서 안 밀리므로 드럼 판정도 안 돈다) — // 그러니 20 스텝이 아니라 몇 스텝을 줘도 'stay' 전용으로는 이 장부가 구조적으로 영원히 0 이다. // 'D' 는 제 걸음 +1 에 드리프트 +1 이 더해져 (행+scroll) 을 +3 씩 바꾸므로 이 홀짝 잠김을 // 깬다 — 그래서 'D'/'stay' 를 섞어 "밀림이 실제로 드럼에 닿는 상태"가 있는지를 잰다. 이것도 // 없으면 게이트가 공허하다. // // L3(2026-08-05): 위 산수는 옛 기하(N=7, midY=3)의 것이다. 새 보드는 N=12, midY=6 이라 // 'D'/'stay' 교대는 20 스텝 창 안에서 24 시드 전부 한 번도 안 걸렸다(실측). 그런데 그건 // 상태가 사라졌다는 뜻이 **아니다** — 순수 'D' 로 재면 24/24 시드가 턴 1 에 걸린다(실측). // 바뀐 것은 도달성이 아니라 탐색 경로다. 그래서 바(장부가 구조적으로 0 이 아니다)는 // 그대로 두고, 탐색만 두 갈래로 넓힌다 — 어느 한쪽이라도 걸리면 이 장부는 살아 있다. const probes = [ (i) => (i % 2 === 0 ? 'D' : 'stay'), // 옛 기하에서 홀짝 잠김을 깨던 경로 () => 'D', // 새 기하에서 걸리는 경로 ]; let found = false; for (const pick of probes) { for (let seed = 1; seed <= 24 && !found; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let i = 0; i < 20 && !P.over; i++) { const before = P.st.park.dyn.road.rearended; E.parkStep(P, pick(i)); if (P.st.park.dyn.road.rearended > before) { found = true; break; } } } if (found) break; } assert.ok(found, '시드 1..24 안에 "밀려서 드럼을 밟는" 상태가 최소 하나 있어야 한다 — ' + '없으면 이 장부는 영원히 0 이고 게이트는 공허하다'); }); test('Y58-ROAD-REAREND-CAP: 같은 주기 안 두 번째 REAREND 부터는 장부만 늘고 하트는 안 깎인다', () => { // Fix round 1 (컨트롤러 실측, 2026-08-04): 설계 법칙 1(§3, "드럼은 하트를 쓴다, 죽이지 // 않는다")을 지키려면 주기(6 박자)당 최초 한 번만 하트를 청구해야 한다. 자연 플레이로 // "한 주기 안에 REAREND 가 두 번" 을 만나는 건 드물다(D 는 두 칸씩 밀려 바닥에 금방 // 박히고, stay/L/R 은 +2 씩만 바뀌어 산-1 위상에서 짝-0 위상인 밴드를 영원히 못 밟는다, // 위 REAREND 게이트의 주석 참고). 그래서 이 게이트는 게이트는 접합을 재지 기하를 재지 // 않는다 원칙대로 `pos[0].y` 를 직접 겨눠 "같은 주기 안 두 번째 진입"을 결정론적으로 // 만든다(Y58-ROAD-OVERTAKE 가 토큰 위치를 직접 겨눈 것과 같은 관용구). // // 산수(실측 재확인): scroll=0, y=3 에서 'D' 한 번 — 이동(+1)과 드리프트(+1)로 y=5, tick 이 // scroll 을 1 로 올린다. (5+1)%6===0 이라 첫 REAREND(주기 0). 이어서 y 를 2 로 다시 // 겨누고 'D' 한 번 더 — y=2→3(이동)→4(드리프트), scroll 이 2 로. (4+2)%6===0 이라 같은 // 주기(0) 안 두 번째 REAREND. const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(1))); const heartsStart = P.hearts; P.st.pos[0].y = 3; E.parkStep(P, 'D'); assert.strictEqual(P.st.park.dyn.road.rearended, 1, '첫 REAREND 는 장부에 반드시 찍혀야 한다'); assert.strictEqual(P.hearts, heartsStart - 1, '첫 REAREND 는 하트를 깎는다'); assert.strictEqual(P.st.park.dyn.road.rearendChargedPeriod, 0, '이번 주기(0)에 청구했다고 적어 둔다'); const heartsAfterFirst = P.hearts; P.st.pos[0].y = 2; E.parkStep(P, 'D'); assert.strictEqual(P.st.park.dyn.road.rearended, 2, '두 번째 REAREND 도 장부(dyn.rearended)엔 그대로 찍힌다 — "죽이지 않는다"는 하트 얘기지 ' + '장부를 죽이라는 뜻이 아니다'); assert.strictEqual(P.hearts, heartsAfterFirst, '같은 주기 안 두 번째부터는 하트가 안 깎인다 — 설계 법칙 1: 드럼은 하트를 쓴다, 죽이지 않는다'); }); test('Y58-ROAD-SCROLL-PURE: 밴드 위치는 dyn.scroll 의 순수 함수다', () => { const st = E._parkRoadBuild(E._parkRoadCell(5)); const rowsAt = (s) => { st.park.dyn.road.scroll = s; return Array.from({ length: st.N }, (_, y) => E._parkRoadBandAt(st, y)).join(''); }; assert.strictEqual(rowsAt(3), rowsAt(3), '같은 scroll 은 같은 밴드다'); assert.notStrictEqual(rowsAt(0), rowsAt(1), 'scroll 이 움직이면 밴드도 움직인다'); }); /* Y58-ROAD-MEDIAN-DASH / Y58-ROAD-LR-LEGAL 은 L3(2026-08-05)에서 폐기됐다. 둘 다 "분리대는 열이고 일부 행만 벽"을 쟀는데, L3 에서 분리대가 열이 아니게 됐다 (칸 사이 모서리로 옮겼다). 대체는 위의 Y58-ROAD-EDGE-DASH-SOLID(무늬가 시드·scroll 결정적)와 Y58-ROAD-SOLID-BINDS-BOTH(실선이 두 도메인을 막는다)다. 삭제만 하고 대체를 안 세우면 커버리지가 조용히 준다 — 그래서 여기 적어 둔다. */ test('Y58-ROAD-GATE-BEAT: 차단 박자에는 전진이 수 목록에서 사라진다 (드럼으로 덮는 것으로는 부족하다)', () => { let tested = 0; for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let i = 0; i < 30 && !P.over; i++) { if (E._parkRoadGateBeat(P.st)) { const keys = E._parkLegalKeys ? E._parkLegalKeys(P) : null; assert.ok(keys, '_parkLegalKeys 가 export 돼 있어야 이 게이트가 잰다'); // 이동 수 문자열은 'up'이 아니라 엔진 관용구 'U'다(_PARK_MOVES, engine.js:6983 — // Task 2 리포트가 이미 같은 typo 를 잡았다). 'up'으로 재면 그 문자열이 애초에 절대 // 안 나오는 값이라 마스크가 죽어도 이 assert 는 항상 통과한다 — 공허한 게이트가 된다. assert.ok(keys.indexOf('U') < 0, `seed ${seed}: 차단 박자에 'U' 가 합법 수에 있으면 G 가 그 박자에 말을 한다 — ` + `깊은 밭은 legal 이므로 드럼으로 덮는 것만으로는 못 막는다. legalMask 로 닫아야 한다`); tested++; break; } E.parkStep(P, 'stay'); } } assert.ok(tested >= 20, `차단 박자에 실제로 도달한 시드가 ${tested}/24 다 — 20 미만이면 주기가 너무 길거나 판이 먼저 끝난다`); }); test('Y58-ROAD-GATE-POCKET: 차단 박자에 포켓이 보고되면 워커와 동료 둘 다 한 수로 닿는다', () => { // L3(2026-08-05)에서 이 게이트의 첫 단언이 바뀌었다. 예전에는 "차단 박자에는 포켓이 // **반드시** 있어야 한다"였는데, L3 는 모서리 실선이 가로 대피를 막으면 _parkRoadPocket 이 // 정직하게 null 을 돌려주도록 만들었다(Y58-ROAD-POCKET-RESPECTS-MASK 가 그 정직성을 잰다). // 그러니 여기서 non-null 을 강요하면 "거짓 포켓을 돌려줘라"를 요구하는 게이트가 된다. // // 실측(이 커밋 시점, 시드 1..24 × 순수 stay 궤적의 첫 차단 박자): 포켓 있음 8 · null 16. // 그 8 이 설계로서 충분한가는 **이 게이트가 답하지 않는다** — 그 판정은 // Y58-ROAD-CN-DISJOINT 의 posed(하한 40)와 L3 완료 스윕이 진다. 여기서 하한을 실측값에 // 맞춰 내리면 초록을 사서 그 판정을 가리는 것이 된다. 그래서 이 게이트는 자기 몫만 지킨다: // **보고된 포켓은 거짓이 아니다**(둘 다 한 수로 닿는다), 그리고 표본이 0 이 아니다. let tested = 0, hadPocket = 0; for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let i = 0; i < 30 && !P.over; i++) { if (E._parkRoadGateBeat(P.st)) { const pocket = E._parkRoadPocket(P.st); if (pocket) { hadPocket++; const md = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); assert.ok(md(P.st.pos[0], pocket) <= 1, `seed ${seed}: 워커가 한 수로 포켓에 닿아야 다툴 일이 생긴다`); assert.ok(md(P.st.pos[1], pocket) <= 1, `seed ${seed}: 동료도 한 수로 닿아야 한다 — 하나만 닿으면 C-N 이 아니다`); } tested++; break; } E.parkStep(P, 'stay'); } } assert.ok(tested >= 20, `차단 박자에 도달한 시드가 ${tested}/24`); assert.ok(hadPocket > 0, `24 시드 어디에서도 포켓이 안 섰다 — 실선이 대피를 전부 죽였다는 뜻이고, 그러면 이 ` + `게이트의 도달성 단언이 한 번도 안 돈다(공허). 실선 비율을 재검토할 신호다`); }); test('Y58-ROAD-POCKET-REAL: 다양한 수로 걸어도 포켓은 정직하다 — 벽이면 null, non-null 이면 반드시 둘 다 닿는다', () => { // Y58-ROAD-GATE-POCKET(위)은 순수 stay 궤적 하나만 잰다(브리프 원문 그대로 유지, 안 건드림). // 그 궤적은 build 가 바닥 행을 일부러 짝수(틈)로 설계해 뒀기 때문에 우연히 항상 유효한 // 포켓에 이른다. 그런데 다양한 수(L/R 로 차선을 바꾸거나 U 로 앞서가는 등)로 걸으면 워커가 // **홀수** 행에서 차단 박자를 맞을 수 있고, 그 행은 분리대가 벽이다 — 컨트롤러 실측(seed // 1..24 x 40틴, 144 차단-박자 표본)으로 27.8% 가 그런 "벽 포켓"이었다. Fix round 1 전에는 // _parkRoadPocket 이 그 벽 칸을 그대로 돌려줬다(아무도 못 들어가는데 포켓이 있다고 거짓 // 보고). 이 게이트는 그 결함을 다시 재발하지 않게 지킨다: 결정론적(Math.random 없음, seed· // turn 만의 함수)으로 "다양한" 수를 골라 걸으며 **모든** 차단 박자를 방문하고(첫 박자에서 // break 하지 않는다 — GATE-POCKET 과 달리), non-null 포켓마다 벽이 아니고 보드 안이고 // 워커·동료 둘 다 한 수 거리인지 확인한다. null 은 실패가 아니다 — 정직한 null 이 이 // 게이트가 지키려는 바로 그 결과다. let sampled = 0; for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let i = 0; i < 40 && !P.over; i++) { if (E._parkRoadGateBeat(P.st)) { sampled++; const pocket = E._parkRoadPocket(P.st); if (pocket !== null) { const n = P.st.N; assert.ok(pocket.x >= 0 && pocket.y >= 0 && pocket.x < n && pocket.y < n, `seed ${seed} turn ${i}: 포켓이 보드 밖이면 애초에 칸이 아니다`); assert.ok(!P.st.wall.has(pocket.y * n + pocket.x), `seed ${seed} turn ${i}: 포켓이 벽 칸이다 — 아무도 못 들어가니 다툼도 안 생긴다 (Fix round 1 이 잡은 결함의 재발)`); const md = (a, b) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y); assert.ok(md(P.st.pos[0], pocket) <= 1, `seed ${seed} turn ${i}: 워커가 한 수로 포켓에 못 닿는다`); assert.ok(md(P.st.pos[1], pocket) <= 1, `seed ${seed} turn ${i}: 동료가 한 수로 포켓에 못 닿는다`); } } // "다양한" 수: 매 턴 그 순간의 합법 목록에서 seed·turn 만의 결정론적 인덱스로 하나 // 고른다(순수 stay 반복이 아니다 — Math.random 도 아니다, C1 유지). const legal = E._parkLegalKeys(P); E.parkStep(P, legal[(seed * 13 + i * 7) % legal.length]); } } assert.ok(sampled >= 20, `차단 박자를 실제로 방문한 횟수가 ${sampled} 다 — 20 미만이면 이 게이트가 거의 아무것도 못 잰 것이다`); }); test('Y58-ROAD-GATE-FREE: 차단 박자에는 드리프트가 면제된다 — 그 박자에 G 는 잃을 것도 없다', () => { // 브리프 원안은 차단 박자까지 순수 'stay' 로 걷는다. 그런데 이 판(N=7, 스폰 행 midY=3, // 주기 6)에서는 순수 'stay' 로 3틱 만에 바닥 행(N-1=6)에 닿고 그 뒤로 영원히 못 내려간다 // (Task 2 의 REAREND 노트와 같은 값, 실측 재확인함: node 로 24 시드 전부 i=5(첫 차단 // 박자)에서 row===6===N-1). 첫 차단 박자(scroll=5, 6번째 틱)는 그보다 뒤라서, 그 시점엔 // 이미 바닥이 드리프트를 막고 있다 — 그러면 이 테스트의 면제 로직을 통째로 지워도(브리프 // 문구를 그대로 되돌려도) 바닥이 대신 막아 줘서 y 가 안 변하고, 게이트는 공허하게 통과한다 // (자체 점검으로 실측 확인 — 아래 커밋 메시지에 기록). 그래서 ↑/↓ 를 번갈아 걸어 바닥에도 // 천장에도 안 닿은 채로 차단 박자에 이르는 경로로 바꿨다: ↑(면제, 순 -1)와 ↓(면제 아님, // 순 +2)를 번갈아 5번 — 행은 3→2→4→3→5→4 로 흐르고 6(바닥)에도 0(천장)에도 안 닿는다. // 그 상태에서 차단 박자에 stay 하면, 면제가 살아 있을 때만 y 가 안 바뀐다(실측: 면제 없이는 // 4→5 로 밀린다). for (let seed = 1; seed <= 12; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let i = 0; i < 30 && !P.over; i++) { if (E._parkRoadGateBeat(P.st)) { const y0 = P.st.pos[0].y; E.parkStep(P, 'stay'); assert.strictEqual(P.st.pos[0].y, y0, `seed ${seed}: 차단 박자에 stay 하면 밀리지 않는다 — 밀리면 "밀리느냐 마느냐"가 ` + `G 의 결정으로 남아 C-N 이 만장일치를 잃는다`); break; } E.parkStep(P, i % 2 === 0 ? 'U' : 'D'); } } }); test('Y58-ROAD-CHAIN: 차량 셋의 앞칸에 토큰이 있고, 세 다리 순서로 정직하게 선언된다', () => { // Fix round 2 (2026-08-04, 컨트롤러 지시 — Task 6 STOP 진단 이후): Fix round 1 의 한 // 다리·세 대안(chain=[0], chainAnyOf=[[0,1,2]])은 실제 동작과는 맞았지만, 그 결과 // 체인이 turn 3 안쪽에 끝나 G 가 남은 87+ 턴 내내 냉(cold)해지고 GC·GN·CN 셋 다 0/144 // 였다(Task 6 리포트). 고침: 세 다리를 진짜로 세운다 — chain=[1,2,0], chainAnyOf 없음. // _parkChainStepDone(engine.js:7266)은 chainAnyOf[i] 가 없으면 그 다리 하나의 토큰 // 생사만 본다 — 겹치는 집합이 없으니 Fix round 1 이 잡았던 폭포(하나 죽으면 셋 다 끝)는 // 재발하지 않는다. 순서가 [0,1,2]가 아니라 [1,2,0]인 이유(동료 자신의 토큰을 맨 뒤로): // [0,1,2]로 먼저 재니 24×6=144 전부가 dest=0에서 영원히 멈췄다 — _parkFields(엔진 // 제너릭 fast-field BFS)는 동료의 몸(st.pos[1])을 장애물로 안 보는데 _parkLegal 의 // companion-occupancy 규칙은 그 칸으로의 이동을 막는다. 다리 0의 표적(gtype 0, 동료 // 바로 앞 칸)으로 가는 필드-최단 경로가 동료 자신이 서 있는 칸(그의 홈)을 지나면, // 그 칸으로 가는 수는 항상 불법이고 워커는 그 옆에 영구히 멈춘다. NPC 토큰은 이 문제가 // 없다(점유 규칙이 동료에게만 걸린다) — 그래서 동료 자신의 다리를 맨 뒤로 미룬다(자세한 // 실측은 engine.js 의 park.chain 주석과 Task 6 리포트 Fix round 2 절 참고). for (let seed = 1; seed <= 8; seed++) { const st = E._parkRoadBuild(E._parkRoadCell(seed)); // Task 6 이 넷째 토큰(gtype 3, 동료의 계약 표적, park.chain 밖)을 더한다 — 추월 대상 // 자체는 여전히 정확히 셋이다(바로 아래 park.chain 단언이 그걸 잰다). assert.strictEqual(st.tokens.length, 4, `seed ${seed}: 추월 대상 셋(동료 1 + NPC 2) + 동료의 계약 표적 하나`); assert.deepStrictEqual(st.park.chain, [1, 2, 0], '세 다리다 — 셋 다 추월해야 체인이 끝난다(순서는 chain 배열 순서, 죽은 순서와 무관하게 dest 는 그 순서대로 캐치업한다)'); assert.strictEqual(st.park.chainAnyOf, undefined, 'chainAnyOf 는 더 이상 안 쓴다 — 세 다리가 전부 필수라 대안 목록이 필요 없다'); // 토큰은 차량 위가 아니라 그 앞 칸에 있다 — 차량 위면 목적지가 진입 불가 셀이 되어 // 경로 메트릭이 Infinity 로 죽는다(route mask 의 Infinity 함정). const bodies = [st.pos[1], ...st.park.road.npc]; for (let i = 0; i < 3; i++) { assert.strictEqual(st.tokens[i].x, bodies[i].x, `token ${i} 는 제 차량과 같은 차선이다`); assert.strictEqual(st.tokens[i].y, bodies[i].y - 1, `token ${i} 는 제 차량 바로 앞이다`); assert.ok(!st.wall.has(st.tokens[i].y * st.N + st.tokens[i].x), `token ${i} 의 칸이 벽이면 그 목적지는 영원히 도달 불가다`); } } }); test('Y58-ROAD-OVERTAKE: 토큰 칸에 닿으면 dest 가 오르고, _parkRoadOnEnter 가 장부를 남긴다', () => { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(3))); assert.strictEqual(P.dest, 0); assert.strictEqual(P.st.park.dyn.road.passed.size, 0); // 토큰 하나를 워커 바로 앞에 옮겨 놓고 한 수로 닿는다 (게이트는 접합을 재지, 기하를 재지 // 않는다). 이동 수 문자열은 이 엔진의 관용구 'U'/'D'/'L'/'R'/'stay' 다(_PARK_MOVES, // engine.js:6983) — 브리프 원문의 'up' 은 이 엔진에 없는 키라 'U' 로 옮긴다. // Fix round 2 (2026-08-04): chain=[1,2,0] 이니 다리 0의 토큰은 gtype 1(NPC0)이다 — // gtype 0(동료 자신)이 아니다(위 park.chain 주석·Y58-ROAD-CHAIN 참고, 순서를 바꾼 // 이유가 이 게이트에도 그대로 적용된다). const t = P.st.tokens[1]; t.x = P.st.pos[0].x; t.y = P.st.pos[0].y - 1; const scoreBefore = P.st.score[0]; E.parkStep(P, 'U'); assert.strictEqual(t.alive, false, '추월한 앞차의 토큰은 죽는다'); assert.strictEqual(P.st.score[0], scoreBefore, 'pad:true 라 GENERIC gem 픽업이 아니라 reach-pad 픽업이 잡는다 — score 가 NaN 으로 새면 안 된다 ' + '(자체 점검으로 잡은 결함: 토큰에 pad/v 가 없으면 engine.js:8258 의 gem 픽업이 tok.v=undefined 를 더해 score 를 NaN 으로 오염시켰다)'); // 자체 점검으로 잡은 결함: t.alive 와 P.dest 만으로는 이 게이트가 _parkRoadOnEnter 를 전혀 // 재지 못한다 — 둘 다 pad:true 토큰이면 GENERIC 경로(engine.js:8277-8290)가 onEnter 보다 // 먼저 처리하기 때문이다. onEnter 가 유일하게 기여하는 것은 dyn.road.passed 장부와 fx다 — // 그래서 이 게이트는 그것도 함께 잰다(첫 구현은 onEnter 안에서 `!t.alive` 로 걸러 늘 이미 // 죽어 있던 토큰이라 아무것도 못 잡고 passed 가 영원히 비어 있었다 — RED 로 재확인됨, 아래 // 커밋 메시지 참고). assert.ok(P.st.park.dyn.road.passed.has(t.gtype), 'dyn.road.passed 에 추월한 토큰의 gtype 이 있어야 한다'); assert.ok(P.st.fx.some(f => f.k === 'overtake'), '추월 fx 가 있어야 한다'); // Fix round 2 (2026-08-04): chain 은 이제 세 다리([1,2,0], chainAnyOf 없음)다 — 다리 // 0 의 토큰(gtype 1)만 죽었으니 dest 는 1 이지, 완주(3)가 아니다. 아직 두 다리가 // 남아 있다는 것 자체가 이 고침의 핵심이다(G 가 계속 무언가를 원해야 GC·GN 이 산다). assert.strictEqual(P.dest, 1, '다리 0 만 끝났다 — 세 다리 중 하나'); assert.ok(P.dest < P.st.park.chain.length, '아직 완주가 아니다 — 남은 다리가 있어야 G 가 계속 산다'); // 나머지 두 다리를 죽은 순서와 무관하게 밟아도(다리 2=gtype 0을 먼저, 다리 1=gtype 2를 // 나중에) dest 는 chain 선언 순서대로 캐치업한다 — "추월 순서 비강제"라는 설계 취지가 // 살아 있는지를 잰다. const t0 = P.st.tokens[0]; t0.x = P.st.pos[0].x; t0.y = P.st.pos[0].y - 1; E.parkStep(P, 'U'); assert.strictEqual(t0.alive, false, '다리 2(gtype 0, 동료 자신)의 토큰도 죽는다(순서와 무관하게 추월은 된다)'); assert.strictEqual(P.dest, 1, '다리 1(gtype 2)이 아직 살아 있으니 dest 는 1 에 멈춰 있다 — 폭포가 아니다'); const t2 = P.st.tokens[2]; t2.x = P.st.pos[0].x; t2.y = P.st.pos[0].y - 1; E.parkStep(P, 'U'); assert.strictEqual(t2.alive, false, '마지막 다리(gtype 2)도 죽는다'); assert.strictEqual(P.dest, 3, '세 다리가 전부 죽었으니 dest 가 3(=chain.length)으로 캐치업한다'); }); test('Y58-ROAD-DEST-MOVES: 목적지는 차량을 따라 움직인다', () => { // Fix round 2 (2026-08-04): chain=[1,2,0] 이라 dest=0 은 chain[0]=gtype 1(NPC0 의 추월 // 토큰)을 겨눈다 — NPC 는 트리거 없이 매 틱 무조건 흐른다(_parkRoadTick 의 NPC 이동 // 루프, park.trig 와 무관) — 그래서 turn 1 부터 바로 결정론적으로 잰다. (동료 자신의 // 토큰(gtype 0)은 이제 다리 2, 맨 마지막이다 — trig=2 로 걸어 잠긴 동료가 실제로 // 움직일 때만 흐르므로 그 다리를 여기서 재면 우연에 좌우된다, 그래서 안 쓴다.) const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(5))); const before = E._parkDestCell(P); E.parkStep(P, 'stay'); const after = E._parkDestCell(P); assert.ok(before && after, '목적지가 있어야 한다'); assert.notDeepStrictEqual(before, after, '앞차가 흐르면 목적지도 흐른다'); }); test('Y58-ROAD-PREFER-NONEMPTY: prefer 는 어떤 상태에서도 빈 집합을 반환하지 않는다', () => { // 빈 집합은 거부가 아니라 그 마음의 침묵이고, 그러면 다음 마음이 무제한으로 지배한다. 인격 // 오라클로 걷는다(sampling blindness 회피 — 'stay' 반복은 편향된 상태 가족만 본다). const reads = E.PARK_FIELD_MECHS.road.reads; let checked = 0; for (let seed = 1; seed <= 24; seed++) { const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); for (let i = 0; i < 25 && !P.over; i++) { const legal = E._parkLegal(P), ctx = reads.ctx ? reads.ctx(P) : null; for (const att of ['C', 'N']) { if (!reads[att].engaged(P, ctx)) continue; const s = reads[att].prefer(P, legal, ctx); assert.ok(s instanceof Set, `${att}.prefer 는 Set 을 반환한다`); assert.ok(s.size > 0, `seed ${seed} turn ${i}: ${att}.prefer 가 빈 집합이다 — 이건 거부가 아니라 침묵이고, ` + `그 마음을 결정에서 통째로 지운다 (SEAM-INVARIANTS)`); checked++; } E.parkStep(P, E.parkOracleMove(P, E.PARK_PERSONAS[i % 6])); } } assert.ok(checked > 100, `engaged 상태를 ${checked} 번밖에 못 봤다 — 표본이 너무 적다`); }); test('Y58-ROAD-CN-DISJOINT: 차단 박자에 C 와 N 의 compliant set 이 서로소다 — 인격 오라클이 실제로 지나는 상태에서', () => { // 걸음을 결정론적 "다양한 수" 에서 **인격 오라클** 로 바꾼다(2026-08-04). 이유: 옛 걸음에서는 // 12/24 시드가 green 이었는데 오라클이 지나는 81 개 상태에서는 서로소가 0 이었다 — 게이트가 // 도달하지 않는 상태 가족을 인증하고 있었다. 바는 승격이 읽는 걸음 위에 서야 한다. const reads = E.PARK_FIELD_MECHS.road.reads; let posed = 0, disjoint = 0; for (let seed = 1; seed <= 24; seed++) { if (!E.PARK_FIELD_MECHS.road.admits(E._parkRoadCell(seed))) continue; for (const persona of E.PARK_PERSONAS) { const P0 = E.parkPlayout(E._parkRoadBuild(E._parkRoadCell(seed)), persona); const P = E.parkStart(E._parkRoadBuild(E._parkRoadCell(seed))); P._lean = true; for (const mv of P0.moves) { if (P.over) break; if (E._parkRoadGateBeat(P.st)) { const ctx = reads.ctx(P), legal = E._parkLegal(P); if (ctx.pocket && reads.C.engaged(P, ctx) && reads.N.engaged(P, ctx)) { posed++; const c = reads.C.prefer(P, legal, ctx), n = reads.N.prefer(P, legal, ctx); let ov = 0; for (const k of c) if (n.has(k)) ov++; if (!ov) disjoint++; } } E.parkStep(P, mv); } } } assert.ok(posed >= 40, `차단 박자에 두 마음이 다 깨어난 상태를 ${posed} 번 봤다 — 표본이 모자라다`); assert.strictEqual(disjoint, posed, `서로소가 ${disjoint}/${posed} 다. 포함관계면 여섯 순서가 같은 답을 내고 상이 0 이 된다`); }); // Y58-ROAD-PAIRS-DECLARED (Task 6 — 브리프 원문 Y58-ROAD-PAIRS-BACKED 에서 이름·바꿈; // 2026-08-04, 사용자 지시로 SHAPE 게이트로 완화): 브리프 원문은 // `assert.ok(expressedCN > 0, ...)` — 오라클 sweep 으로 잰 특정 수치를 직접 요구했다. // Fix round 1(체인을 한 다리·세 대안으로 선언)에서 GC·GN·CN 셋 다 0/144, Fix round 2 // (체인을 세 다리 순서로 되돌림, park.chain=[1,2,0])에서 GC 84/84·GN 0/84·CN 0/84 — // 두 라운드 다 이 파일에 특정 숫자를 pin 했었다. 사용자 지시: 세 쌍의 최종 측정(GC/GN/ // CN expressed, admitted 시드 수, faithful completion, C-N co-engagement 등)은 프로젝트 // 전체의 **단일 최종 측정 라운드**로 이월한다 — 이 커밋이 그 숫자를 재확정하지 않는다. // 그래서 이 게이트는 이제 **모양**만 잰다: pairs 선언 자체(리터럴이 아니라 구조), // parkPosedPairs 유니온, 그리고 admits 가 "시드 목록을 하드코딩한 게 아니라 셀 단위로 // 판단하는 런타임 술어"라는 것과 "전부 거부하는 죽은 함수는 아니다"라는 최소 비공허성. // 실제 expressed 수치·admitted 시드 개수·완주율·co-engagement 카운트는 전부 미측정으로 // 남긴다 — Task 6 리포트의 "구현 확정 / 측정 이월" 절이 그 목록이다. test('Y58-ROAD-PAIRS-DECLARED: pairs 선언의 모양 — C-N 하나만 선언하고, m4 서명과 합쳐 세 쌍이 되고, admits 는 하드코딩 목록이 아닌 술어다', () => { const m = E.PARK_FIELD_MECHS.road; assert.deepStrictEqual(m.pairs, [['C', 'N']], 'm4 서명이 G-C·G-N 을 이미 주므로 모듈은 C-N 하나만 선언한다(리터럴 배열 — 이후 실측이 갚는다)'); // m4 + road 는 세 쌍 전부를 세운다고 주장한다 — 그 유니온의 모양만 잰다. const posed = E.parkPosedPairs('m4', 'road').map(p => p.join('')).sort(); assert.deepStrictEqual(posed, ['CN', 'GC', 'GN'], 'm4 + road = 세 쌍'); // admits 는 함수(런타임 술어)여야 한다 — 시드별 참/거짓을 미리 박아 둔 표가 아니라 // 매 셀을 그 자리에서 판단해야 한다는 뜻이다. 그리고 24 시드 전부를 거부하면 이 // 선언은 애초에 아무것도 측정할 수 없는 죽은 모듈이다(최소 비공허성 — 구체적 개수는 // 안 잰다, 위 주석 참고). assert.strictEqual(typeof m.admits, 'function', 'admits 는 함수여야 한다 — 하드코딩된 시드 목록이면 새 지형에 대해 아무것도 판단 못 한다'); let admittedAny = false; for (let seed = 1; seed <= 24; seed++) { if (m.admits(E._parkRoadCell(seed))) { admittedAny = true; break; } } assert.ok(admittedAny, 'admits 가 시드 1..24 전부를 거부하면 이 선언은 측정 불가능한 죽은 모듈이다 (정확한 admitted 개수는 이 게이트가 안 잰다 — 프로젝트 최종 측정 라운드로 이월)'); }); test('Y58-ROAD-SHIP-PIN-DERIVED: 승격 핀은 리터럴이 아니라 술어 호출이다', () => { const src = require('fs').readFileSync(require('path').join(__dirname, 'engine.js'), 'utf8'); assert.ok(/const PARK_ROAD_SHIPPABLE = _parkRoadRecovers\(/.test(src), 'PARK_ROAD_SHIPPABLE 은 _parkRoadRecovers(...) 호출이어야 한다 — ' + '리터럴 true/false 는 측정이 아니라 단언이다 (derive-never-assert)'); }); /* ============ Y58 ROAD SCREEN — Task 7 화면 게이트 (2026-08-04) ============ * 이 세 게이트가 재는 것은 소스 규율이지, "시계가 실제로 화면에 있는가"가 아니다 — 그건 * 텍스트로 못 잰다(브리프 원문: 그런 모양의 게이트는 글자가 있기만 하면 통과한다, 그려지는지와 * 무관하게). 그 증거는 이 파일이 아니라 frame-legibility 류의 픽셀 델타 측정이 쥔다 * (task-7-report.md 참고, Δ 를 단위와 함께 적었다). engine.test.js 는 DOM 이 없다(C11) — * app.js 쪽은 항상 소스 텍스트로만 잰다(Y20-GUIDED-APP 의 선례 그대로). */ test('Y58-ROAD-ZERO-TEXT: 도로 페인터는 글자를 안 그린다 — fillText/strokeText 가 소스 어디에도 없다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _paintParkRoad(st, x0, y0, cell) {'); const end = src.indexOf('PARK_FIELD_RENDER.road = _paintParkRoad'); assert.ok(start > -1, '_paintParkRoad 가 app.js 에 있어야 한다'); assert.ok(end > start, 'PARK_FIELD_RENDER.road 등록이 페인터 뒤에 있어야 한다'); const paint = src.slice(start, end); assert.ok(!/fillText|strokeText/.test(paint), 'ZERO-TEXT: 도로 페인터는 모양만 그려야 한다 — 글자·숫자·문자 원시 명령 금지'); }); test('Y58-ROAD-SCREEN-REGISTERED: road 필드 메커닉이 렌더 심 클라이언트와 시계 선언을 둘 다 갖는다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); assert.ok(/PARK_FIELD_RENDER\.road\s*=\s*_paintParkRoad/.test(src), 'PARK_FIELD_RENDER.road 가 _paintParkRoad 를 등록해야 한다(다른 모든 필드 메커닉과 같은 심)'); assert.ok(/PARK_FIELD_CLOCK\.road\s*=\s*_clockOwn\(/.test(src), 'PARK_FIELD_CLOCK.road 는 own:true 시계를 선언해야 한다 — 판 전체 주기라 걸어 둘 엔티티가 ' + '없고, 공유 HUD 판-시계 줄은 2026-08-03 에 제거됐다(y58 과 무관한 동시 작업)'); }); test('Y58-ROAD-POCKET-NULL-SAFE: 안전 포켓 읽기는 .x/.y 를 건드리기 전에 진위 검사로 감싸져 있다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _paintParkRoad(st, x0, y0, cell) {'); const end = src.indexOf('PARK_FIELD_RENDER.road = _paintParkRoad'); const paint = src.slice(start, end); const call = paint.indexOf('E._parkRoadPocket(st)'); assert.ok(call > -1, '_paintParkRoad 는 브리프가 내보낸 안전-포켓 술어 E._parkRoadPocket(st) 를 읽어야 한다'); const after = paint.slice(call, call + 200); assert.ok(/if\s*\(\s*pocket\s*\)/.test(after), '포켓 읽기는 쓰기 전에 널 가드가 있어야 한다 — _parkRoadPocket 은 차단 박자의 41.7%에서 ' + 'null 이다(Task 3 실측, 시드 1..24×40턴 표본) — 가드 없이 짜면 그 자리에서 죽는다'); }); test('Y58-ROAD-SIGNAL-THREE-LAMP: 도로 시계는 3구 신호등이다 — 자유/예고/차단 세 상태가 소스에 있다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _paintParkRoad(st, x0, y0, cell) {'); const end = src.indexOf('PARK_FIELD_RENDER.road = _paintParkRoad'); const paint = src.slice(start, end); assert.ok(!/_parkPipRow\(/.test(paint), '6점 핍 줄은 3구 램프로 교체돼야 한다 — 핍은 "몇 박자 남았나"만 말하고 ' + '"지금 나한테 무슨 일이 생기나"를 안 말한다'); for (const hue of ['PARK_ROAD_SIGNAL_GO', 'PARK_ROAD_SIGNAL_WARN', 'PARK_ROAD_SIGNAL_STOP']) { assert.ok(paint.indexOf(hue) > -1, `신호등은 ${hue} 를 써야 한다 — 세 상태가 서로 다른 색이어야 읽힌다`); } assert.ok(/E\._parkRoadGateBeat\(st\)/.test(paint), '빨강은 엔진의 차단 박자 술어에서 나와야 한다 — 화면이 시간표를 다시 계산하면 물리와 갈린다'); }); test('Y58-ROAD-DRUM-SHARES-SIGNAL-HUE: 드럼 밴드와 신호등 빨강은 같은 상수다 — 한 어휘', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const m = src.match(/const PARK_ROAD_SIGNAL_STOP\s*=\s*'(#[0-9a-fA-F]{6})'/); const d = src.match(/const PARK_ROAD_DRUM_HUE\s*=\s*'(#[0-9a-fA-F]{6})'/); assert.ok(m, 'PARK_ROAD_SIGNAL_STOP 이 선언돼야 한다'); assert.ok(d, 'PARK_ROAD_DRUM_HUE 가 선언돼야 한다'); assert.strictEqual(m[1], d[1], '차단 신호와 드럼은 같은 빨강이어야 한다 — 다르면 사용자가 "빨간 블록이 뭔지 모르겠다"고 한 ' + '그 상태가 그대로 남는다'); const start = src.indexOf('function _paintParkRoad(st, x0, y0, cell) {'); const end = src.indexOf('PARK_FIELD_RENDER.road = _paintParkRoad'); const paint = src.slice(start, end); assert.ok(/gateBeatPulse/.test(paint), '차단 박자에 드럼이 신호등과 같은 박자로 맥동해야 한다 — 두 마크를 잇는 유일한 동적 신호다'); }); test('Y58-ROAD-NPC-IS-AGENT: 교통 NPC 는 차체가 아니라 원형 에이전트로 그려진다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _paintParkRoad(st, x0, y0, cell) {'); const end = src.indexOf('PARK_FIELD_RENDER.road = _paintParkRoad'); const paint = src.slice(start, end); const mark = paint.indexOf('road.npc'); assert.ok(mark > -1, '마크⑦ 이 road.npc 를 읽어야 한다'); const seg = paint.slice(mark, mark + 1400); assert.ok(!/roundRect\(cx - w \/ 2/.test(seg), '모난 차체(roundRect)는 원형 에이전트로 교체돼야 한다'); assert.ok(!/앞유리|전조등/.test(seg), '차 부품(앞유리·전조등)은 남아 있으면 안 된다 — 에이전트지 자동차가 아니다'); assert.ok(/bx\.arc\(/.test(seg), '에이전트 몸은 원(arc)이어야 한다'); assert.ok(seg.indexOf('PARK_ROAD_VEHICLE_HUE') > -1, '색은 기존 상수를 그대로 쓴다 — 워커 파랑·동료 자홍과 안 겹치도록 이미 고른 값이다'); }); test('Y58-ROAD-MEDIAN-SOLID-LINE: 못 건너는 행은 실선으로 그려진다 — 노란 바리케이드가 아니라', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _paintParkRoad(st, x0, y0, cell) {'); const end = src.indexOf('PARK_FIELD_RENDER.road = _paintParkRoad'); const paint = src.slice(start, end); assert.ok(paint.indexOf('#ffcf5c') === -1, '노란 빗금 바(#ffcf5c)는 흰 실선으로 교체돼야 한다 — 도로 페인트는 노랑 바리케이드가 아니다'); assert.ok(/PARK_ROAD_LANE_HUE/.test(paint), '실선도 파선과 같은 도로 페인트 색(PARK_ROAD_LANE_HUE)이어야 한다 — 같은 선의 두 상태다'); assert.ok(/E\._parkRoadSolidEdge\(/.test(paint), '어느 모서리가 실선인지는 엔진의 _parkRoadSolidEdge 에서 나와야 한다 — 화면이 규칙을 ' + '다시 만들면 안 된다. L3(2026-08-05)에서 분리선이 열의 벽 행에서 칸 사이 모서리로 옮겨져, ' + '이 단언의 증인도 st.wall 에서 그 술어로 바뀌었다'); }); test('Y58-ROAD-ARCH-DECOR: 도로 셀은 arch 를 선언하고, 벽 장식이 road 분기를 갖는다 (갓길에 공원 벤치 금지)', () => { const cell = E._parkRoadCell(3); assert.strictEqual(cell.arch, 'road', '_parkRoadCell 은 arch:"road" 를 선언해야 한다 — 없으면 _parkWallDecor 가 마지막 else(공원 ' + '나무·벤치)로 떨어져 도로 갓길에 벤치가 선다'); const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _parkWallDecor('); assert.ok(start > -1, '_parkWallDecor 가 app.js 에 있어야 한다'); const fn = src.slice(start, start + 3500); assert.ok(/arch === 'road'/.test(fn), "_parkWallDecor 는 arch === 'road' 분기를 가져야 한다"); }); test('Y58-ROAD-ARCH-IS-NOT-PHYSICS: arch 추가가 보드 기하를 안 바꾼다', () => { // arch 는 렌더 전용 필드다. 벽 집합·스폰·토큰·NPC 가 전부 바이트로 같아야 한다. for (let seed = 1; seed <= 24; seed++) { const st = E._parkRoadBuild(E._parkRoadCell(seed)); const shape = JSON.stringify({ wall: [...st.wall].sort((a, b) => a - b), pos: st.pos, npc: st.park.road.npc, tok: st.tokens.map(t => [t.x, t.y, t.gtype]), }); // 같은 시드를 두 번 지어도 같아야 한다(C1) — arch 가 rng 를 소비하지 않았음을 함께 잰다. const st2 = E._parkRoadBuild(E._parkRoadCell(seed)); const shape2 = JSON.stringify({ wall: [...st2.wall].sort((a, b) => a - b), pos: st2.pos, npc: st2.park.road.npc, tok: st2.tokens.map(t => [t.x, t.y, t.gtype]), }); assert.strictEqual(shape, shape2, `seed ${seed}: arch 추가가 rng 를 소비하면 안 된다`); } }); test('PARK-ROAD-BLINKER-HONEST: 깜빡이는 계획이 막힌 박자에 안 뜬다 — 거짓 예고 금지', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _paintParkRoadBlinker('); assert.ok(start > -1, '_paintParkRoadBlinker 가 app.js 에 있어야 한다'); const end = src.indexOf('PARK_FIELD_RENDER_POST.road', start); assert.ok(end > start, 'PARK_FIELD_RENDER_POST.road 등록이 그 함수 뒤에 있어야 한다'); const fn = src.slice(start, end); const call = fn.indexOf('_parkCompanionPlan('); assert.ok(call > -1, '블링커는 동료의 계획을 엔진에서 읽어야 한다 — 화면이 다시 계산하면 안 된다'); const after = fn.slice(call, call + 400); assert.ok(/\.stuck/.test(after), 'stuck 을 검사해야 한다 — _parkCompanionPlan 은 arrived 와 stuck 을 둘 다 next:null 로 ' + '돌려주므로, 구분 못 하면 "막혔다"를 "간다"로 그리는 거짓 예고가 된다'); assert.ok(/if\s*\(\s*!?\s*plan/.test(after) || /plan\s*&&/.test(after), 'plan 이 null 일 수 있으므로 널 가드가 있어야 한다 (_parkRoadPocket 과 같은 규율)'); }); test('PARK-ROAD-BLINKER-ZERO-TEXT: 블링커도 글자를 안 그린다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _paintParkRoadBlinker('); const end = src.indexOf('PARK_FIELD_RENDER_POST.road', start); assert.ok(start > -1 && end > start, '블링커 함수와 그 등록이 있어야 한다'); assert.ok(!/fillText|strokeText/.test(src.slice(start, end)), 'ZERO-TEXT: 블링커도 모양만 그려야 한다 — 이 함수는 _paintParkRoad 슬라이스 밖이라 ' + 'Y58-ROAD-ZERO-TEXT 가 안 덮는다. 이 테스트가 그 공백을 메운다'); }); /* ---- y58 화면 가독성 셋 (사용자 결정 2026-08-06) -------------------------------------- 세 마크 전부 소스 게이트다. 캔버스는 엔진이 못 만지므로(C11) 이 파일은 픽셀을 볼 수 없고, 실측은 Playwright 프로브가 이미 했다 — 초록 칠은 legal 칸 누락 0, 갓길 소품은 scroll 모델 기대집합 대비 누락 0(정적 모델로 대조하면 누락 28, 즉 바에 이빨이 있다). 여기 세 게이트가 지키는 것은 그 실측이 서 있던 **배선**이다: 배선이 지워지면 프로브를 다시 돌릴 사람이 없다. */ test('Y58-ROAD-FACING-LOCKED: 도로판에서는 모든 몸이 위를 본다 — 시선이 상태를 안 나른다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const fstart = src.indexOf('function _parkFaceOf('); assert.ok(fstart > -1, '_parkFaceOf 가 있어야 한다 — 시선 고정은 _parkActor 안이 아니라 ' + '호출부 앞단에 산다(그 글리프는 공원의 모든 몸이 쓰므로, 안에서 죽이면 y46 감시자와 ' + 'y50 황소의 시선까지 같이 죽는다)'); const fn = src.slice(fstart, fstart + 300); assert.ok(/st\.park\.road/.test(fn) && /dy:\s*-1/.test(fn), '도로판일 때만 위(dy:-1)로 고정해야 한다 — 다른 판의 시선은 그 판의 관계를 나른다'); assert.ok(/return\s+f;/.test(fn), '도로판이 아니면 인자를 그대로 돌려줘야 한다 (바이트 안정성)'); // 두 주인공이 그 함수를 통해 시선을 읽는가. for (const [name, anchor] of [['drawParkAgent', 'st.facing[0]'], ['drawParkCompanion', 'st.facing[1]']]) { const s = src.indexOf('function ' + name + '('); assert.ok(s > -1, name + ' 가 있어야 한다'); const body = src.slice(s, s + 900); assert.ok(body.includes('_parkFaceOf(st, ' + anchor + ')'), name + ' 는 _parkFaceOf 를 통해 시선을 읽어야 한다 — 날 st.facing 을 직접 쓰면 고정이 샌다'); } // 교통 NPC 와 xx 액터의 눈도 위다: 눈 오프셋이 음수 y 여야 한다. const rstart = src.indexOf('function _paintParkRoad(st, x0, y0, cell) {'); const rend = src.indexOf('PARK_FIELD_RENDER.road = _paintParkRoad', rstart); const paint = src.slice(rstart, rend); assert.ok(rstart > -1 && rend > rstart, '_paintParkRoad 와 그 등록이 있어야 한다'); assert.ok(/cy\s*-\s*0\.22\s*\*\s*r/.test(paint), '교통 NPC 의 눈은 위(-0.22r)를 봐야 한다'); assert.ok(/cy\s*-\s*0\.20\s*\*\s*r/.test(paint), 'xx 액터의 눈도 위(-0.20r)를 봐야 한다'); assert.ok(!/cy\s*\+\s*0\.2[02]\s*\*\s*r/.test(paint), '아래를 보는 옛 눈 오프셋이 남아 있으면 안 된다 — 한 판에 두 시선이 서면 그 차이가 ' + '뜻이 있는 것처럼 읽힌다'); }); test('Y58-ROAD-MOVES-GREEN: 갈 수 있는 칸이 매 프레임 엔진에서 다시 읽혀 칠해진다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _paintParkRoadMoves('); assert.ok(start > -1, '_paintParkRoadMoves 가 있어야 한다'); const end = src.indexOf('PARK_FIELD_RENDER_POST.road', start); assert.ok(end > start, '이 페인터는 POST 심 등록보다 앞에 있어야 한다'); const fn = src.slice(start, src.indexOf('function _paintParkRoadPost(', start)); assert.ok(/E\._parkLegal\(P\)/.test(fn), '화면은 legal 집합을 엔진에서 읽어야 한다 — 다시 계산하면 두 진실이 갈린다(블링커와 같은 규율)'); assert.ok(/P\.over/.test(fn), 'P.over 가드가 있어야 한다 — 끝난 판에 갈 곳을 그리면 거짓말이다'); assert.ok(/PARK_ROAD_SIGNAL_GO/.test(fn), '초록은 신호등 GO 램프와 같은 상수여야 한다 — 드럼-빨강이 신호-빨강과 같은 값인 것과 같은 이유(한 어휘)'); assert.ok(/m\.x === from\.x && m\.y === from\.y/.test(fn), "'stay' 는 칠하지 않아야 한다 — 제자리는 이동 방향이 아니고, 워커 몸 밑을 칠하면 대비만 깎인다"); assert.ok(!/fillText|strokeText/.test(fn), 'ZERO-TEXT'); // 매 턴 갱신은 전용 카운터가 없다는 것으로 지킨다: 이 함수는 인자로 받은 st/P 만 읽는다. assert.ok(!/setTimeout|requestAnimationFrame|Date\.now/.test(fn), '자체 타이머를 들면 판이 끝날 때까지 매 턴 갱신된다는 성질이 그 타이머에 의존하게 된다'); // POST 심에는 id 당 painter 가 하나뿐이다 — 두 마크가 디스패처를 통해 순서대로 서야 한다. const post = src.slice(src.indexOf('function _paintParkRoadPost('), end + 120); assert.ok(post.indexOf('_paintParkRoadMoves(') < post.indexOf('_paintParkRoadBlinker('), '초록 칠이 깜빡이보다 먼저 그려져야 한다 — 순서가 곧 층이고, 초록이 호박색 화살표를 덮으면 ' + '남의 수를 읽는 채널이 죽는다'); assert.ok(/PARK_FIELD_RENDER_POST\.road\s*=\s*_paintParkRoadPost/.test(src), 'POST 심의 road 클라이언트는 그 디스패처여야 한다'); }); test('Y58-ROAD-DECOR-SCROLLS: 갓길 소품은 흐르고, 다른 판의 소품은 못박혀 있다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const start = src.indexOf('function _parkDecorKey('); assert.ok(start > -1, '_parkDecorKey 가 있어야 한다'); const fn = src.slice(start, start + 500); assert.ok(/dyn\.scroll/.test(fn), '소품의 원본 행은 PUBLIC dyn.road.scroll 을 읽어 당겨야 한다 (C1)'); assert.ok(/return y \* n \+ x;/.test(fn), '도로판이 아니면 그 칸 자신이어야 한다 — 다른 서른 판과 허브 썸네일의 소품은 바이트 동일해야 한다'); assert.ok(/\(\(\(y - \(dyn\.scroll \| 0\)\) % n\) \+ n\) % n/.test(fn), '원본 행은 (y - scroll) 을 n 으로 wrap 해야 한다 — 판 위로 사라지지 않고 되돌아온다'); assert.ok(src.includes('_parkWallDecor(arch, park, _parkDecorKey(st, x, y, n)'), '벽 장식 호출이 _parkDecorKey 를 통과해야 한다 — 안 그러면 함수만 있고 아무 것도 안 흐른다'); // 기하는 안 움직인다: 스크롤은 그림에만 걸리고, 어느 칸이 벽인가는 빌드 때 굳는다. const call = src.indexOf('_parkWallDecor(arch, park, _parkDecorKey('); const line = src.slice(src.lastIndexOf('\n', call), src.indexOf('\n', call)); assert.ok(/st\.wall\.has\(kk\)/.test(line), '장식은 여전히 그 칸이 벽일 때만 그려야 한다 — 소품이 흐른다고 벽이 흐르면 기하가 움직인다'); }); /* ============ Y58 THE FLOWING ROAD — 크로싱 층 착석 (Task 8, 2026-08-04) ============ * 두 게이트: si 40 을 핀으로 박고, y58 을 앉혀도 크로싱 fallback 이 늘지 않는다는 것을 델타로 * 잰다. ship 은 false 다 — 바 실측은 이 프로젝트의 마지막 라운드(Task 9)로 미룬다. */ test('Y58-SLOT: y58 이 si 40 으로 앉고, 기존 슬롯의 si 는 한 칸도 안 움직인다', () => { const slots = CAMP.PARK_CROSSINGS; const y58 = slots.find(s => s.id === 'y58'); assert.ok(y58, 'y58 슬롯이 있어야 한다'); assert.strictEqual(y58.si, 40, 'si 는 append 다 — 재번호하면 라이브 셀이 전부 리롤된다'); assert.strictEqual(y58.kind, 'm4'); assert.strictEqual(y58.playMech.fieldMech, 'road'); assert.strictEqual(y58.demoMech.moveMech, 'push', '데모는 소코반 — 장르 최대 이격'); // si 는 유일하다. const sis = slots.map(s => s.si); assert.strictEqual(new Set(sis).size, sis.length, 'si 중복 금지'); }); test('Y58-SLOT-NO-FALLBACK: y58 을 앉혀도 크로싱 fallback 이 늘지 않는다 (y58 자신의 실제 피커 경로, 델타)', () => { // parkCrossings(seed) 를 통째로 돌리면 y46 의 기존(이 태스크와 무관한) EXHAUSTED 상태가 // 델타를 오염시킨다 — 그래서 y58 자신의 데모/플레이 셀만, parkCrossings 가 쓰는 것과 동일한 // 시드 유도식으로 직접 호출한다. _parkCrossBoard(kind, cell) 는 cell.mech 만 보고 kind // 문자열 자체는 무시하므로, 원시 시드 정수를 cell 로 넘기면(브리프 초안이 그랬다) 아무 것도 // 검사하지 못한 채 델타는 무조건 0 이 되고, 심지어 kind='y58' 은 legacy walk 분기로 떨어져 // 그 자리에서 던진다(실측 확인). 그 공허함을 피하려고 실제 피커가 쓰는 두 빌더 // (_parkCrossingDemoCell/_parkCrossingPlayCell) 를 그대로 쓴다. const cx = CAMP.PARK_CROSSINGS.find(s => s.id === 'y58'); assert.ok(cx, 'y58 슬롯이 있어야 한다'); const before = CAMP.parkCrossFallbacks(); let ran = 0; for (let s = 1; s <= 8; s++) { const dSeed = (s * 61 + 11 + cx.si * 197) >>> 0; // parkCrossings 의 유도식 그대로 const pSeed = (s * 89 + 23 + cx.si * 211) >>> 0; const demoCell = CAMP._parkCrossingDemoCell(cx.kind, cx.demoMech, dSeed, cx.demoHaz); const play = CAMP._parkCrossingPlayCell(cx.kind, demoCell, cx.playMech, pSeed, cx.playHaz, undefined, cx.ship); assert.ok(demoCell && play && play.cell, `seed ${s}: 보드가 안 서졌다`); ran++; } // NON-VACUITY: 루프가 실제로 8 번 다 돌았는지 직접 확인한다 — 0 번 돈 루프의 델타 0 은 // 아무것도 증명하지 못한다. assert.strictEqual(ran, 8, '루프가 실제로 8 번 다 돌지 않았다 — 델타 0 은 공허하다'); const after = CAMP.parkCrossFallbacks(); assert.strictEqual(after - before, 0, 'fallback 델타가 0 이 아니면 필터가 후보를 못 찾고 있다'); }); test('PLAZA-LAYOUT: seed-pure board, three signalled doorways, the 6-beat pin, anchors reachable', () => { for (const seed of [1, 7, 23]) { const a = E._parkPlazaBuild(E._parkPlazaCell(seed)); const b = E._parkPlazaBuild(E._parkPlazaCell(seed)); assert.equal(JSON.stringify(a.park.plaza), JSON.stringify(b.park.plaza)); // C1 determinism const n = a.N, Z = a.park.plaza; // three doorway gaps in the belt wall, each with its own signal cell on walkway assert.equal(Z.doorKeys.length, 3); assert.equal(Z.sigKeys.length, 3); for (const k of Z.doorKeys) assert.ok(!a.wall.has(k), 'doorway is open ground'); for (const k of Z.sigKeys) assert.ok(!a.wall.has(k), 'signal stands on walkway'); // THE 6-BEAT PIN: farthest two signals are 6 cursor steps apart (walls-only BFS) // bfsW is parameterised by the blocking set W; `bfs` and `bfs2` below bind it to the real // wall and to the all-doorways-shut wall. Same distances, one copy of the search. const bfsW = (src, W) => { const d = new Array(n * n).fill(Infinity); d[src] = 0; const q = [src]; for (let h = 0; h < q.length; h++) { const kk = q[h], x = kk % n, y = (kk / n) | 0; for (const dd of [{x:0,y:-1},{x:1,y:0},{x:0,y:1},{x:-1,y:0}]) { const nx = x + dd.x, ny = y + dd.y; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (W.has(nk) || d[nk] <= d[kk] + 1) continue; d[nk] = d[kk] + 1; q.push(nk); } } return d; }; const bfs = (src) => bfsW(src, a.wall); let far = 0; for (let i = 0; i < 3; i++) { const d = bfs(Z.sigKeys[i]); for (let j = 0; j < 3; j++) far = Math.max(far, d[Z.sigKeys[j]]); } assert.equal(far, 6, 'farthest signal pair is exactly 6 cursor beats'); // both exits reachable from both entrances (all signals green = belt gaps open) const dN = bfs(Z.entrN), dS = bfs(Z.entrS); for (const ex of [Z.exitR, Z.exitB]) { assert.ok(isFinite(dN[ex])); assert.ok(isFinite(dS[ex])); } // the belt separates: with ALL doorways walled shut, N cannot reach the south-half exit const w2 = new Set(a.wall); for (const k of Z.doorKeys) w2.add(k); const bfs2 = (src) => bfsW(src, w2); assert.ok(!isFinite(bfs2(Z.entrN)[Z.exitB]), 'doorways are the ONLY belt passages'); } }); test('PLAZA-TIMETABLE: 10 residents, 4-beat gaps, colour totals, round-robin entrances, seed variation', () => { const counts = new Set(); for (const seed of [1, 2, 3, 7, 23]) { const st = E._parkPlazaBuild(E._parkPlazaCell(seed)); const S = st.park.dyn.plaza.sched; assert.equal(S.length, 10); for (let i = 0; i < 10; i++) { assert.equal(S[i].beat, 2 + i * 4, 'spawn beats are 2,6,..,38'); assert.ok(S[i].color === 'R' || S[i].color === 'B'); assert.ok(S[i].entr === 'N' || S[i].entr === 'S'); if (i > 0) assert.notEqual(S[i].entr, S[i - 1].entr, 'entrances round-robin'); } const r = S.filter(e => e.color === 'R').length; assert.ok(r >= 3 && r <= 7, 'colour totals stay mixed after seed flips'); counts.add(S.map(e => e.color + e.entr).join('')); } assert.ok(counts.size >= 3, 'seeds vary the timetable'); }); // PLAZA-SEED-PURE는 계획이 축자 지정한 게이트가 아니다 — 리뷰에서 드러난 이빨 구멍을 메우려고 // 더한 것이다. PLAZA-LAYOUT의 `JSON.stringify(a.park.plaza)` 비교는 **공허하다**: park.plaza는 // n·beltY·gapX와 리터럴 좌표로만 만들어져 rng가 한 방울도 안 섞이므로, sched·wall·tokens·spawn이 // 매 호출 randomize돼도 그 단언은 통과한다(서로 다른 두 시드조차 바이트 동일한 park.plaza를 낸다). // 여기서 진짜 시드 순수성을 잰다. 형제 Y58-ROAD-SEED-PURE의 모양. test('PLAZA-SEED-PURE: build 는 seed 만의 함수다 (C1) — 그리고 seed 는 편성표를 실제로 흔든다', () => { // 서명에 sched·wall·tokens·spawn을 전부 넣는다 — park.plaza만 보는 낡은 비교가 놓치던 것들. const sig = (st) => JSON.stringify({ N: st.N, wall: [...st.wall].sort((x, y) => x - y), walkway: [...st.park.walkway].sort((x, y) => x - y), plaza: st.park.plaza, sched: st.park.dyn.plaza.sched, remainR: st.park.dyn.plaza.remainR, remainB: st.park.dyn.plaza.remainB, signals: st.park.dyn.plaza.signals, tokens: st.tokens, chain: st.park.chain, pos: st.pos, spawn: st.park.spawn, }); // (1) 같은 시드는 두 번 불러도 같은 보드다 — 시드 1..64 전수. const seen = new Map(); for (let seed = 1; seed <= 64; seed++) { const a = sig(E._parkPlazaBuild(E._parkPlazaCell(seed))); const b = sig(E._parkPlazaBuild(E._parkPlazaCell(seed))); assert.strictEqual(a, b, `seed ${seed}: 같은 cell 은 같은 보드를 낳는다`); seen.set(seed, a); } // (2) 그리고 시드가 실제로 편성표를 흔든다. 기하는 상수라 서명 차이는 오직 sched에서 온다. // 54 는 실측값이다(시드 1..64, rng salt = seed*7433+2897, 소비 4회: 시작색·시작입구·조별 균형교환 둘). // 시드 소비 순서나 salt 가 바뀌면 이 수가 움직인다 — 그게 이 핀의 존재 이유다. // (Task 4b 갱신: 51 -> 54. 색이 입구 패리티에서 풀려 주기 4 가 되고 뒤집기 둘이 조별 균형교환 // 둘로 바뀌면서 편성표 공간이 112 종 -> 144 종으로 넓어졌다. 소비 횟수는 그대로 넷이다. // **핀은 갱신하는 것이 옳은 대응이고 제거는 금지다** — 이 게이트의 원저자 규약 그대로.) assert.strictEqual(new Set(seen.values()).size, 54, '시드 1..64 가 낳는 서로 다른 보드는 정확히 54 종 (편성표 변주의 회귀 핀)'); const tt = new Set(); for (let seed = 1; seed <= 64; seed++) tt.add(E._parkPlazaBuild(E._parkPlazaCell(seed)).park.dyn.plaza.sched.map(e => e.color + e.entr).join('')); assert.strictEqual(tt.size, 54, '시드 1..64 의 서로 다른 편성표도 정확히 54 종'); // (2a) Task 4b 가 더한 구조 핀 — **이 게이트가 지키는 것은 이제 "변주"만이 아니다.** // 스펙 §3 확정 문구("절반의 교통이 벨트를 건넌다")는 시드마다의 기대값이 아니라 매 판의 // 사실이어야 한다. v1 은 색·입구를 같은 패리티에 묶어 한 시드 비트로 횡단량을 정해 버렸고 // (횡단 32/100, 시드 8/10 이 1~2명), falsifier 의 double-bind 가 그 자리에서 죽었다. // 이 단언이 그 회귀를 잡는다: 어떤 시드에서도 정확히 절반이 벨트를 건넌다. for (let seed = 1; seed <= 200; seed++) { const S = E._parkPlazaBuild(E._parkPlazaCell(seed)).park.dyn.plaza.sched; const crossers = S.filter(e => (e.entr === 'N' && e.color === 'B') || (e.entr === 'S' && e.color === 'R')).length; assert.strictEqual(crossers, E.PARK_PLAZA_COUNT / 2, `seed ${seed}: 벨트를 건너는 주민은 매 판 정확히 절반이다 (스펙 §3 대각 출구의 확정 함의)`); } // (2b) 골든 편성표. "51 종"은 **거친 통계다** — 실측해 보니 salt 2891 도 2897 도 똑같이 51 을 // 낸다. 그래서 개수 핀만으로는 salt 한 글자 변화를 못 잡는다(이 게이트의 첫 반증 시도가 초록으로 // 통과해서 알았다). 편성표 자체를 못 박아야 salt·시드 소비 순서·뒤집기 인덱스 범위 중 하나라도 // 움직이는 순간 붉어진다. 값은 실측이다(salt = seed*7433+2897). // (Task 4b 갱신: 색 주기가 2 -> 4 로, 뒤집기 둘이 조별 균형교환 둘로 바뀌어 세 골든이 전부 // 움직였다. 이것이 이 핀이 의도한 바로 그 반응이다 — 새 값도 실측이다.) const GOLDEN = { 1: 'BNBSBNBSBNRSRNRSRNBS', 7: 'RNRSRNBSBNBSBNRSRNRS', 23: 'RNBSRNRSBNBSBNBSBNRS' }; for (const s of Object.keys(GOLDEN)) { const got = E._parkPlazaBuild(E._parkPlazaCell(+s)).park.dyn.plaza.sched.map(e => e.color + e.entr).join(''); assert.strictEqual(got, GOLDEN[s], `seed ${s} 의 편성표는 골든이다 — 보드 생성이 조용히 바뀌면 여기가 먼저 붉어진다`); } // (3) C1 arity — persona 가 들어설 자리가 구조적으로 없다. assert.strictEqual(E._parkPlazaBuild.length, 1, '_parkPlazaBuild 는 cell 하나만 받는다 (C1)'); assert.strictEqual(E._parkPlazaCell.length, 1, '_parkPlazaCell 은 seed 하나만 받는다 (C1)'); // (4) 충돌 유발 필드 여섯. 이건 가설이 아니라 **실측된 회귀 경로**다 — Task 1 도중 두 번 // 연달아 밟았고(_parkFields engine.js:7593 이 park.deep/verge/walkway/distDeep 를, // _parkNCtx engine.js:7752 가 park.contracts.length 를 조건 없이 읽는다), 형제 road 는 // 지금도 그것으로 죽고 있다. const st = E._parkPlazaBuild(E._parkPlazaCell(1)); for (const f of ['walkway', 'verge', 'deep', 'water']) assert.ok(st.park[f] instanceof Set, `park.${f} 는 빈 Set 이라도 반드시 있어야 한다 (_parkFields 가 이름으로 읽는다)`); assert.ok(Array.isArray(st.park.distDeep) && st.park.distDeep.length === st.N * st.N, 'park.distDeep 는 n*n 배열이어야 한다'); assert.ok(Array.isArray(st.park.contracts), 'park.contracts 는 빈 배열이라도 반드시 있어야 한다 (_parkNCtx 가 .length 를 무조건 읽는다)'); // 지형 부재가 곧 커서가 deep 요금을 안 무는 이유다 — 구조로 못 박는다. assert.strictEqual(st.park.deep.size, 0, '광장에는 deep 이 한 칸도 없다'); assert.strictEqual(st.park.water.size, 0, '광장에는 water 가 한 칸도 없다'); assert.ok(st.park.walkway.size > 0, '광장은 걸을 수 있어야 한다'); // (5) 그리고 가장 값싼 방어: 실제로 한 판 돌려 본다. 여섯 필드를 개별로 세는 것보다, // "다음에 어떤 필드가 추가로 필요해져도" 이 한 줄이 먼저 잡는다. assert.doesNotThrow(() => E.parkPlayout(E._parkPlazaBuild(E._parkPlazaCell(1)), E.PARK_PERSONAS[0]), 'parkPlayout 이 던지면 park 에 엔진이 이름으로 읽는 필드가 빠진 것이다'); }); test('PLAZA-RESOLVE: queue physics, swap, rotation cycle, n-way — the fixpoint pins', () => { const st = E._parkPlazaBuild(E._parkPlazaCell(7)); const P = E.parkStart(st); const D = st.park.dyn, mk = (x, y, ex) => ({ x, y, color: 'R', exitKey: ex, stun: 0, pips: 3, spawnIdx: D.ents.length, done: false }); const n = st.N, Z = st.park.plaza; // (a) QUEUE, NOT A CRASH: A stays, B intends A's cell -> B stays too, zero hearts, zero crashes D.ents.push(mk(6, 3, Z.exitB)); D.ents.push(mk(6, 2, Z.exitB)); D.ents[0].stun = 1; // force A still this beat const h0 = P.hearts; E._parkPlazaResolve(P, [D.ents[0].y * n + D.ents[0].x, D.ents[0].y * n + D.ents[0].x]); // both "arrive at" A's cell assert.equal(P.hearts, h0); assert.equal(D.plaza.crashes.length, 0); assert.equal(D.ents[1].x, 6); assert.equal(D.ents[1].y, 2, 'follower held in queue'); // (b) SWAP: both keep their cells, one heart, both stunned D.ents.length = 0; D.ents.push(mk(4, 2, 99)); D.ents.push(mk(5, 2, 99)); E._parkPlazaResolve(P, [2 * n + 5, 2 * n + 4]); assert.equal(P.hearts, h0 - 1); assert.equal(D.plaza.crashes.length, 1); assert.ok(D.ents[0].x === 4 && D.ents[1].x === 5, 'swap: both keep cells'); assert.ok(D.ents[0].stun === E.PARK_PLAZA_STUN && D.ents[1].stun === E.PARK_PLAZA_STUN); // (c) ROTATION CYCLE (3-ring) resolves CONSERVATIVE: all stay, no hearts (spec §4-4 pin) D.ents.length = 0; D.plaza.crashes.length = 0; D.ents.push(mk(2, 2, 99)); D.ents.push(mk(3, 2, 99)); D.ents.push(mk(3, 3, 99)); const h1 = P.hearts; E._parkPlazaResolve(P, [2 * n + 3, 3 * n + 3, 2 * n + 2]); // 2,2->3,2 ; 3,2->3,3 ; 3,3->2,2 assert.equal(P.hearts, h1); assert.equal(D.plaza.crashes.length, 0); assert.ok(D.ents.every((e, i) => e.x === [2, 3, 3][i] && e.y === [2, 2, 3][i]), 'cycle: all stay'); // (d) N-WAY into one empty cell: ONE heart, lowest spawnIdx occupies, rest bounce D.ents.length = 0; D.ents.push(mk(5, 4, 99)); D.ents.push(mk(7, 4, 99)); D.ents.push(mk(6, 3, 99)); E._parkPlazaResolve(P, [4 * n + 6, 4 * n + 6, 4 * n + 6]); assert.equal(P.hearts, h1 - 1); assert.ok(D.ents[0].x === 6 && D.ents[0].y === 4, 'lowest spawnIdx occupies'); assert.ok(D.ents[1].x === 7 && D.ents[2].y === 3, 'others bounce to origin'); }); // PLAZA-STUNNED-BLOCKS 는 계획이 축자 지정한 게이트가 아니다 — 스펙 §3-5("상호 회피 없음")와 // §4-4 꼬리("기절 주민은 다음 박 BFS 의 장애물")의 문구 충돌을 컨트롤러가 **§4-4 승**으로 판정한 // 뒤, 그 판정에 이빨을 달려고 더한 것이다. 판정만 바꾸고 게이트를 안 달면 다음 사람이 조용히 // 되돌린다. PLAZA-RESOLVE(축자 게이트)는 건드리지 않는다. // // 이 게이트의 심장은 (2)다. "우회했다"만 재면 그게 **기하** 때문인지 **기절** 때문인지 못 가른다. // 같은 ent 의 stun 하나만 뒤집었을 때 intent 가 되돌아와야 단언이 기절 상태에 실제로 반응한다. test('PLAZA-STUNNED-BLOCKS: 쓰러진 몸은 다음 박 BFS 의 벽이다 — 깨어 있는 몸은 아니다 (스펙 §4-4 꼬리)', () => { const st = E._parkPlazaBuild(E._parkPlazaCell(5)); const n = st.N, Z = st.park.plaza, dz = st.park.dyn.plaza; const K = (x, y) => y * n + x; // 바깥 문간 둘을 닫아 가운데 문간을 **벨트를 건너는 유일한 통로**로 만든다. 좁은 통로여야 // "우회 가능"이라는 도피구가 사라지고 단언이 이분법이 된다. dz.signals = [false, true, false]; const doorMid = Z.doorKeys[1]; assert.equal(doorMid, K(6, 6), '가운데 문간은 (6,6)'); const mk = (x, y, stun) => ({ x, y, color: 'B', exitKey: Z.exitB, stun, pips: 3, spawnIdx: st.park.dyn.ents.length, done: false }); const blocker = mk(6, 6, 1); // 문간 위에 쓰러진 주민 const follower = mk(6, 5, 0); // 그 바로 뒤에서 남쪽 출구로 가려는 주민 st.park.dyn.ents.push(blocker, follower); // (1) 기절자는 벽이다 — 뒤 주민은 그 칸을 내지 않는다. 유일 통로가 막혔으니 무경로 -> 제자리. const shut = E._parkPlazaResidentIntent(st, follower); assert.notEqual(shut, doorMid, '기절해 쓰러진 몸이 깔고 있는 칸으로 다음 주민이 걸어 들어간다'); assert.equal(shut, K(6, 5), '유일 통로가 막혔으므로 무경로 -> 제자리 (스펙 §3-5)'); // (2) THE PIN: 같은 ent 의 stun 만 0 으로 바꾸면 intent 가 그 칸으로 **되돌아온다**. 기하는 // 한 글자도 안 바뀌었으므로 (1)의 우회는 기절 때문이지 벽 때문이 아니다. blocker.stun = 0; assert.equal(E._parkPlazaResidentIntent(st, follower), doorMid, '깨어난 몸은 BFS 에 보이지 않아야 한다 — 상호 회피 없음(스펙 §3-5)은 그대로다'); // (3) 퇴장한 몸도 막지 않는다: 기절로 되돌리되 done 을 켠다. blocker.stun = 1; blocker.done = true; assert.equal(E._parkPlazaResidentIntent(st, follower), doorMid, '배달돼 퇴장한 주민은 더 이상 몸이 아니다'); // (4) 자기 몸에는 갇히지 않는다 — 기절 중인 주민 자신의 의도는 제 칸이다. blocker.done = false; assert.equal(E._parkPlazaResidentIntent(st, blocker), doorMid, '기절 중인 주민은 제 칸을 낸다'); }); test('PLAZA-DELIVER: spawn delay on occupied entrance, delivery scores, completion, beat cap', () => { const st = E._parkPlazaBuild(E._parkPlazaCell(3)); const P = E.parkStart(st); const D = st.park.dyn; // drive real beats with the SHIPPED ORACLE — the cursor actually plays, the world runs. // (계획의 `sawDelay` 는 선언·대입만 되고 **한 번도 읽히지 않는 죽은 변수**였다. 아무것도 단언하지 // 않으므로 지워도 단언은 한 줄도 안 바뀐다 — Task 1 의 BFS 헬퍼 매개변수화와 같은 논리다. // 제목이 약속한 지연 경로의 진짜 커버리지는 아래 PLAZA-SPAWN-SLIP 이 진다.) // // ★ 왜 `'stay'`(유휴 커서)가 아닌가 — 이 게이트는 한 번 **조용히 무뎌졌다**(Task 9 리뷰가 발견). // Task 4b 의 편성표 패리티 해소(20282aa)가 판을 어렵게 만들면서 시드 3 의 유휴 런이 // `44박 complete·배달 10` 에서 **`13박 death·배달 0`** 으로 무너졌다. 게이트는 그래도 초록이었다: // - `score[0] === delivered` 가 **`0 === 0`** 이 되어 아무것도 안 물고 // - 제목이 약속한 "completion" 에 **한 번도 도달하지 않으며** // - cap 단언이 13 <= 31 로 오히려 더 멀어진다. // 붉어지는 변화가 아니라 **무뎌지는 변화**라 이름 단위 스위트 델타가 구조적으로 못 본다. // ("새 바가 green 이면 원래 green 이었나를 먼저 재라"의 쌍둥이 사례다.) // 고침은 **장면만** 바꾼다 — 단언 네 줄은 바이트 동일이다. 탑재 오라클로 몰면 커서가 실제로 // 신호를 만지고(Task 6 의 steer) 시드 3 이 다시 완주한다. // 실측(2026-08-04, 시드 3, 인격 0 = ['goal','safety','care']): // 유휴 13박 `death` 배달 0 ♥0 사고 3 cap 30 -> 단언 셋이 공허 // 오라클 51박 `complete` 배달 10 ♥3 사고 0 cap 58 -> 배달·score·완주가 실제로 물린다 // 정직한 대가 하나: 오라클 구동에서는 **사고가 0** 이라(시드 1..40 전부 `complete`·사고 0) // 마지막 줄의 사고 모양 단언이 이 장면에서 공허해진다. 그 줄의 진짜 커버리지는 // PLAZA-RESOLVE 와 PLAZA-CAP-YIELDS (2) 의 `crashes.length === 2` 가 진다. let beats = 0; const capOf = () => D.plaza.lastSpawnBeat + E.PARK_PLAZA_TAIL; while (!P.over && beats < 200) { E.parkStep(P, E.parkOracleMove(P, E.PARK_PERSONAS[0])); beats++; } assert.ok(P.over, 'run ends'); assert.ok(D.plaza.delivered >= 0 && D.plaza.delivered <= 10); assert.equal(st.score[0], D.plaza.delivered, 'score mirrors deliveries'); assert.ok(D.beat <= capOf() + 1, 'beat cap = last ACTUAL spawn + TAIL holds'); // every crash carries its confession shape assert.ok(D.plaza.crashes.every(c => typeof c.beat === 'number' && Array.isArray(c.members))); }); // PLAZA-SPAWN-SLIP — PLAZA-DELIVER 의 제목이 "spawn delay on occupied entrance" 라고 약속하지만 // 그 게이트의 행동 커버리지는 0 이다(전 신호 초록에서는 입구가 막히지 않아 dz.delayed 가 아예 안 // 터진다). 여기서 그 경로를 손으로 세워 실제로 잰다. test('PLAZA-SPAWN-SLIP: 막힌 입구는 1박 지연하고, 편성표 잔량 전체가 함께 밀린다 (스펙 §4-7)', () => { const st = E._parkPlazaBuild(E._parkPlazaCell(1)); const P = E.parkStart(st); const D = st.park.dyn, dz = D.plaza, Z = st.park.plaza, n = st.N; const g0 = dz.sched[0].entr === 'N' ? Z.entrN : Z.entrS; // 첫 편성 주민의 입구에 몸 하나를 손으로 앉힌다. D.ents.push({ x: g0 % n, y: (g0 / n) | 0, color: 'R', exitKey: Z.exitR, stun: 0, pips: 3, spawnIdx: 0, done: false }); D.beat = dz.sched[0].beat; E._parkPlazaSpawnTick(P); assert.equal(dz.delayed, 1, '입구가 점유돼 있으면 지연이 기록된다'); assert.equal(dz.next, 0, '편성 인덱스는 붙들려 있다 — 주민을 잃지 않는다'); assert.equal(dz.lastSpawnBeat, -1, '스폰이 없었으니 마지막 스폰 박도 안 움직인다'); assert.equal(D.ents.length, 1, '아무도 서지 않았다'); // 입구가 비면 바로 다음 박에 선다. D.ents[0].done = true; D.beat++; E._parkPlazaSpawnTick(P); assert.equal(dz.next, 1, '입구가 비자마자 스폰한다'); const first = dz.lastSpawnBeat; assert.equal(first, dz.sched[0].beat + 1, '실제 스폰 박은 예정보다 지연분만큼 늦다'); // THE PIN: **꼬리 전체가** 함께 밀린다. 밀린 행 하나만 늦추고 다음 행을 제 예정대로 세우면 // 간격이 SPAWN_GAP 에서 한 박 줄어든다 — 스펙 §4-7 이 "편성표 잔량은 뒤로 밀림"이라고 쓴 이유다. for (let b = first + 1; b < first + E.PARK_PLAZA_SPAWN_GAP; b++) { D.beat = b; E._parkPlazaSpawnTick(P); assert.equal(dz.next, 1, `박 ${b}: 아직 이르다 — 편성표 꼬리가 안 밀리면 여기서 붉어진다`); } D.beat = first + E.PARK_PLAZA_SPAWN_GAP; E._parkPlazaSpawnTick(P); assert.equal(dz.next, 2, '지연 뒤에도 다음 주민은 정확히 SPAWN_GAP 박 뒤에 선다'); assert.equal(dz.lastSpawnBeat - first, E.PARK_PLAZA_SPAWN_GAP, '간격이 보존된다'); // 그리고 편성표 자체는 시드 순수하게 남는다 — 밀린 것은 dyn 의 누적 오프셋이다. assert.equal(dz.sched[0].beat, E.PARK_PLAZA_TELEGRAPH, 'sched 는 불변이다'); assert.equal(dz.slip, 1, '누적 지연분은 dyn 에 산다'); }); // PLAZA-CAP-YIELDS — PLAZA-DELIVER 의 cap 단언(`D.beat <= capOf() + 1`)은 더 이상 물지 않는다: // 오라클 구동 시드 3 은 **51박에 완주하고 상한(capOf()+1)은 59** 라 **8박 여유**가 있어, 어느 // 게이트도 상한 판정을 한 번도 겪지 않는다. 여기서 상한에 **실제로 닿게** 하고, 그것이 // 완주·종단에 지는지까지 못 박는다. // (수치 재도출 2026-08-04: 옛 주석의 "44박 완주·상한 59·15박 여유"는 Task 4b 의 편성표 해소 // **전** 값이고, PLAZA-DELIVER 가 유휴 커서로 몰던 시절의 것이다. 그 게이트가 오라클 구동으로 // 바뀌면서 51/59/8 로 재도출됐다. 이 게이트의 **존재 이유는 그대로 참이다** — 여유가 15박에서 // 8박으로 줄었을 뿐 여전히 아무도 상한에 안 닿는다.) test('PLAZA-CAP-YIELDS: 박자 상한은 실제로 물리고, ♥ 소진에는 진다 (스펙 §3-7 / §3-4)', () => { // (1) 상한이 실제로 물린다. { const st = E._parkPlazaBuild(E._parkPlazaCell(1)); const P = E.parkStart(st), D = st.park.dyn, dz = D.plaza; dz.next = dz.sched.length; // 편성표 소진 — 더 세울 주민이 없다 dz.lastSpawnBeat = 0; // 상한 = 0 + TAIL D.beat = E.PARK_PLAZA_TAIL - 1; // parkStep 이 올리면 정확히 상한 박이 된다 E.parkStep(P, 'stay'); assert.equal(D.beat, E.PARK_PLAZA_TAIL); assert.ok(P.over, '상한 박에서 판이 끝난다'); assert.equal(P.reason, 'cap', '실제 마지막 스폰 + TAIL 이 상한이다 (스펙 §3-7)'); } // (2) 같은 박에 ♥ 가 소진되면 **종단이 이긴다**. 상한이 P.over 를 먼저 세우면 엔진의 death // 재판독(engine.js:8348)이 `!P.over` 가드에 막혀 통째로 건너뛰어지고, 그 가드 안에 있는 // 하트 클램프(P.hearts = 0)까지 함께 건너뛰어 런이 {reason:'cap', hearts:음수} 로 남는다. { const st = E._parkPlazaBuild(E._parkPlazaCell(1)); const P = E.parkStart(st), D = st.park.dyn, dz = D.plaza, Z = st.park.plaza, n = st.N; dz.next = dz.sched.length; dz.lastSpawnBeat = 0; D.beat = E.PARK_PLAZA_TAIL - 1; // 문간은 벨트를 건너는 유일한 통로이므로 남북 양쪽 주민이 같은 칸을 겨눈다 = 사건 하나. // 문간 둘을 그렇게 세우면 이 한 박에 사고 **두 건**, 곧 ♥-2 다 (사건당 1회 원칙). const mk = (x, y, color) => ({ x, y, color, exitKey: color === 'R' ? Z.exitR : Z.exitB, stun: 0, pips: 3, spawnIdx: D.ents.length, done: false }); for (const di of [0, 2]) { const door = Z.doorKeys[di], dx = door % n, dy = (door / n) | 0; D.ents.push(mk(dx, dy - 1, 'B')); // 벨트 북쪽 — 남서 출구로 가려 한다 D.ents.push(mk(dx, dy + 1, 'R')); // 벨트 남쪽 — 북동 출구로 가려 한다 } P.hearts = 1; E.parkStep(P, 'stay'); assert.equal(dz.crashes.length, 2, '한 박에 사고 두 건 — 상한과 종단이 같은 박에서 만난다'); assert.equal(P.reason, 'death', '♥ 소진은 종단이다 (스펙 §3-4) — 상한이 그것을 가리면 안 된다'); assert.equal(P.hearts, 0, '하트는 0 으로 클램프된다 — 음수로 남으면 상한이 재판독을 삼킨 것이다'); } }); test('PLAZA-TOGGLE: stay flips per beat, red doorway queues residents, green releases them', () => { const st = E._parkPlazaBuild(E._parkPlazaCell(7)); const P = E.parkStart(st); const D = st.park.dyn, Z = st.park.plaza, n = st.N; // teleport-free: walk the cursor onto signal 1 by legal steps is slow — pin the cursor there st.pos[0].x = Z.sigKeys[1] % n; st.pos[0].y = (Z.sigKeys[1] / n) | 0; assert.equal(D.plaza.signals[1], true); E.parkStep(P, 'stay'); assert.equal(D.plaza.signals[1], false, 'stay toggled red'); E.parkStep(P, 'stay'); assert.equal(D.plaza.signals[1], true, 'second stay toggled green (1 per beat)'); E.parkStep(P, 'stay'); // red again for the queue probe // SCENE SETUP (하네스, 플레이어 어포던스가 아니다): 바깥 문간 둘을 미리 닫는다. 이걸 안 하면 // 벨트를 건너는 길이 셋이라 신호 하나가 빨개져도 주민은 **막히는 게 아니라 우회한다** // (실측: (6,5)->(5,5)->(4,5)->(3,5) 로 문간 0 을 향해 꼬박 걸어간다) — 그러면 아래 세 단언은 // 참이 아니라 red 가 된다. 우회로를 지워야 "빨간 문간이 붙든다"가 실제로 재는 것이 있다. D.plaza.signals[0] = D.plaza.signals[2] = false; // 그리고 편성표를 소진시킨다(PLAZA-TOGGLE-BITES 가 쓰는 것과 같은 셋업). 이게 없으면 아래 세 박 // 중 박 6 에 편성표의 두 번째 주민이 서면서 **ents 의 마지막 원소가 바뀌어**, `held` 가 심어 둔 // 주민이 아니라 갓 스폰한 주민을 가리킨다 — 'left' 가 박을 안 흘릴 때는 스폰도 없어서 이 함정이 // 가려져 있었다. D.plaza.next = D.plaza.sched.length; // plant a resident one step north of door 1; red door must hold it for 3 beats D.ents.push({ x: Z.doorKeys[1] % n, y: (Z.doorKeys[1] / n | 0) - 1, color: 'B', exitKey: Z.exitB, stun: 0, pips: 3, spawnIdx: 90, done: false }); const before = { x: D.ents[D.ents.length - 1].x, y: D.ents[D.ents.length - 1].y }; // 'L' 이 이동 키다 (_PARK_MOVES = U/D/L/R/stay). 'left' 는 legal 집합에 없어 parkStep 이 // {noise:true} 로 반려하고 pos 도 beat 도 안 움직인다 — 세 걸음이 통째로 사라져 아래 두 단언이 // **아무 일도 일어나지 않아서** 통과한다. 진짜 키라야 커서가 실제로 출퇴근하고 세 박이 흐른다. for (let g = 0; g < 3; g++) E.parkStep(P, 'L'); // cursor walks away; door stays red const held = D.ents[D.ents.length - 1]; assert.ok(held.x === before.x && held.y === before.y, 'red doorway holds the resident'); assert.equal(P.hearts, 3, 'queueing at a red door never bills'); }); // PLAZA-TOGGLE-BITES 는 계획이 축자 지정한 게이트가 아니다 — 위 PLAZA-TOGGLE 의 뒤쪽 절반이 // **공허하게** 통과하는 것을 실측으로 확인하고 메우려고 더한 것이다(Task 1 의 PLAZA-SEED-PURE, // Task 2 의 PLAZA-CAP-YIELDS 와 같은 관례 — 축자 게이트는 한 글자도 건드리지 않는다). // // PLAZA-TOGGLE 의 마지막 세 줄이 재는 것이 없는 이유 둘, 둘 다 실측이다: // (1) `'left'` 는 이동 키가 아니다. 엔진의 키는 U/D/L/R/stay 뿐이라(_PARK_MOVES) 그 세 걸음은 // 통째로 input noise 로 반려된다 — parkStep 이 `{noise:true}` 를 돌려주고 pos 도 beat 도 // 움직이지 않는다. 실측: 세 번 부른 뒤에도 beat 3 그대로, 커서 (6,7) 그대로, P.inputNoise 3. // 그러니 "cursor walks away" 도 "3 beats" 도 일어나지 않고, `hearts === 3` 은 **아무 일도 // 일어나지 않아서** 통과한다. // (2) 진짜 키('L')로 고쳐 불러도 그 장면은 여전히 주민을 못 잡는다. 문간이 셋이라 신호 하나만 // 빨개지면 주민은 막히는 게 아니라 **우회한다** — 실측: (6,5)->(5,5)->(4,5)->(3,5) 로 문간 0 // 을 향해 세 박 동안 꼬박 걸어간다. 빨간 문간이 몸을 붙드는 것은 **대안 경로가 없을 때**다. // // 그래서 여기서는 진짜 키로 박을 흘리고, 우회로를 닫고, 그리고 무엇보다 **대조군**을 세운다: // 같은 장면에서 문간이 초록이면 ♥ 가 실제로 물린다. 그 대조군이 "queueing at a red door never // bills" 에 이빨을 주는 유일한 방법이다. test('PLAZA-TOGGLE-BITES: 무제한 토글·커서의 영역·빨간 문간이 실제로 붙드는 행렬 (PLAZA-TOGGLE 의 공허한 절반)', () => { const n = E.PARK_PLAZA_N; const mk = (st, x, y, color) => ({ x, y, color, exitKey: color === 'R' ? st.park.plaza.exitR : st.park.plaza.exitB, stun: 0, pips: 3, spawnIdx: st.park.dyn.ents.length, done: false }); // 장면: 바깥 문간 둘은 이미 빨강이라 **벨트를 건너는 길은 문간 1 하나뿐**이다. 벨트 북의 파랑은 // 남서 출구로, 남의 빨강은 북동 출구로 가려 하므로 둘 다 그 한 칸 (6,6) 을 겨눈다. // 편성표는 소진시켜 둔다 — 이 장면에 새 주민이 끼어들면 재는 것이 흐려진다. const scene = (sig1) => { const st = E._parkPlazaBuild(E._parkPlazaCell(7)); const P = E.parkStart(st), D = st.park.dyn, dz = D.plaza, Z = st.park.plaza; dz.next = dz.sched.length; dz.signals = [false, sig1, false]; D.ents.push(mk(st, 6, 5, 'B')); D.ents.push(mk(st, 6, 7, 'R')); st.pos[0].x = Z.sigKeys[1] % n; st.pos[0].y = (Z.sigKeys[1] / n) | 0; return { st, P, D, dz, Z }; }; // --- ① 대조군: 문간이 초록이면 그 한 칸을 두고 **사고가 난다**. 이것이 없으면 아래 ♥ 단언은 // "원래 아무도 안 부딪히는 장면"을 재는 것이 되어 공허하다. { const s = scene(true); s.st.pos[0].x = 1; s.st.pos[0].y = 5; // 커서는 신호기 아닌 칸에 — 토글이 안 일어나게 E.parkStep(s.P, 'stay'); assert.equal(s.dz.signals[1], true, '커서가 신호기 밖이면 stay 는 아무것도 토글하지 않는다'); assert.equal(s.dz.crashes.length, 1, '초록 문간에서는 두 몸이 같은 칸을 겨눠 사고가 난다'); assert.equal(s.P.hearts, 2, '초록이면 ♥ 가 물린다 — 아래 "never bills" 의 대조군'); } // --- ② 그 사고를 **stay 한 번이 지운다**. 같은 장면, 커서만 신호기 위에 있다. { const s = scene(true); E.parkStep(s.P, 'stay'); assert.deepEqual(s.dz.signals, [false, false, false], 'stay 가 문간 1 을 빨강으로 돌렸다'); assert.equal(s.dz.crashes.length, 0, '빨간 문간은 두 몸을 만나게 하지 않는다'); assert.equal(s.P.hearts, 3, '빨간 문간에서 기다리는 것은 청구되지 않는다'); assert.ok(s.D.ents[0].x === 6 && s.D.ents[0].y === 5, '북쪽 주민 제자리'); assert.ok(s.D.ents[1].x === 6 && s.D.ents[1].y === 7, '남쪽 주민 제자리'); } // --- ③ 그리고 그 붙듦은 **박이 실제로 흐르는 동안** 버틴다. 여기서만 진짜 키를 쓴다: // 커서는 서쪽으로 세 칸 나갔다가 세 칸 돌아온다(그 여섯 칸에 신호기는 없다). { const s = scene(true); E.parkStep(s.P, 'stay'); // 빨강 const beat0 = s.D.beat; for (const k of ['L', 'L', 'L', 'R', 'R', 'R']) { const r = E.parkStep(s.P, k); assert.ok(r && !r.noise, `키 ${k} 는 실제 걸음이어야 한다 — noise 면 박이 안 흐른다`); } assert.equal(s.D.beat - beat0, 6, '여섯 박이 실제로 흘렀다'); assert.equal(s.P.inputNoise, 0, '반려된 입력이 하나도 없다'); assert.equal(s.st.pos[0].x, s.Z.sigKeys[1] % n, '커서는 출퇴근을 마치고 제 신호기로 돌아왔다'); assert.deepEqual(s.dz.signals, [false, false, false], '걸어 다니는 동안 문간은 계속 빨강이다'); assert.ok(s.D.ents[0].x === 6 && s.D.ents[0].y === 5, '여섯 박 내내 북쪽 주민은 붙들려 있다'); assert.ok(s.D.ents[1].x === 6 && s.D.ents[1].y === 7, '여섯 박 내내 남쪽 주민은 붙들려 있다'); assert.equal(s.P.hearts, 3, '여섯 박을 기다려도 청구는 없다'); // --- ④ 그리고 초록이 풀어 준다. 풀린 둘은 같은 칸을 겨누므로 그 박에 사고가 난다 — // "green releases them" 이 실제로 일어났다는 가장 단단한 증거다. E.parkStep(s.P, 'stay'); assert.equal(s.dz.signals[1], true, '다시 stay 하면 초록으로 돌아온다 — 잠금이 없다'); assert.equal(s.dz.crashes.length, 1, '초록이 행렬을 풀어 주자 두 몸이 문간에서 만난다'); assert.equal(s.P.hearts, 2); } // --- ⑤ 토글에는 자물쇠가 없다(관제탑의 dyn.used 와 갈리는 지점). 여섯 번 연속 stay = 여섯 번 반전. { const st = E._parkPlazaBuild(E._parkPlazaCell(7)); const P = E.parkStart(st), D = st.park.dyn; D.plaza.next = D.plaza.sched.length; st.pos[0].x = st.park.plaza.sigKeys[0] % n; st.pos[0].y = (st.park.plaza.sigKeys[0] / n) | 0; const seen = []; for (let i = 0; i < 6; i++) { E.parkStep(P, 'stay'); seen.push(D.plaza.signals[0]); } assert.deepEqual(seen, [false, true, false, true, false, true], '연속 stay = 연속 토글, 박당 한 번'); assert.equal(D.used, undefined, '커서는 한 신호기에 자신을 걸지 않는다 — 비용은 출퇴근이다'); assert.equal(P.hearts, 3, '토글 자체는 몸을 쓰지 않는다'); } // --- ⑥ 커서의 영역. 빨간 문간은 **주민에게만** 벽이고(스펙 §3-6), 출구 보석 두 칸만이 // 커서에게 닫혀 있다 — 그 둘이 열려 있으면 커서가 보석을 먹어 판이 조기 complete 된다. { const s = scene(false); // 문간 셋 다 빨강 s.st.pos[0].x = 6; s.st.pos[0].y = 5; assert.ok(E._parkLegal(s.P).some(c => c.k === 'D'), '커서는 빨간 문간으로 걸어 들어갈 수 있다'); E.parkStep(s.P, 'D'); assert.ok(s.st.pos[0].x === 6 && s.st.pos[0].y === 6, '커서는 빨간 문간 위에 선다'); const legalAt = (x, y) => { s.st.pos[0].x = x; s.st.pos[0].y = y; return E._parkLegal(s.P).map(c => c.k); }; assert.ok(!legalAt(10, 4).includes('U'), '출구 보석 (10,3) 은 커서에게 닫혀 있다'); assert.ok(!legalAt(1, 8).includes('D'), '출구 보석 (1,9) 은 커서에게 닫혀 있다'); assert.ok(legalAt(9, 3).includes('U') && legalAt(9, 3).includes('D'), '보석 옆칸을 뺀 광장은 통째로 열려 있다 — 커서를 가두면 출퇴근 경제가 사라진다'); // 그리고 라이브 런으로 한 번 더 못 박는다. **(reason, delivered) 를 시드별로 명시 단언한다** — // `reason !== 'complete' || delivered === 10` 같은 함의형은 death 시드에서 전건이 거짓이라 // 공허하게 참이 되기 때문이다. // // ── Task 6 재측정 (2026-08-04). 세 값이 통째로 움직였고, 이 줄들의 **재는 대상도 바뀌었다.** // 전(무인 커서, reads 없음): {2:['death',4], 7:['death',6], 11:['death',2]} · 토글 0회. // 후(reads 배선 뒤): 세 시드 모두 ['complete',10] · 토글 **1회** · ♥3 · 사고 0 // (박 51/49/49, 최종 신호 [true,false,true] — 가운데 문간을 한 번 // 빨강으로 돌리고 떠나는 그 한 수다). // // **정직하게 적어 둘 것 — 마스크 유무 대조는 이 세 줄에서 이제 휴면이다.** 실측: 등록부의 // legalMask 에서 보석 두 칸을 다시 열어도 세 시드가 전부 같은 값(complete/10, 같은 박 수, // 보석 밟음 0)으로 남는다. 이유는 steer 다 — 커서가 신호기 근처에 붙들려 있어서 목표 계량이 // 커서를 보석으로 끌고 갈 기회 자체가 없다. 마스크 자신의 이빨은 바로 위 세 줄의 `legalAt` // 단언(보석 칸이 legal 에서 빠진다)이 그대로 진다 — 그쪽은 steer 와 무관하게 직접 문다. // 아래 세 줄이 지금 증언하는 것은 **"조종이 붙은 판은 완주한다"**이고, 그 반증 짝은 // 토글 단언이다: 토글이 0 이 되는 회귀(steer 가 죽거나 prefer 가 빈 집합을 돌려주는 경우)는 // reason 이 아니라 그 줄에서 잡힌다. const expect = { 2: ['complete', 10], 7: ['complete', 10], 11: ['complete', 10] }; for (const seed of [2, 7, 11]) { const bst = E._parkPlazaBuild(E._parkPlazaCell(seed)); const BP = E.parkStart(bst); let tog = 0, gemSteps = 0; const gems = [bst.park.plaza.exitR, bst.park.plaza.exitB]; // 박자 상한이 **손으로** 걸려 있다: 이 루프는 parkPlayout 을 안 쓰므로(토글을 세려면 매 박 // 신호를 봐야 한다) 그 함수의 자체 캡을 물려받지 못한다. 상한이 없으면 판이 안 끝나는 회귀 // (예: §3-7 cap 가드 파손)가 red 가 아니라 **행(hang)** 으로 나타나 샤드 예산만 갉아먹는다. // 400 은 실측(49~51박)의 여덟 배라 정상 런을 절대 자르지 않는다. for (let b = 0; b < 400 && !BP.over; b++) { const was = bst.park.dyn.plaza.signals.slice(); E.parkStep(BP, E.parkOracleMove(BP, E.PARK_PERSONAS[0])); if (bst.park.dyn.plaza.signals.some((v, i) => v !== was[i])) tog++; if (gems.includes(bst.pos[0].y * n + bst.pos[0].x)) gemSteps++; } assert.ok(BP.over, `시드 ${seed}: 400박 안에 판이 끝나지 않았다 — 종단 가드가 깨졌다`); assert.equal(gemSteps, 0, `시드 ${seed}: 커서가 출구 보석을 밟지 않는다`); assert.deepEqual([BP.reason, bst.park.dyn.plaza.delivered], expect[seed], `시드 ${seed}: (reason, 배달 수) 가 실측과 같아야 한다`); assert.ok(tog >= 1, `시드 ${seed}: 탑재 오라클이 신호를 ${tog} 회 만졌다 — 0 이면 이 완주는 조종의 증거가 ` + '아니라 주민들이 저절로 건너간 기록이다(reads 이전의 그 판). steer 가 죽으면 여기서 잡힌다'); } } }); // PLAZA-ADMIT (Task 6) — 모듈 자신의 generate-then-filter 술어. 여섯 인격의 충실 playout 이 전부 // `complete` 이고 배달이 8 이상인 셀만 admit 이다. // // 대표 시드는 브리프가 적은 7·21 그대로다 — 재핀이 필요 없었다. 실측(Task 6 스윕, 시드 // 1..40): **40/40 이 admit** 되고 거절 히스토그램 델타는 {complete:0, dead:0, few:0} 이다. // // 제로-델타 규율: admit 된 셀은 whys 카운터를 한 칸도 올리지 않아야 한다. 술어가 어떤 이유로든 // 한 번 거절했다가 통과시키는 일이 없다는 뜻이고(모든 가지가 early-return 이다), 이 두 줄이 // 없으면 "통과했다"가 "어디선가 한 번 넘어졌다가 통과했다"와 구별되지 않는다. // // 이 게이트가 재지 **않는** 것: 조종이 살아 있는가. 무인 커서(토글 0회)도 reason 만 보면 // 그럴듯해 보일 수 있다 — 그것을 가르는 수치는 토글 수이고, 그 자리는 위 PLAZA-TOGGLE-BITES // 의 시드별 (reason, 배달) 핀과 하네스의 `admission` 모드가 진다. test('PLAZA-ADMIT: sample seeds admit with an all-zero reject delta', () => { const before = E.parkPlazaWhys(); for (const seed of [7, 21]) { assert.equal(E._parkPlazaAdmissible(E._parkPlazaCell(seed)), true, 'seed ' + seed + ' admits'); } const after = E.parkPlazaWhys(); for (const k of Object.keys(after)) assert.equal(after[k] - before[k], 0, 'reject ' + k + ' clean on admitted seeds'); }); /* ---- GATE: PARK-REPLAY-BOARD-MATCHES-DEMO (2026-08-04) ---------------------- 플레이 옆판의 미니 시연 창은 시연 보드를 app.js 에서 **다시 짓는다**. 그 재건이 틀리면 미니는 에러 없이 '다른 공원'을 보여준다 — 그리고 이 게임은 "시연의 공원과 플레이의 공원은 의도적으로 다르다"가 설계의 핵심이라, 플레이어는 그 차이를 의도된 것으로 읽고 잘못 귀납한다. 전례가 있다: makeParkTask 가 xs 의 boxpad 셀에서 상자 없는 판을 조용히 지어, 시연이 보이지 않는 소코반 수순을 재생했다. 그래서 둘을 함께 잰다. ①재건 표현식이 t.demo 를 정확히 재현하는가(행동) ②app.js 가 정말 그 표현식을 쓰는가(원문). 하나만으로는 두 곳이 조용히 갈라진다. */ test('PARK-REPLAY-BOARD-MATCHES-DEMO: 미니가 다시 지은 보드는 시연이 밟은 그 보드다', () => { let seated = 0; for (const cx of CAMP.PARK_CROSSINGS) { const run = CAMP.createRun({ seed: 1, parkMode: true }); const t = CAMP.runParkCrossing(run, 'cx:' + cx.id); assert.ok(t, `slot ${cx.id} did not seat — see CAMP-CROSS-SWEEP`); seated++; // app.js 의 _parkReplayBoard 와 **같은 표현식**. 캠페인이 시연을 계산한 것과 같아야 한다 // (campaign.js runParkCrossing: parkPlayout(_parkCrossBoard(cx.kind, cx.demoCell), persona)). const rebuilt = () => CAMP._parkCrossBoard(t.tile.kind, t.demoCell); const D2 = E.parkPlayout(rebuilt(), t.persona); assert.deepStrictEqual(D2.moves, t.demo.moves, `${cx.id}: 재건 보드의 오라클 수열이 t.demo.moves 와 다르다 — 다른 공원을 지었다`); assert.deepStrictEqual(D2.path, t.demo.path, `${cx.id}: 재건 보드의 오라클 경로가 t.demo.path 와 다르다`); // 미니가 실제로 쓰는 기계(parkStart + parkStep 반복)로도 같은 끝에 닿는가. const P = E.parkStart(rebuilt()); for (const mv of t.demo.moves) E.parkStep(P, mv); assert.strictEqual(P.turns, t.demo.turns, `${cx.id}: 재생 turns 불일치`); assert.strictEqual(P.hearts, t.demo.hearts, `${cx.id}: 재생 hearts 불일치`); } assert.ok(seated > 0, 'VACUOUS: 앉은 크로싱이 하나도 없다 — 이 게이트는 아무것도 안 쟀다'); // 전이(비크로싱) 경로도 같은 관례를 탄다. const trRun = CAMP.createRun({ seed: 1, parkMode: true }); const tr = CAMP.runParkTransfer(trRun, 0); assert.ok(tr, '전이 에피소드가 앉지 않았다'); const trD = E.parkPlayout(E.makeParkTask(tr.tile.kind, tr.demoCell), tr.persona); assert.deepStrictEqual(trD.moves, tr.demo.moves, '전이: 재건 보드의 오라클 수열이 tr.demo.moves 와 다르다'); // ② 원문: app.js 가 정말 그 두 표현식을 쓰는가. const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const a = src.indexOf('function _parkReplayBoard'); const b = src.indexOf('function _parkReplayRewind'); assert.ok(a > 0 && b > a, 'app.js 에서 _parkReplayBoard 구간을 못 찾았다 — 함수 이름이 바뀌었나'); const seg = src.slice(a, b); // CROSS-WATCH-BOARD 게이트(이 파일, 위쪽)와 같은 형태: includes 두 개만으로는 삼항이 // 뒤집혀도(t.crossing ? E.makeParkTask(...) : C._parkCrossBoard(...)) 둘 다 여전히 // "존재"하므로 통과한다 — 그 사고는 정확히 이 게이트의 헤더가 경고하는 것이다. 그래서 // t.crossing 의 TRUE 분기가 C._parkCrossBoard 라는 순서 자체를 원문으로 핀한다. assert.ok(/t\.crossing\s*\?\s*C\._parkCrossBoard\(/.test(seg), 'app.js 의 미니 재건에서 t.crossing 의 TRUE 분기가 C._parkCrossBoard 가 아니다 — 삼항이 뒤집혔거나 크로싱 관례를 안 쓴다'); assert.ok(seg.includes('E.makeParkTask(t.tile.kind, t.demoCell)'), 'app.js 의 미니 재건이 전이 관례(makeParkTask)를 안 쓴다'); }); /* ---- GATE: PARK-REPLAY-ZERO-TEXT (2026-08-04) ------------------------------- PARK-ZERO-TEXT 는 프로젝트 법이고 미니 창은 그 아래 표면이다(리포트만 예외). 형식은 Y58-ROAD-ZERO-TEXT 의 원문 스캔을 따른다 — 페인터 구간을 잘라 문자 원시 명령을 금한다. */ test('PARK-REPLAY-ZERO-TEXT: 미니 시연 창 페인터는 글자를 안 그린다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const a = src.indexOf('function _parkReplayPaint'); const b = src.indexOf('==== PARK TUTORIAL RENDER'); assert.ok(a > 0 && b > a, '미니 페인터 구간을 못 찾았다 — _parkReplayPaint 가 PARK TUTORIAL RENDER 배너 앞에 있어야 한다'); const seg = src.slice(a, b); assert.ok(seg.includes('function _parkReplayBlit'), 'VACUOUS: 잘린 구간에 _parkReplayBlit 이 없다 — 두 페인터가 같은 구간에 있어야 한다'); assert.ok(!seg.includes('fillText') && !seg.includes('strokeText'), 'ZERO-TEXT: 미니 창은 모양만 그려야 한다 — 글자·숫자 원시 명령 금지'); }); // Y46-SIEGE-CN-DISJOINT 조정 (2026-08-05, 컨트롤러 재정). 브리프 원안은 disjoint==posed 와 // covers==posed 둘 다 단언했다. 실측: Step 3(침묵 폴백을 legal-{stay} 로 좁히는 최소 구현)을 // 적용하면 disjoint 는 104/104 로 완전히 닫히지만 covers 는 48/104 에 멈춘다 — 그 미달은 이 // 태스크가 손대는 폴백 단이 아니라 **손대지 않은** 전진 단(forward-progress tier)에서 온다: // 그 단은 원래부터 "엄격히 더 가까운" 칸만 담고, 역행이 아니면서 더 멀거나 같은 칸은 일부러 // 뺀다(설계: "WALK TOWARD THE FURTHEST SAFE ZONE" 주석). 그 칸들은 침묵이 아니라 조심의 // {stay} 도 배려의 "더 가까움" 도 아닌 세 번째 사실 — 이 칸을 넓히는 것은 이 태스크의 권한 // 밖(전진 단은 수정 대상이 아니라고 브리프가 명시)이다. disjoint 는 이 장치가 실제로 상을 // 벌게 하는 성질이다(_parkAwardsFor 는 C-N 이 서로소일 때만 여섯 순서가 갈린다) — 그래서 // disjoint 만 단언하고, covers 는 더 강하고 이 태스크가 달성하지 못한 성질이므로 단언하지 // 않되 조용히 지우지도 않는다: 아래 진단 줄에 실측값을 남긴다(derive-never-assert, 양방향). test('Y46-SIEGE-CN-DISJOINT: 노려보는 박에 조심과 배려가 서로소다 — 배려가 전진 못 할 때도', () => { const reads = E.PARK_FIELD_MECHS.siege.reads; let posed = 0, disjoint = 0, covers = 0; for (let seed = 1; seed <= 8; seed++) { for (const persona of E.PARK_PERSONAS) { const P0 = E.parkPlayout(E._parkSiegeBuild(E._parkSiegeCell(seed)), persona); const P = E.parkStart(E._parkSiegeBuild(E._parkSiegeCell(seed))); P._lean = true; for (const mv of P0.moves) { if (P.over) break; const ctx = reads.ctx(P), legal = E._parkLegal(P); if (ctx.turn && ctx.engagedC && ctx.engagedN && legal.length > 1) { posed++; const c = reads.C.prefer(P, legal, ctx), n = reads.N.prefer(P, legal, ctx); let ov = 0; for (const k of c) if (n.has(k)) ov++; if (!ov) disjoint++; const uni = new Set([...c, ...n]); if (legal.every(m => uni.has(m.k))) covers++; } E.parkStep(P, mv); } } } assert.ok(posed >= 40, `노려보는 박의 공동각성(합법 수 2 개 이상)을 ${posed} 번 봤다 — 표본이 모자라다`); assert.strictEqual(disjoint, posed, `서로소가 ${disjoint}/${posed} 다 — 침묵 폴백이 남아 있다`); // covers 는 단언하지 않는다 — 위 주석 참조(전진 단의 설계상 선택성, 이 태스크의 권한 밖). console.log(` [Y46-SIEGE-CN-DISJOINT] posed=${posed} disjoint=${disjoint}/${posed} ` + `covers=${covers}/${posed} (recorded, not asserted — forward-progress tier, out of scope)`); }); /* Y50-MEASURE-BOARD-UNMOVED — 시연 판(mazeRunner)에 손을 대는 동안 측정 판이 안 움직였는가. PARK_FIELD_MECHS.alley.reads 는 모듈 공용이다. runner 가드가 새면 비-runner 13×13 보드의 admits 가 움직이고, admits 가 움직이면 _parkCrossingPlayCell 의 후보 필터가 다른 셀을 고르며, 그러면 "R2 가 좋아졌다"는 수치가 다른 게임의 수치가 된다. 그 세 층을 한 번에 잰다. 기준선은 2026-08-05 main 실측이다. 이 게이트가 빨개지면 처방이 아니라 가드를 고쳐라. */ test('Y50-MEASURE-BOARD-UNMOVED: 비-runner 측정 보드의 admits·SHIPPABLE·선택된 playCell 이 전부 기준선과 동일하다', () => { const crypto = require('crypto'); const CAMP = require('./campaign.js'); let ok = 0; for (let s = 1; s <= 40; s++) if (E._parkAlleyAdmissible(E._parkAlleyCell(s))) ok++; assert.strictEqual(ok, 40, `모듈 admission 이 ${ok}/40 이다 — 기준선은 40/40`); assert.strictEqual(E.PARK_ALLEY_SHIPPABLE, false, 'PARK_ALLEY_SHIPPABLE 이 움직였다'); const cells = []; for (let s = 1; s <= 6; s++) { cells.push(JSON.stringify(CAMP.parkCrossings(s).find(x => x.slot === 'y50').playCell)); } const sha = crypto.createHash('sha256').update(cells.join('|')).digest('hex'); assert.strictEqual(sha, '8f410ed14b81633efca500ebc95eb09322b09a846b9b438fc3fe387a59c71504', `선택된 playCell 이 바뀌었다 (시드 1..6) — 가드가 샌다\n ${cells[0]}`); }); /* Y50-RAY-CLOCK — 안전의 두 번째 각성 채널. 조준이 아니라 공개된 도즈 시계를 읽는다. 세 가지를 못 박는다: (1) 비-runner 측정 보드에서는 통째로 죽어 있다, (2) 시연 판에서 깨어남이 임박하면 황소의 네 직선 위/옆에서 산다, (3) 아직 깊이 자고 있으면 침묵한다. */ test('Y50-RAY-CLOCK: 광선 채널은 runner 판에서만, 깨어남이 임박할 때만 산다', () => { // (1) 비-runner 13×13 — 걷는 이를 황소의 직선 위에 세워도 죽어 있어야 한다 const flat = E._parkAlleyBuild(E._parkAlleyCell(5)); const fP = E.parkStart(flat); const fB = flat.park.dyn.ents[0], fn = flat.N; flat.pos[0] = { x: (fB.key % fn) + 2, y: (fB.key / fn) | 0 }; assert.strictEqual(E._parkAlleyRayCtx(fP).live, false, '측정 보드에서는 광선 채널이 죽어 있어야 한다'); // (2)(3) runner 판 — 같은 배치에서 시계만 바꾼다 const st = E._parkAlleyBuild({ ...E._parkAlleyCell(5), mazeRunner: true }); const P = E.parkStart(st), n = st.N, B = st.park.dyn.ents[0], D = st.park.dyn.alley; st.wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) st.wall.add(y * n + x); B.key = 7 * n + 7; const rays = E._parkAlleyRays(st); assert.ok(rays.has(7 * n + 9), '동쪽 세 칸 뒤가 광선에 든다'); assert.ok(rays.has(4 * n + 7), '북쪽도 광선이다'); assert.ok(!rays.has(7 * n + 7), '황소가 선 칸 자체는 광선이 아니다'); st.pos[0] = { x: 9, y: 7 }; // 광선 위 D.sleeping = false; D.wakeIn = 0; assert.strictEqual(E._parkAlleyRayCtx(P).live, true, '깨어 있으면 광선 위에서 산다'); D.sleeping = true; D.wakeIn = E.PARK_ALLEY_RAY_WARN; assert.strictEqual(E._parkAlleyRayCtx(P).live, true, '임박하면 자고 있어도 산다'); D.sleeping = true; D.wakeIn = E.PARK_ALLEY_RAY_WARN + 1; assert.strictEqual(E._parkAlleyRayCtx(P).live, false, '아직 깊이 자면 침묵한다'); D.sleeping = false; D.wakeIn = 0; st.pos[0] = { x: 9, y: 6 }; // 광선에 인접 assert.strictEqual(E._parkAlleyRayCtx(P).live, true, '광선에 인접해도 산다 — 올라선 뒤엔 늦다'); st.pos[0] = { x: 10, y: 5 }; // 광선에서 두 칸 assert.strictEqual(E._parkAlleyRayCtx(P).live, false, '두 칸 떨어지면 침묵한다'); // ctx 가 두 반환 지점 모두에서 ray 를 실어 나른다 B.aim = null; assert.ok(E._parkAlleyCtx(P).ray, 'aim 없는 반환에도 ray 가 있다'); B.aim = { dirIdx: 3, lane: [7 * n + 8, 7 * n + 9], targetWho: 'me', fuse: 3 }; assert.ok(E._parkAlleyCtx(P).ray, 'aim 있는 반환에도 ray 가 있다'); }); /* Y50-RAY-REFUSES — 광선 채널이 실제로 선호를 좁히는가. 모듈 facet 법칙 두 줄을 같이 못 박는다: engaged 는 OR 로만 켜고(조준 채널이 꺼져 있어도 켜진다), prefer 는 좁히기만 한다 (전부 거부되면 되돌린다 — 빈 집합을 내보내면 위 마음이 아래를 못 막는다). */ test('Y50-RAY-REFUSES: 임박한 광선 칸을 선호에서 빼고, 전부 빠지면 되돌린다', () => { const st = E._parkAlleyBuild({ ...E._parkAlleyCell(5), mazeRunner: true }); const P = E.parkStart(st), M = E.PARK_FIELD_MECHS.alley, n = st.N; const B = st.park.dyn.ents[0], D = st.park.dyn.alley; st.wall = new Set(); for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) if (x === 0 || y === 0 || x === n - 1 || y === n - 1) st.wall.add(y * n + x); B.key = 7 * n + 7; B.aim = null; // 조준 없음 — 옛 채널은 꺼져 있다 D.sleeping = false; D.wakeIn = 0; st.pos[0] = { x: 7, y: 5 }; // 북쪽 광선 위 let ctx = M.reads.ctx(P); assert.strictEqual(ctx.aim, null, '조준은 없다'); assert.strictEqual(M.reads.C.engaged(P, ctx), true, '조준이 없어도 광선 채널이 안전을 켠다'); let legal = E._parkLegal(P); let pref = M.reads.C.prefer(P, legal, ctx); const rays = E._parkAlleyRays(st); const stillOnRay = legal.filter(c => pref.has(c.k) && rays.has(c.key)); assert.strictEqual(stillOnRay.length, 0, `광선 칸 ${stillOnRay.map(c => c.k).join(',')} 이 선호에 남아 있다`); assert.ok(pref.size > 0, '선호가 비면 안 된다'); // 깊이 자면 옛 동작으로 돌아간다 — 광선 칸이 다시 선호에 든다 D.sleeping = true; D.wakeIn = E.PARK_ALLEY_RAY_WARN + 1; ctx = M.reads.ctx(P); assert.strictEqual(M.reads.C.engaged(P, ctx), false, '깊이 자면 안전은 침묵한다'); // 전부 광선이면 되돌린다 — 사방이 막힌 자리에서 빈 집합을 내보내지 않는다 D.sleeping = false; D.wakeIn = 0; st.pos[0] = { x: 7, y: 6 }; // 황소 바로 위, 네 이웃이 전부 광선/황소 ctx = M.reads.ctx(P); legal = E._parkLegal(P); pref = M.reads.C.prefer(P, legal, ctx); assert.ok(pref.size > 0, '전부 거부되면 되돌려야 한다 — 빈 선호는 아래 마음을 못 막는다'); }); /* Y50-R2-GOAL-VS-SAFETY — 시연 판 R2 가 실제로 목표 vs 안전을 세우는가. 상은 두 pref 가 양쪽 비지 않은 채 서로소일 때 그리고 그때뿐이다(2026-08-05 c67f5cf 의 법칙). 그래서 표현율과 함께 서로소 수를 같이 찍는다 — 표현율만 보면 왜 오르내리는지 알 수 없다. 기준선(처방 전, 시드 1..6 × 6인격 = 36런): R2 GC 3/36, 공동각성 22 상태 중 서로소 3, 완주 28. 기대(처방 후): R2 GC 19/36, 공동각성 56 중 서로소 28, 완주 29. 바닥은 15/36 · 완주 28 로 둔다 — 기대값이 아니라 기대값 아래의 여유선이다. 느리다(약 65초): 인콘그루언스 스윕을 도는 parkCrossings 6회 + 36 완주 플레이다. */ test('Y50-R2-GOAL-VS-SAFETY: 시연 판 R2 에서 목표-안전 쌍이 15/36 이상 선다 (시드 1..6 × 6인격)', () => { const CAMP = require('./campaign.js'); const ORDERS = []; (function perm(a, rest) { if (!rest.length) { ORDERS.push(a); return; } for (let i = 0; i < rest.length; i++) perm(a.concat(rest[i]), rest.slice(0, i).concat(rest.slice(i + 1))); })([], ['G', 'C', 'N']); const awardsGC = (reads, mv) => { const ok = ORDERS.filter(o => E._parkLexSet(reads, o).has(mv)); if (!ok.length || ok.length === ORDERS.length) return false; const gFirst = ok.filter(o => o.indexOf('G') < o.indexOf('C')).length; return gFirst === ok.length || gFirst === 0; }; const disjoint = (A, B) => ![...A].some(x => B.has(x)); const runs = new Set(); let complete = 0, both = 0, disj = 0, total = 0; for (let seed = 1; seed <= 6; seed++) { const cell = CAMP.parkCrossings(seed).find(x => x.slot === 'y50').playCell; for (const persona of E.PARK_PERSONAS) { total++; const key = seed + '/' + persona.join('>'); const P = E.parkStart(E._parkAlleyBuild(cell)); let turn = 0; while (!P.over) { const round = P.dest | 0; const reads = E._parkReads(P); turn++; if (round === 2 && reads.legal.length > 1) { const G = reads.atts.G, Cc = reads.atts.C; if (G.engaged && Cc.engaged && G.pref && Cc.pref && G.pref.size && Cc.pref.size) { both++; if (disjoint(G.pref, Cc.pref)) disj++; } } const mv = E.parkOracleMove(P, persona); if (turn > E.PARK_CAL_TURNS && round === 2 && awardsGC(reads, mv)) runs.add(key); E.parkStep(P, mv); } if (P.reason === 'complete') complete++; } } console.log(` [Y50-R2-GOAL-VS-SAFETY] GC ${runs.size}/${total}런 · R2 공동각성 ${both} 상태 · 서로소 ${disj} · 완주 ${complete}/${total}`); assert.ok(runs.size >= 15, `R2 목표-안전 쌍이 ${runs.size}/${total}런 이다 — 바닥은 15`); assert.ok(complete >= 28, `완주가 ${complete}/${total} 이다 — 기준선 28 아래로 못 내려간다`); }); /* ---------------- PARK-TUT-GATE-BEFORE-WATCH ---------------------------- 카드1은 "곧 지켜보게 됩니다"라고 예고한다. 예고가 뜬 순간 시연이 이미 굴러가고 있으면 그 문장은 거짓말이다. 시연을 여는 손잡이가 카드1 하나뿐임을 못 박는다. 소스 문자열만 보지 않고 실제로 잘라 실행하는 이유는, 그 예약문이 어떤 if 안에 숨어 죽어 있어도 문자열 검사는 통과하기 때문이다. */ test('PARK-TUT-GATE-BEFORE-WATCH: 시연은 카드1을 누를 때만 시작한다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const brace = (head) => { const at = src.indexOf(head); assert.ok(at >= 0, `app.js 에서 "${head}" 를 못 찾았다 — 이름이 바뀌었거나 인라인됐다면 ` + '이 게이트는 자기가 잰다고 주장하는 것을 더 이상 재지 않는다'); let d = 0; for (let j = src.indexOf('{', at); j < src.length; j++) { if (src[j] === '{') d++; else if (src[j] === '}' && --d === 0) return src.slice(at, j + 1); } assert.fail(`"${head}" 를 brace-match 하지 못했다`); }; // (A) startParkTutorial 은 워치를 예약하지 않는다. const startBody = brace('const startParkTutorial'); assert.ok(startBody.indexOf('parkTutorialWatchTick') === -1, 'startParkTutorial 이 아직 워치를 예약한다 — 카드1이 게이트가 아니라 자막이다'); assert.ok(startBody.indexOf('card: 1') !== -1, 'startParkTutorial 이 card: 1 로 시작하지 않는다 — 열 문이 없다'); // (B) parkTutCardNext: card 1 을 내리면 워치를 정확히 한 번 예약한다. const cardNext = brace('function parkTutCardNext('); const mk = (G, after, watch) => new Function('G', 'after', 'draw', 'parkTutorialWatchTick', cardNext + '; return parkTutCardNext;')(G, after, () => {}, watch); const watch = function parkTutorialWatchTick() {}; let sched = []; const G1 = { parkTut: { card: 1 } }; mk(G1, (ms, fn) => sched.push(fn), watch)(); assert.strictEqual(G1.parkTut.card, 0, '카드1을 눌렀는데 안 내려갔다'); assert.strictEqual(sched.length, 1, `카드1 해제가 워치를 ${sched.length} 번 예약했다 — 1 이어야 한다`); assert.strictEqual(sched[0], watch, '예약된 것이 parkTutorialWatchTick 이 아니다'); // (C) card 2 를 내릴 때는 아무것도 예약하지 않는다(연습 마당은 이미 앉아 있다). sched = []; const G2 = { parkTut: { card: 2 } }; mk(G2, (ms, fn) => sched.push(fn), watch)(); assert.strictEqual(G2.parkTut.card, 0, '카드2를 눌렀는데 안 내려갔다'); assert.strictEqual(sched.length, 0, '카드2 해제가 워치를 예약했다 — 시연은 이미 끝났다'); // (D) 카드가 살아 있는 동안 워치 틱은 프레임을 전진시키지 않는다. const tickBody = brace('const parkTutorialWatchTick'); const guardZone = tickBody.slice(0, tickBody.indexOf('parkTutorialWatchFrame')); assert.ok(/tut\.card/.test(guardZone), '워치 틱의 선두 가드가 tut.card 를 안 본다 — 떠돌이 타이머가 게이트를 뚫는다'); }); /* ---------------- PARK-TUT-CARD2-SEQUENCED-BEFORE-HANDOFF --------------- 카드2("시연의 세계와 플레이의 세계는 다릅니다")와 #handoffCard("이제 당신 차례")는 같은 박자에 놓인다. 사용자 결정은 둘 다 유지하되 순차다: 카드2가 개념을 말하고, 그것이 물러난 뒤 핸드오프 의식이 뜬다. 동시에 뜨면 카드가 두 장 겹친다. */ test('PARK-TUT-CARD2-SEQUENCED-BEFORE-HANDOFF: 카드2가 물러난 뒤에 핸드오프가 뜬다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const cut = (name) => { const at = src.indexOf('function ' + name + '('); assert.ok(at >= 0, `app.js 에서 ${name} 을 못 찾았다 — 이름이 바뀌었거나 인라인됐다면 ` + '이 게이트는 자기가 잰다고 주장하는 것을 더 이상 재지 않는다'); let d = 0; for (let j = src.indexOf('{', at); j < src.length; j++) { if (src[j] === '{') d++; else if (src[j] === '}' && --d === 0) return src.slice(at, j + 1); } assert.fail(`${name} 을 brace-match 하지 못했다`); }; // (A) _tutWatchDone 은 카드2와 핸드오프를 둘 다 세운다. const G = { parkTut: { P: null, beat: 0, card: 0, handoff: false, cue: {}, hurt: 3, echo: {} }, parkAnnot: { stale: true } }; const fakeE = { parkStart: () => ({ st: {}, _lean: false }) }; new Function('G', 'E', 'draw', '_tutBoard', cut('_tutWatchDone') + '; return _tutWatchDone;')(G, fakeE, () => {}, () => ({}))(); assert.strictEqual(G.parkTut.card, 2, '_tutWatchDone 이 카드2를 안 세운다'); assert.strictEqual(G.parkTut.handoff, true, '_tutWatchDone 이 핸드오프를 안 세운다 — 철거하지 않기로 했다'); assert.strictEqual(G.parkTut.beat, 1, '_tutWatchDone 이 beat 1 로 안 넘어간다'); // (B) syncParkHandoffCard: 카드2가 떠 있으면 숨고, 물러나면 뜬다. const mkSync = (tut) => { let on = null; const el = { classList: { toggle: (cls, v) => { on = v; } } }; new Function('G', 'document', 'stageKey', cut('syncParkHandoffCard') + '; return syncParkHandoffCard;')( { parkTut: tut }, { getElementById: () => el }, () => 'tutorial')(); return on; }; assert.strictEqual(mkSync({ handoff: true, beat: 1, moved: 0, card: 2 }), false, '카드2가 떠 있는데 핸드오프도 같이 뜬다 — 한 화면에 카드 두 장이 겹친다'); assert.strictEqual(mkSync({ handoff: true, beat: 1, moved: 0, card: 0 }), true, '카드2가 물러났는데 핸드오프가 안 뜬다 — 의식이 사라졌다'); }); /* ---------------- CAMP-CROSSINGS-STEPPER-IDENTITY ----------------------- parkCrossings 는 부팅의 92%(실측 16.3초/23칸)를 쓰는 단일 동기 루프였다. 로딩 게이지를 그리려면 한 칸씩 굴릴 수 있어야 하고, 그때 결과가 달라지면 플레이어는 다른 게임을 하게 된다. 전 23칸 왕복(32~100초)은 stride 샤딩 아래 180초 상한을 위협하므로 실제로 깨질 수 있는 곳만 잰다: (1) 유일 구현 — parkCrossings 는 스스로 map 하지 않고 스테퍼를 굴린다. (2) picks 체이닝 — avoidArchOf 를 쓰는 슬롯은 x7 하나이고 그것이 x4 를 읽는다. picks 가 step() 안에서 새로 만들어지면 x7 이 avoid 를 못 받는다. */ test('CAMP-CROSSINGS-STEPPER-IDENTITY: 한 칸씩 굴려도 같은 크로싱이 나온다', () => { const CAMPC = require('./campaign.js'); const csrc = fs.readFileSync(path.join(__dirname, 'campaign.js'), 'utf8'); // (1) 유일 구현 const at = csrc.indexOf('function parkCrossings('); assert.ok(at >= 0, 'campaign.js 에서 parkCrossings 를 못 찾았다'); let d = 0, body = ''; for (let j = csrc.indexOf('{', at); j < csrc.length; j++) { if (csrc[j] === '{') d++; else if (csrc[j] === '}' && --d === 0) { body = csrc.slice(at, j + 1); break; } } assert.ok(body.indexOf('PARK_CROSSINGS.map') === -1, 'parkCrossings 가 아직 스스로 map 한다 — 구현이 둘이면 언젠가 갈라진다'); assert.ok(body.indexOf('parkCrossingsStepper') !== -1, 'parkCrossings 가 스테퍼를 안 쓴다'); // (2) picks 체이닝 const L = CAMPC.PARK_CROSSINGS.filter(c => c.id === 'x4' || c.id === 'x7'); assert.strictEqual(L.length, 2, 'x4/x7 을 로스터에서 못 찾았다 — 게이트가 겨눌 곳을 잃었다'); assert.strictEqual(L[1].avoidArchOf, 'x4', 'x7 이 더 이상 x4 를 피하지 않는다 — picks 체이닝을 재는 이 게이트는 무효다'); const SEED = 12345; const s = CAMPC.parkCrossingsStepper(SEED, L); assert.strictEqual(s.total, 2, `total 이 ${s.total} 이다 — 2 여야 한다`); let guard = 0; while (s.step()) { if (++guard > 10) assert.fail('step() 이 안 끝난다'); } assert.strictEqual(s.done, 2, `done 이 ${s.done} 이다 — 2 여야 한다`); const oneShot = CAMPC.parkCrossingsStepper(SEED, L); const ref = []; while (oneShot.step()) ref.push(oneShot.result[oneShot.done - 1]); assert.deepStrictEqual(s.result, ref, '한 칸씩 굴린 결과가 다르다 — picks 가 step() 사이에 안 이어진다(x7 이 x4 를 못 피했다)'); assert.strictEqual(s.result[0].slot, 'x4'); assert.strictEqual(s.result[1].slot, 'x7'); }); /* ---------------- CAMP-DEFER-CROSSINGS-IS-OPT-IN ------------------------ 부팅 게이지는 크로싱 23칸을 프레임에 걸쳐 굴린다. 그러려면 createRun 이 크로싱 없이 돌아올 수 있어야 하는데, 그 플래그가 LIVE_OPTS 로 새면 agent_harness(232행)와 기존 게이트(14738·15395행)가 크로싱 없는 런을 받는다. 그리고 빈 값은 [] 여야 한다 — null 이면 runParkCrossing/_parkLiveCrossings 의 || 폴백이 걸려 게이지 뒤에 숨은 16초 동기 재계산이 생긴다. */ test('CAMP-DEFER-CROSSINGS-IS-OPT-IN: 미루기는 start() 전용이고 빈 값은 []다', () => { const CAMPD = require('./campaign.js'); const OPTS = CAMPD.LIVE_OPTS || {}; assert.ok(!('deferCrossings' in OPTS), 'LIVE_OPTS 에 deferCrossings 가 있다 — 헤드리스 프로브와 기존 게이트가 오염된다'); const SEED = 4242; const full = CAMPD.createRun({ ...OPTS, seed: SEED }); assert.strictEqual(full.park.crossings.length, CAMPD.PARK_CROSSINGS.length, `기본 런의 crossings 가 ${full.park.crossings.length} 칸이다 — 로스터 전부여야 한다`); const lazy = CAMPD.createRun({ ...OPTS, seed: SEED, deferCrossings: true }); assert.ok(Array.isArray(lazy.park.crossings), 'deferCrossings 런의 crossings 가 배열이 아니다 — null 이면 || 폴백이 걸려 숨은 16초가 생긴다'); assert.strictEqual(lazy.park.crossings.length, 0, 'deferCrossings 인데 크로싱이 계산됐다 — 미룬 게 아니다'); // park 의 나머지는 그대로 — 미루는 것은 크로싱 하나다. assert.strictEqual(lazy.park.demoSeed, full.park.demoSeed); assert.strictEqual(lazy.park.gameSeed, full.park.gameSeed); assert.deepStrictEqual(lazy.park.persona, full.park.persona); assert.deepStrictEqual(lazy.park.demo, full.park.demo); assert.strictEqual(lazy.park.tasks.length, full.park.tasks.length); // 미룬 창에서 두 소비처는 조용히 빈 결과를 낸다(폴백이 안 걸린다). 우연이 아니라 // 기록된 성질이다 — Task 5 는 이 창에 어떤 라우트도 못 닿게 하는 것으로 막는다. assert.deepStrictEqual(CAMPD._parkLiveCrossings(lazy), [], '_parkLiveCrossings 가 빈 배열이 아니다 — 폴백이 걸려 숨은 재계산이 돈다'); assert.strictEqual(CAMPD.runParkCrossing(lazy, 'cx:y20'), null, 'runParkCrossing 이 빈 목록에서 무언가를 찾았다'); }); /* ---------------- BOOT-GAUGE-COUNTS-REAL-WORK --------------------------- 로딩 바가 세는 것은 실제 작업이어야 한다. 총량을 상수 24 로 박으면 로스터가 늘어나는 날 바가 조용히 거짓말한다. 총량은 스테퍼가 스스로 말한 total 에서 유도돼야 한다. +1 은 createRun 자신의 틱이다 — 크로싱을 미뤄도 보드 두 장에 실측 ~1.1초가 들고, 그 시간을 0/N 에 세워 두면 바가 멈춘 것으로 보인다. */ test('BOOT-GAUGE-COUNTS-REAL-WORK: 게이지 총량은 로스터에서 유도된다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const brace = (head) => { const at = src.indexOf(head); assert.ok(at >= 0, `app.js 에서 "${head}" 를 못 찾았다`); let d = 0; for (let j = src.indexOf('{', at); j < src.length; j++) { if (src[j] === '{') d++; else if (src[j] === '}' && --d === 0) return src.slice(at, j + 1); } assert.fail(`"${head}" 를 brace-match 하지 못했다`); }; const total = new Function(brace('function _bootGaugeTotal(') + '; return _bootGaugeTotal;')(); assert.strictEqual(total({ total: 5 }), 6, '5칸 스테퍼의 총량이 6 이 아니다'); assert.strictEqual(total({ total: 23 }), 24, '23칸 스테퍼의 총량이 24 가 아니다'); assert.strictEqual(total({ total: 40 }), 41, '총량이 스테퍼를 안 따라간다 — 상수로 박혀 있으면 로스터가 늘 때 바가 거짓말한다'); const sBody = brace('function start()'); assert.ok(sBody.indexOf('deferCrossings') !== -1, 'start() 가 크로싱을 안 미룬다 — 12초가 여전히 통짜 정지다'); assert.ok(sBody.indexOf('parkCrossingsStepper') !== -1, 'start() 가 스테퍼를 안 굴린다'); const html = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf8'); const stageAt = html.indexOf('
'); assert.ok(stageAt >= 0, 'index.html 에 #stage 가 없다'); assert.ok(html.indexOf('id="bootbar"') > stageAt, '#bootbar 가 #stage 안에 없다 — 보드 상단에 못 붙는다'); const css = fs.readFileSync(path.join(__dirname, 'style.css'), 'utf8'); assert.ok(/#bootbar\s*\{[^}]*top:\s*0/.test(css), '#bootbar 가 상단(top:0)에 고정돼 있지 않다'); }); /* ---------------- BOOT-START-REENTRANCY-GUARDED --------------------------- critical fix (2026-08-05, 리뷰 라운드 1). 옛 start() 는 완전 동기라 재진입이 물리적으로 불가능했다. Task 5 가 부팅을 프레임에 걸쳐 펴면서 bootGaugeShow~bootGaugeHide 사이에 클릭 가능한 몇 초 창(칸 비용 410ms~5,761ms)이 열렸는데, board 의 click/keydown과 startBtnRoute() 세 진입점은 여전히 "start() 는 재진입하지 않는다"를 전제로 짜여 있었다. 그 창에서 ▶ 를 다시 누르거나 보드를 클릭하면: (1) 두 번째 부팅이 첫 번째의 G.campaign 을 자기 시드로 덮어써 crossings 의 "run seed 의 순수 함수(C1)" 불변식이 깨지고, (2) after(400, parkBootRoute) 가 중복 예약된다. 게다가 pump 안에서 던지면 아무도 못 잡아 게이지가 박제되고 _bootGauge 가 영영 안 풀려 재진입 가드까지 죽는 데드엔드가 됐다(옛 동기 코드는 예외 시 G.campaign 이 null 로 남아 다음 입력이 재시도할 수 있었다 — 그 점에서 더 나빴다). 정적 grep 은 문자열 존재만 재고 실제 재진입/실패 시나리오를 안 잰다(C11 부분문자열 함정). 그래서 이 게이트는 app.js 에서 start()/clearTimers()/_bootFail()/네 부팅 게이지 헬퍼의 실제 소스를 brace-match 로 뽑아, 최소 스텁(document/window/G/C/after/setHint/draw/ parkBootRoute) 으로 감싼 새 Function 안에서 진짜로 실행해 잰다. after(ms,fn) 을 fn() 즉시 호출로 접어 프레임 분할을 결정적 동기 실행으로 만든다 — async 대기 없이 실제 재진입 타이밍(부팅이 한창 진행 중인 순간)을 재현할 수 있다. */ test('BOOT-START-REENTRANCY-GUARDED: 부팅 중 재진입은 no-op, 실패는 재시도 가능하게 되돌린다', () => { const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8'); const brace = (head) => { const at = src.indexOf(head); assert.ok(at >= 0, `app.js 에서 "${head}" 를 못 찾았다`); let d = 0; for (let j = src.indexOf('{', at); j < src.length; j++) { if (src[j] === '{') d++; else if (src[j] === '}' && --d === 0) return src.slice(at, j + 1); } assert.fail(`"${head}" 를 brace-match 하지 못했다`); }; const startSrc = brace('function start()'); const clearTimersSrc = brace('function clearTimers()'); const totalSrc = brace('function _bootGaugeTotal('); const showSrc = brace('function bootGaugeShow('); const tickSrc = brace('function bootGaugeTick('); const hideSrc = brace('function bootGaugeHide('); const failSrc = brace('function _bootFail('); // 시나리오 하나를 완전히 새로운 Function 스코프에서 처음부터 실행한다(시나리오 간 상태 // 오염 없음). failAt: 0=정상 완주(mid-pump 재진입 프로브 포함), 1=틱1에서 던짐, 2=pump 에서 // 던짐(크로싱 두 칸 중 두 번째에서). function runScenario(failAt) { const harnessSrc = ` let _bootGauge = null; ${totalSrc} ${showSrc} ${tickSrc} ${hideSrc} ${clearTimersSrc} let G = { timers: [] }; const after = (ms, fn) => fn(); // 프레임 분할을 동기로 접어 재진입 타이밍을 결정적으로 잰다 const window = {}; // requestAnimationFrame 없음 -> frame() 이 after 로 폴백 const bootbarEl = { classList: { on: false, add() { this.on = true; }, remove() { this.on = false; } }, firstElementChild: { style: { width: '0%' } } }; const document = { getElementById: (id) => id === 'bootbar' ? bootbarEl : null }; let routeCalls = 0; function setHint() {} function draw() {} function parkBootRoute() { routeCalls++; } ${failSrc} let createRunCalls = 0; let reentryProbed = false, reentryCreateRunDelta = null; const FAIL_AT = ${failAt}; const C = { _isOrderingCycle: () => true, // 첫 후보에서 바로 사이클 확정 -> 스캔 1회 createRun(opts) { createRunCalls++; if (FAIL_AT === 1) throw new Error('boom-tick1'); return { seed: opts.seed, ruleSet: {}, park: { crossings: [] } }; }, parkCrossingsStepper(seed) { let done = 0; const total = 2; const result = []; return { total, result, step() { if (FAIL_AT === 2 && done === 1) throw new Error('boom-pump'); if (done >= total) return false; // 재진입 프로브: pump 가 첫 칸을 굴린 시점 = "부팅이 한창 진행 중"을 흉내낸다. // 실제로는 사용자가 이 창에서 board 를 클릭하거나 ▶ 를 다시 누르는 순간이다. if (FAIL_AT === 0 && !reentryProbed && done === 0) { reentryProbed = true; const before = createRunCalls; start(); reentryCreateRunDelta = createRunCalls - before; } done++; result.push({ id: 'x' + done }); return true; } }; }, }; ${startSrc} globalThis.__SCEN__ = () => ({ createRunCalls, routeCalls, reentryCreateRunDelta, bootGaugeAfter: _bootGauge, campaignAfter: G.campaign, }); start(); `; new Function(harnessSrc)(); const out = globalThis.__SCEN__; delete globalThis.__SCEN__; return out(); } // (a) 정상 완주 + mid-pump 재진입 프로브: 부팅이 한창 진행 중일 때 start() 를 다시 부르면 // 완전한 no-op 이어야 한다(clearTimers/G 리셋/createRun 없음), 그리고 정상 완주 쪽은 // parkBootRoute 가 정확히 한 번만 불려야 한다(중복 예약 없음). const ok = runScenario(0); assert.strictEqual(ok.reentryCreateRunDelta, 0, '부팅이 한창 진행 중일 때 start() 를 다시 불렀더니 C.createRun 이 더 불렸다 — 재진입이 ' + '막히지 않는다(두 번째 부팅이 첫 번째의 G.campaign 을 자기 시드로 덮어쓸 수 있다)'); assert.strictEqual(ok.routeCalls, 1, 'parkBootRoute 가 정확히 한 번이 아니다 — after(400, parkBootRoute) 가 중복 예약됐다'); assert.strictEqual(ok.bootGaugeAfter, null, '정상 완주 뒤 _bootGauge 가 안 풀렸다'); // (b) 틱 1 실패 -> 재시도 가능해야 한다(옛 동기 코드가 갖던 성질) const failTick1 = runScenario(1); assert.strictEqual(failTick1.bootGaugeAfter, null, '틱 1 이 던졌는데 _bootGauge 가 안 풀렸다 — 다음 입력이 영원히 no-op 이다(데드엔드)'); assert.strictEqual(failTick1.campaignAfter, null, '틱 1 이 던졌는데 G.campaign 이 null 로 안 돌아왔다 — board click/keydown 의 ' + '"if (!G.campaign) start()" 재시도 경로가 안 열린다'); // (c) pump 실패(크로싱 도중) -> 역시 재시도 가능해야 한다 const failPump = runScenario(2); assert.strictEqual(failPump.bootGaugeAfter, null, 'pump 이 던졌는데 _bootGauge 가 안 풀렸다 — 데드엔드(옛 동기 코드보다 나쁘다)'); assert.strictEqual(failPump.campaignAfter, null, 'pump 이 던졌는데 G.campaign 이 null 로 안 돌아왔다 — 재시도 경로가 안 열린다'); }); /* ---------------- PUSH-DEMO-FORK-FINGERPRINT --------------------------- _parkPushBuild 는 네 슬롯(xs·y23·y50·y58)의 시연 보드를 만든다. y50 에 rivalPads 포크를 심을 때 나머지 셋이 같이 움직이면 세 자리의 측정치가 한꺼번에 무효가 된다. CROSS-DEMO-SEAM 이 그 일을 하기로 돼 있었지만 그 게이트는 슬롯 집합 단언에서 먼저 죽어 바이트 비교에 도달하지 못한다(base 에서 red 확인). PARK-YARD-FROZEN 과 같은 실패 모드다 — 값 비교 앞의 집합 단언이 값 비교를 영원히 가린다. 그래서 세 슬롯의 시연 셀 지문을 여기에 직접 핀으로 박는다. y50 은 의도적으로 바뀌므로 목록에 없다. */ test('PUSH-DEMO-FORK-FINGERPRINT: y50 포크가 xs·y23·y58 시연으로 새지 않는다', () => { const crypto = require('crypto'); const CAMPF = require('./campaign.js'); const PUSH_DEMO_FP = { xs: '695968ce7e54366c', y23: '843506757fe092a3', y58: 'c47c1ba49493b2fc' }; const stable = (v) => { if (v instanceof Set) return 'S[' + [...v].sort((a, b) => a - b).join(',') + ']'; if (Array.isArray(v)) return '[' + v.map(stable).join(',') + ']'; if (v && typeof v === 'object') return '{' + Object.keys(v).sort().map(k => k + ':' + stable(v[k])).join(',') + '}'; return String(v); }; const SEED = 12345; for (const id of Object.keys(PUSH_DEMO_FP)) { const cx = CAMPF.PARK_CROSSINGS.find(c => c.id === id); assert.ok(cx, `${id} 을 로스터에서 못 찾았다 — 이 핀은 더 이상 아무것도 안 지킨다`); assert.strictEqual(cx.demoMech.moveMech, 'push', `${id} 이 더 이상 push 시연이 아니다 — 포크 누수를 잴 이유가 사라졌다`); const dSeed = (SEED * 61 + 11 + cx.si * 197) >>> 0; const cell = CAMPF._parkCrossingDemoCell(cx.kind, cx.demoMech, dSeed, cx.demoHaz); const fp = crypto.createHash('sha256').update(stable(cell)).digest('hex').slice(0, 16); assert.strictEqual(fp, PUSH_DEMO_FP[id], `${id} 의 시연 셀이 바뀌었다 (${PUSH_DEMO_FP[id]} -> ${fp}) — y50 포크가 공유 모듈로 샜다`); } }); /* ---------------- Y20-SEED-BOARD-FROZEN -------------------------------- _parkBombBlocked / _parkBombTargetKey 는 가이드 판(cell.guidedBomb)과 시드 보드가 공유하는 함수다. 가이드 판을 고치다 시드 보드가 같이 움직이면 승격된 보드가 조용히 오염된다. 스펙 Risk 2 는 "변경마다 재확인한다"고 적지만 사람이 잊으면 그만이다. 지문을 직접 박아 잊을 수 없게 만든다. */ test('Y20-SEED-BOARD-FROZEN: 가이드 판 개조가 시드 보드로 새지 않는다', () => { const crypto = require('crypto'); const Y20_SEED_FP = { 3: '1c933f91edb9c863', 7: 'cce44f31142deef8', 11: 'b97277d8674642b2' }; const stable = (v) => { if (v instanceof Set) return 'S[' + [...v].sort((a, b) => a - b).join(',') + ']'; if (v instanceof Map) return 'M[' + [...v.entries()].sort().map(e => e[0] + ':' + stable(e[1])).join(',') + ']'; if (Array.isArray(v)) return '[' + v.map(stable).join(',') + ']'; if (v && typeof v === 'object') return '{' + Object.keys(v).sort().map(k => k + ':' + stable(v[k])).join(',') + '}'; return String(v); }; for (const seed of Object.keys(Y20_SEED_FP)) { const st = E._parkBombBuild({ seed: Number(seed) }); // guidedBomb 없음 = 시드 보드 const fp = crypto.createHash('sha256').update(stable(st)).digest('hex').slice(0, 16); assert.strictEqual(fp, Y20_SEED_FP[seed], `시드 ${seed} 의 보드가 바뀌었다 (${Y20_SEED_FP[seed]} -> ${fp}) — 가이드 판 개조가 ` + '시드 보드와 공유하는 _parkBombBlocked/_parkBombTargetKey 를 통해 샜다'); } assert.strictEqual(E.PARK_BOMB_SHIPPABLE, true, 'PARK_BOMB_SHIPPABLE 이 움직였다 — 이 계획은 승격 재판단이 아니다(스펙 Non-Goals)'); }); /* ---- GATE: Y20-GEM-POCKET — 포켓의 출입구는 정확히 둘이다 (2026-08-05, Task 22 보석 포켓 재배치) ---- 세 체인 보석 (7,2) (8,2) (7,3) 은 철로 둘러싸인 포켓 안에 있다. 포켓에 인접한 비-벽 칸이 정확히 (6,3)(나무 face, 심고 물러나는 지름길)과 (7,1)(열린 문, 다섯 걸음 우회) 둘이어야 "포켓엔 들어가는 길이 둘"이라는 무대의 첫 전제가 실제 기하로도 성립한다. 셋 이상이면 우회가 하나 더 생기고, 하나 뿐이면 우회가 아예 없다 — 둘 다 무대가 가르치려는 대비를 깬다. */ test('Y20-GEM-POCKET: the gem pocket has EXACTLY two entrances — the wooden face and the open door', () => { const st = E._parkBombBuild({ ...E._parkBombCell(3), guidedBomb: true }); const n = st.N, K = (x, y) => y * n + x, b = st.park.bomb; const pocket = new Set([0, 1, 2].map(i => K(st.tokens[i].x, st.tokens[i].y))); assert.deepStrictEqual([...pocket].sort((a, c) => a - c), [K(7, 2), K(8, 2), K(7, 3)].sort((a, c) => a - c), 'the gem pocket cells moved — this gate is stale'); const entrances = new Set(); for (const k of pocket) { const x = k % n, y = (k / n) | 0; for (const [dx, dy] of [[0, -1], [0, 1], [-1, 0], [1, 0]]) { const nx = x + dx, ny = y + dy; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (pocket.has(nk)) continue; if (!st.wall.has(nk)) entrances.add(nk); } } assert.deepStrictEqual([...entrances].sort((a, c) => a - c), [K(6, 3), K(7, 1)].sort((a, c) => a - c), `the pocket has ${entrances.size} entrance(s), not exactly two: ` + [...entrances].map(k => `(${k % n},${(k / n) | 0})`).join(' ')); }); /* ---- GATE: Y20-DETOUR-OR-BOMB — 우회는 살아 있고, 나무를 쓰면 물러나야 한다 (2026-08-05) ---- (a) DETOUR: 폭탄 없이 방 문턱 (5,3)에서 첫 보석 (7,2)까지 걸어갈 수 있고, 그 거리는 정확히 5걸음 (row 1 을 돌아가는 길)이다 — 우회가 실제로 존재한다는 것이 무대의 안전판이다. (b) BOMB: 나무 face (6,3)을 터뜨리면 그 폭발 크로스가 심은 자리인 (5,3) 자체를 덮는다 — 심고 한 칸도 안 물러나면 자기 폭발에 맞는다는 뜻이고, 그 위험이 지름길의 값이다(안전 비용의 근거). */ test("Y20-DETOUR-OR-BOMB: the no-bomb detour is 5 steps, and the wood's own blast cross covers the threshold", () => { const st = E._parkBombBuild({ ...E._parkBombCell(3), guidedBomb: true }); const n = st.N, K = (x, y) => y * n + x, b = st.park.bomb; // (a) the detour, walking only — no crate opened. const start = K(5, 3), seen = new Map([[start, 0]]), q = [start]; for (let h = 0; h < q.length; h++) { const k = q[h], x = k % n, y = (k / n) | 0; for (const [dx, dy] of [[0, -1], [0, 1], [-1, 0], [1, 0]]) { const nx = x + dx, ny = y + dy; if (nx < 0 || ny < 0 || nx >= n || ny >= n) continue; const nk = ny * n + nx; if (seen.has(nk) || st.wall.has(nk) || b.crates.has(nk)) continue; seen.set(nk, seen.get(k) + 1); q.push(nk); } } const g0 = K(st.tokens[0].x, st.tokens[0].y); assert.strictEqual(seen.get(g0), 5, `the no-bomb detour from the room threshold to gem 0 is ${seen.get(g0)} steps, not 5 — the detour moved`); // (b) the wooden face's own blast reaches back onto the threshold it is planted from. const cross = E._parkBombCross(st, b.face.gem); assert.ok(cross.has(K(5, 3)), "the wooden face's blast cross no longer covers the room threshold (5,3) — planting stopped being risky"); }); /* ---- GATE: Y20-PAIR-POSES — G-C 가 6인격 전원의 트라젝토리에서 포즈된다 (2026-08-05) ---- Task 20 의 반증 프로브는 PASS 했다(오라클이 우리를 나온다) — 이 게이트를 쓰는 조건. Task 24 의 attTarget 개편이 G-C 를 6인격 전부의 pairs 에 등장시켰다(리포트 표). needPairs 는 이 판에서 꺼져 있으므로(Y20-GUIDED-COMPLETES) P.posed 는 안 쌓인다 — §4 엔진 변경(park.readPairs 신설)은 안전하지만 불필요해서 넣지 않았다(P.posed 는 app.js/campaign.js 어디서도 0회 소비). 대신 campaign.js 가 실제로 쓰는 것과 같은 방식으로 parkPairExpressed 를 직접 불러 트라젝토리 상태 중 G-C 가 판별력을 가지는 지점의 수를 센다. `if (rec)` 로 감싸지 않는다 — 측정값을 그대로 단언한다. */ test('Y20-PAIR-POSES: G-C is expressed on every one of the 6 persona trajectories', () => { const counts = E.PARK_PERSONAS.map(persona => { const P = E.parkPlayout(E._parkBombBuild({ seed: 3, guidedBomb: true }), persona); return E.parkPairExpressed(E._parkBombBuild({ seed: 3, guidedBomb: true }), P.moves, ['G', 'C']); }); assert.deepStrictEqual(counts, [1, 1, 1, 1, 1, 1], `G-C expressed-state counts per persona shifted (measured 1/1/1/1/1/1 on 2026-08-05), got [${counts.join(',')}]`); const total = counts.reduce((a, c) => a + c, 0); assert.strictEqual(total, 6, `G-C is posed across ${total}/6 persona trajectories, not the full 6`); }); /* ---------- SRC-CONCAT-DRIFT: 소스 파트와 커밋된 아티팩트가 어긋나지 않는다 ---------- Stage 2(2026-08-06)에서 engine.js·app.js 는 engine.src/ · app.src/ 의 파트를 manifest.txt 순서대로 **순수 콘캣**한 산출물이 됐다. 사람이 파트를 고치고 빌드를 잊거나, 반대로 아티팩트만 손으로 고치면 두 표현이 갈라진다 — 그러면 다음 사람이 파트를 고쳐 빌드하는 순간 남의 수정이 조용히 사라진다. 이 게이트가 그 창을 닫는다. 이빨 다섯: ① 콘캣 == 아티팩트(바이트) ② 매니페스트가 디스크의 모든 파트를 덮는다 (고아 파트 = 조용한 누락) ③ 매니페스트가 없는 파일을 가리키지 않는다 ④ 매니페스트에 중복된 줄이 없다 ⑤ 매니페스트의 순서가 파일명 NNN- 접두의 오름차순과 일치한다. ⑤가 필요한 이유: 콘캣 순서의 진실은 매니페스트뿐이라, 두 줄을 접두와 어긋나게 바꿔 놓아도 빌드는 성공하고 ①~④도 전부 green 이다. 그러면 파일명이 순서를 조용히 거짓말한다. */ test('SRC-CONCAT-DRIFT(engine): engine.src 파트를 재콘캣하면 커밋된 engine.js 와 바이트가 같다', () => { const parts = require('./tools/lib/src-parts.js'); const listed = parts.readManifest(__dirname, 'engine.src'); const onDisk = parts.partFiles(__dirname, 'engine.src'); assert.ok(listed.length > 0, 'engine.src/manifest.txt 가 비어 있습니다'); const missing = listed.filter((f) => onDisk.indexOf(f) < 0); assert.deepStrictEqual(missing, [], `manifest 가 없는 파일을 가리킵니다: ${missing.join(', ')}`); const orphan = onDisk.filter((f) => listed.indexOf(f) < 0); assert.deepStrictEqual(orphan, [], `manifest 에 없는 고아 파트가 있습니다(빌드에서 조용히 빠집니다): ${orphan.join(', ')}`); const dup = listed.filter((f, i) => listed.indexOf(f) !== i); assert.deepStrictEqual(dup, [], `manifest 에 중복된 파트가 있습니다: ${dup.join(', ')}`); // ⑤ 매니페스트 순서 == 파일명 사전순. 중복이 없음을 위에서 이미 단언했으므로, // "이웃한 역전이 하나도 없다"는 것과 "전체가 정렬돼 있다"는 것은 같은 말입니다. // 역전을 찾으면 어긋난 **두 줄을 이름으로** 지목합니다 — 배열 26개를 통째로 덤프하면 // 어디가 문제인지 사람이 못 읽습니다. let inv = -1; for (let i = 0; i + 1 < listed.length; i++) if (listed[i] > listed[i + 1]) { inv = i; break; } assert.ok(inv < 0, inv < 0 ? '' : `engine.src/manifest.txt 의 순서가 파일명 오름차순과 어긋납니다 — ` + `${inv + 1}번째 줄 '${listed[inv]}' 뒤에 '${listed[inv + 1]}' 가 옵니다. ` + `콘캣 순서의 진실은 매니페스트뿐이고 파일명 앞 NNN- 접두는 사람이 그 순서를 눈으로 읽는 ` + `유일한 단서라, 둘이 어긋나면 파일명이 순서를 거짓말합니다. 순서를 정말 바꿔야 한다면 ` + `파일명 접두도 함께 바꿔 다시 오름차순으로 맞추세요.`); const built = parts.concatParts(__dirname, 'engine.src'); const artifact = fs.readFileSync(path.join(__dirname, 'engine.js')); let at = 0; const lim = Math.min(built.length, artifact.length); while (at < lim && built[at] === artifact[at]) at++; assert.ok( built.equals(artifact), `engine.src 재콘캣(${built.length} bytes)과 engine.js(${artifact.length} bytes)가 다릅니다 — ` + `첫 불일치 offset ${at}. \`npm run build:src\` 를 잊었거나 아티팩트를 손으로 고쳤습니다.` ); console.log(` [SRC-CONCAT-DRIFT] engine.js = ${listed.length} parts, ${built.length} bytes, byte-identical`); }); test('SRC-CONCAT-DRIFT(app): app.src 파트를 재콘캣하면 커밋된 app.js 와 바이트가 같다', () => { const parts = require('./tools/lib/src-parts.js'); const listed = parts.readManifest(__dirname, 'app.src'); const onDisk = parts.partFiles(__dirname, 'app.src'); assert.ok(listed.length > 0, 'app.src/manifest.txt 가 비어 있습니다'); const missing = listed.filter((f) => onDisk.indexOf(f) < 0); assert.deepStrictEqual(missing, [], `manifest 가 없는 파일을 가리킵니다: ${missing.join(', ')}`); const orphan = onDisk.filter((f) => listed.indexOf(f) < 0); assert.deepStrictEqual(orphan, [], `manifest 에 없는 고아 파트가 있습니다(빌드에서 조용히 빠집니다): ${orphan.join(', ')}`); const dup = listed.filter((f, i) => listed.indexOf(f) !== i); assert.deepStrictEqual(dup, [], `manifest 에 중복된 파트가 있습니다: ${dup.join(', ')}`); // ⑤ engine 게이트와 같은 이빨입니다(왜 필요한지는 위 주석 블록을 보세요). let inv = -1; for (let i = 0; i + 1 < listed.length; i++) if (listed[i] > listed[i + 1]) { inv = i; break; } assert.ok(inv < 0, inv < 0 ? '' : `app.src/manifest.txt 의 순서가 파일명 오름차순과 어긋납니다 — ` + `${inv + 1}번째 줄 '${listed[inv]}' 뒤에 '${listed[inv + 1]}' 가 옵니다. ` + `콘캣 순서의 진실은 매니페스트뿐이고 파일명 앞 NNN- 접두는 사람이 그 순서를 눈으로 읽는 ` + `유일한 단서라, 둘이 어긋나면 파일명이 순서를 거짓말합니다. 순서를 정말 바꿔야 한다면 ` + `파일명 접두도 함께 바꿔 다시 오름차순으로 맞추세요.`); const built = parts.concatParts(__dirname, 'app.src'); const artifact = fs.readFileSync(path.join(__dirname, 'app.js')); let at = 0; const lim = Math.min(built.length, artifact.length); while (at < lim && built[at] === artifact[at]) at++; assert.ok( built.equals(artifact), `app.src 재콘캣(${built.length} bytes)과 app.js(${artifact.length} bytes)가 다릅니다 — ` + `첫 불일치 offset ${at}. \`npm run build:src\` 를 잊었거나 아티팩트를 손으로 고쳤습니다.` ); console.log(` [SRC-CONCAT-DRIFT] app.js = ${listed.length} parts, ${built.length} bytes, byte-identical`); }); /* ---------- PIN-LAZY: 승격 핀은 로드 시점에 파생되지 않는다 ---------------- campaign.js 를 require 하는 비용의 93%(7.68초 중 7.11초)가 최상위 승격 핀 9개의 즉시 평가였다(2026-08-06 실측: 9줄을 false 로 치환하면 0.57초). 계기판의 워커는 그 값을 한 번도 읽지 않으면서 워커마다 그 값을 치렀다. 핀은 이제 메모이즈된 지연 게터이고, 이 게이트는 그것이 최상위로 되돌아가는 것을 막는다. derive-never-assert 는 그대로다 — 여전히 파생하고, *언제* 파생하는지만 바뀌었다. */ test('PIN-LAZY: campaign.js 최상위에 승격 핀의 즉시 파생이 남아 있지 않다', () => { const src = fs.readFileSync(path.join(__dirname, 'campaign.js'), 'utf8'); const eager = src.split('\n') .map((l, i) => [i + 1, l]) .filter(([, l]) => /^const\s+PARK_Y\d+_SHIPPABLE\s*=\s*_parkY\d+Recovers\s*\(\s*\)\s*;/.test(l)); assert.deepStrictEqual(eager.map(([n, l]) => `${n}: ${l.trim()}`), [], '최상위에서 즉시 파생되는 승격 핀이 있습니다 — 지연 게터로 옮기세요. ' + 'require 비용이 워커마다 7초씩 붙습니다.'); console.log(' [PIN-LAZY] campaign.js 최상위 즉시 파생 0건'); }); /* ---------- PIN-LAZY-EQUIV: 지연 핀의 값은 다시 파생해도 같다 ---------------- 지연이 옮기는 것은 *언제* 파생하느냐다. 파생 함수가 모듈 수준의 가변 상태(캐시, fallback 카운터)에 의존한다면 "로드 직후에 잰 값"과 "다른 일을 한 뒤에 잰 값"이 달라질 수 있고, 그러면 이 리팩터는 조용히 측정을 바꾼 것이 된다. 이 게이트는 park 작업을 먼저 돌려 공유 상태를 흔든 다음, 게터가 돌려주는 값과 _parkY*Recovers() 를 **그 시점에 다시 부른** 값이 같은지 본다. 다르면 지연이 문제가 아니라 파생이 순수하지 않다는 발견이고, 그건 즉시 평가 시절에도 숨어 있던 것이다. */ test('PIN-LAZY-EQUIV: 지연 승격 핀 9개는 나중에 다시 파생해도 같은 값이다', () => { const PINS = [ ['PARK_Y23_SHIPPABLE', '_parkY23Recovers'], ['PARK_Y24_SHIPPABLE', '_parkY24Recovers'], ['PARK_Y17_SHIPPABLE', '_parkY17Recovers'], ['PARK_Y8_SHIPPABLE', '_parkY8Recovers'], ['PARK_Y56_SHIPPABLE', '_parkY56Recovers'], ['PARK_Y55_SHIPPABLE', '_parkY55Recovers'], ['PARK_Y46_SHIPPABLE', '_parkY46Recovers'], ['PARK_Y53_SHIPPABLE', '_parkY53Recovers'], ['PARK_Y52_SHIPPABLE', '_parkY52Recovers'], ]; // ① 공유 상태를 흔든다 — 크로싱 보드를 짓고 여섯 인격을 걸린다. const cx = CAMP.PARK_CROSSINGS[0]; const dSeed = (1 * 61 + 11 + cx.si * 197) >>> 0; const pSeed = (1 * 89 + 23 + cx.si * 211) >>> 0; const demoCell = CAMP._parkCrossingDemoCell(cx.kind, cx.demoMech, dSeed, cx.demoHaz); const play = CAMP._parkCrossingPlayCell(cx.kind, demoCell, cx.playMech, pSeed, cx.playHaz, undefined, true); let stirred = 0; if (play.filtered === true) { for (const persona of E.PARK_PERSONAS) { E.parkPlayout(CAMP._parkCrossBoard(cx.kind, play.cell), persona); stirred++; } } // 자물쇠: 흔들기가 정말 일어났는지. 위 if 는 로스터 첫 슬롯의 플레이 셀이 인콘그루언스 // 필터를 넘을 때만 참이다. 언젠가 넘지 못하게 되면 이 게이트는 아무 신호 없이 "핀을 두 번 // 읽는다"로 퇴화하고, 이 바가 서 있는 전제("다른 일을 한 뒤에 잰 값")가 통째로 사라진다. assert.strictEqual(stirred, E.PARK_PERSONAS.length, `공유 상태 흔들기가 ${stirred}회 돌았습니다(기대 ${E.PARK_PERSONAS.length}회 = 인격 수) — ` + `로스터 첫 슬롯 ${cx.id} 의 플레이 셀이 인콘그루언스 필터를 넘지 못했습니다(play.filtered=${play.filtered}). ` + `이 게이트는 "park 작업을 먼저 돌려 공유 상태를 흔든 뒤"를 전제로 하므로, ` + `필터를 넘는 슬롯으로 cx 를 바꾸세요(AA_SEEDS=1 node tools/roster-bars.mjs 의 '필터 1/1' 열).`); // ② 그 뒤에 게터를 읽고, 같은 시점에 파생 함수를 직접 불러 비교한다. const drift = []; for (const [pin, fn] of PINS) { assert.strictEqual(typeof CAMP[fn], 'function', `${fn} 가 export 되어 있지 않습니다`); const got = CAMP[pin]; assert.strictEqual(typeof got, 'boolean', `${pin} 가 boolean 이 아닙니다: ${typeof got}`); const fresh = CAMP[fn](); if (got !== fresh) drift.push(`${pin}: 게터 ${got} vs 재파생 ${fresh}`); } assert.deepStrictEqual(drift, [], '지연 핀의 값이 재파생과 다릅니다 — 파생이 평가 시점(공유 캐시·카운터)에 의존합니다: ' + drift.join(', ')); // ③ 두 번째 읽기가 메모이즈되는지 — 게터가 매번 7초를 다시 물면 지연의 이득이 사라진다. // // 이 한 줄은 "판정은 상태 한계(결정적) 1순위, 시간 한계는 안전망"이라는 이 저장소의 규칙에 // 대한 **의도된 예외**다. 다시 유도하지 않아도 되도록 판단 근거를 적어 둔다: // · 결정적 대안은 파생 호출 횟수를 세는 것인데, 그러려면 campaign.js 안에 계측을 심어야 // 한다. 이 단계의 계약이 "성능만 바꾼다"이므로 그 계측이 계약을 깬다. // · 위 ②의 드리프트 단언(게터 값 vs 그 시점 재파생)이 **결정적 바로 그대로 살아 있다** — // 시간은 ②를 대신하는 것이 아니라 ②가 못 보는 "매번 다시 파생" 하나만 본다. // · 마진이 크다: 실측 0ms 대 한계 200ms, 메모이즈가 없을 때의 웜 재파생 합계는 1,191ms // (2026-08-06, 핀 9개 웜 1회). 6배 여유라 부하로 흔들리는 종류의 판정이 아니다. const t0 = Date.now(); for (const [pin] of PINS) void CAMP[pin]; const ms = Date.now() - t0; assert.ok(ms < 200, `메모이즈된 두 번째 읽기가 ${ms}ms 걸렸습니다 — 게터가 매번 다시 파생하고 있습니다`); console.log(` [PIN-LAZY-EQUIV] 핀 9개 재파생 일치, 두 번째 읽기 ${ms}ms`); }); /* ---------- TRIAGE-SEED-LIST: 시드 리스트로 나눠 재도 한 번에 잰 것과 같다 --------- 미리보기 트리아지는 시드 루프를 자기 안에 갖고 있어서 (슬롯 × 시드) 로 분배할 수 없었다. seeds 인자가 배열을 받으면 워커 하나가 시드 하나를 재고 드라이버가 합칠 수 있다. 이 게이트는 "나눠 재고 합친 것"이 "한 번에 잰 것"과 같은지를 잰다 — 합산이 가능하다는 주장이 바로 이 도구의 정확성이기 때문이다. 픽스처가 하나면 이 바는 반쯤 공허하다. 로스터 첫 슬롯은 시드 1..4 에서 tied 분기가 한 번도 참이 되지 않아 undecidedPairs 가 늘 0쌍이고, 그러면 pairGap 누계를 통째로 0 으로 바꿔도 deepStrictEqual 이 통과한다 — 이 태스크가 새로 낸 바로 그 필드의 합산이 증명되지 않는다. 그래서 비-0 인 슬롯을 하나 더 돈다: 0 경계와 비-0 경계를 둘 다 잡는다. 합치는 함수는 여기 있지 않다 — CAMP._parkTriageMerge 다. 부르는 곳이 이 게이트(CJS)와 tools/preview-sweep.mjs(ESM) 둘인데 모듈 체계가 달라 tools/lib/ 로는 게이트가 못 닿으므로, 둘의 공통 조상인 campaign.js 에 한 벌만 둔다. 사본이 두 벌이면 트리아지가 비-가산 필드를 늘리는 날 게이트만 red 가 되고, 게이트 쪽 사본만 고쳐 green 을 얻은 사람이 드라이버의 틀린 표를 조용히 남긴다. 그래서 이 게이트가 재는 것은 **드라이버가 실제로 쓰는 바로 그 함수**다. */ test('TRIAGE-SEED-LIST: _parkPreviewTriage 를 시드별로 나눠 합치면 통째로 잰 것과 같다', () => { assert.strictEqual(typeof CAMP._parkTriageMerge, 'function', '_parkTriageMerge 가 export 되어 있지 않습니다 — 이 게이트가 재는 것은 드라이버가 쓰는 ' + '바로 그 병합 함수입니다. 여기서 사본을 다시 쓰지 마세요(사본이 두 벌이면 이 바가 ' + 'tools/preview-sweep.mjs 를 더는 증명하지 못합니다).'); // 두 픽스처. NONVACUOUS 는 짐작이 아니라 실측으로 골랐다 — 로스터 23슬롯을 시드 1..4 로 // 훑으면 undecidedPairs>0 인 슬롯은 일곱이고(x5 4쌍·y50 28쌍·y12 24쌍·y14 24쌍·y23 29쌍· // y24 24쌍·y10 34쌍), 그 중 y10 이 가장 크며 byPair 도 한 종류(y12 는 CN 24 뿐)가 아니라 // GN 18·CN 16 두 종류에 걸친다. 벽시계도 y10 1.9초 대 y12 9.3초로 싸다. const FIXTURES = [CAMP.PARK_CROSSINGS[0].id, 'y10']; const NONVACUOUS = 'y10'; // 자물쇠 ①: 픽스처가 로스터에서 사라지면 _parkPreviewTriage 가 "no crossing slot 'y10'" 으로 // 죽는데, 그 메시지는 읽는 사람에게 무엇을 하라는 말인지 알려 주지 않는다(TRIAGE-DRIFT · // TRIAGE-PARTITION · TRIAGE-DISCRIMINATES 셋이 y26·y27 로 정확히 그렇게 죽어 있다). assert.ok(CAMP.PARK_CROSSINGS.some(c => c.id === NONVACUOUS), `비-0 픽스처 '${NONVACUOUS}' 가 로스터(PARK_CROSSINGS)에 없습니다 — 슬롯이 제거됐습니다. ` + `undecidedPairs > 0 인 슬롯을 하나 골라 NONVACUOUS 와 FIXTURES 를 함께 고치세요. ` + `고르는 법: node tools/preview-sweep.mjs 4 의 undecPairs/run 열이 0 이 아닌 슬롯. ` + `여기서 0 인 슬롯을 쓰면 이 바는 통과하면서 아무것도 증명하지 못합니다.`); const seen = []; for (const id of FIXTURES) { const whole = CAMP._parkPreviewTriage(id, 4); assert.strictEqual(whole.seeds, 4, `${id}: seeds 필드는 시드 개수를 담아야 합니다`); assert.strictEqual(typeof whole.undecidedPairs, 'number', `${id}: undecidedPairs 가 없습니다 — 시드별 결과를 합치려면 이 원자료가 필요합니다`); // 자물쇠 ②: 로스터가 바뀌어 이 픽스처가 조용히 0 이 되면, 아래 비교는 undecidedPairs 를 // 0=0 으로만 확인하게 되어 합산 로직이 통째로 틀려도 green 이 된다. if (id === NONVACUOUS) { assert.ok(whole.undecidedPairs > 0, `${id} 픽스처의 undecidedPairs 가 시드 1..4 에서 0쌍입니다 — 이 게이트가 공허해집니다 ` + '(pairGap 누계가 통째로 틀려도 통과). 비-0 인 슬롯으로 픽스처를 다시 고르세요.'); } const parts = [1, 2, 3, 4].map((s) => CAMP._parkPreviewTriage(id, [s])); // 드라이버(tools/preview-sweep.mjs)가 부르는 바로 그 함수. 가산·비-가산 규약이 전부 그 // 안에 있으므로 여기서 skip 이나 perRun 을 손보지 않는다 — 손보면 사본이 다시 생긴다. const merged = CAMP._parkTriageMerge(parts); assert.deepStrictEqual(merged, whole, `${id}: 시드별로 나눠 합친 트리아지가 통째로 잰 것과 다릅니다`); seen.push(`${id} undecidedPairs=${whole.undecidedPairs}쌍/${whole.runs}런` + `(byPair GC${whole.undecided.byPair.GC}·GN${whole.undecided.byPair.GN}·CN${whole.undecided.byPair.CN})`); } console.log(` [TRIAGE-SEED-LIST] 시드 1..4(4시드×6인격) 통째/따로 동일 — ${seen.join(', ')}`); });