// PROCITY CityGen self-check — `node web/js/citygen/selfcheck.js`. Plain node, zero deps. // Asserts the Lane A acceptance contract. Prints all-green or dies loud with the first failure. // // The overlap/facing helpers are IMPORTED from plan.js (lotCorners/obbOverlap) so the harness and // the generator can never disagree about what "overlap" or "facing" means. import { readFileSync, existsSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import { generatePlan, chunkIndex, lotCorners, obbOverlap, CHUNK } from './plan.js'; import { generatePlanOSM, osmTownKeys, validateTownCache, registerTownCache, MIN_TOWN_SHOPS, medianShopSpacing, MAX_MEDIAN_SPACING_M } from './plan_osm.js'; import { generatePlanFor, gigKeyFor, POSTER_CLEAR } from './index.js'; import { createAddresses, STREET_TOLERANCE_M } from './address.js'; import { allFacadeSkins, SHOP_TYPES, VENUE_KINDS, genreForVenueKind, vergeBand, DISTRICT_KINDS } from '../core/registry.js'; import { xmur3 } from '../core/prng.js'; const HERE = dirname(fileURLToPath(import.meta.url)); const ASSETS = join(HERE, '..', '..', 'assets', 'gen'); const STOCK_GODVERSE_DIR = join(HERE, '..', '..', 'assets', 'stock_godverse'); // G's tier-1 atlases (orphan check) let failures = 0, checks = 0; const ok = (cond, msg) => { checks++; if (!cond) { failures++; console.error(' ✗ ' + msg); } }; const section = s => console.log('\n' + s); const SEEDS = [20261990, 1, 42, 777, 2000000000, 8675309]; const SOLID = new Set(['shop', 'anchor', 'house', 'stall']); // lots realised as building shells by Lane B // ROUND27 THE FACADE FIX — B's canon is canonical: the VISIBLE facade is local +Z ⇒ world (sin ry, cos ry). // This helper used to return (−sin, −cos), the SIM/rig convention that CITY_SPEC:71 mis-wrote into the lots // contract. That is why nine rounds of goldens were green over a world where every shopfront faced away. const facing = l => [Math.sin(l.ry), Math.cos(l.ry)]; // THE CROSS-CONVENTION GATE (ROUND27 wave 5 — the lesson made law). // F proved the trap: measuring lot.ry against CITY_SPEC:71's own convention is SELF-AGREEMENT — it returned // 72/72 "facing" over a town of blank walls, because a self-consistent check over a self-contradicting spec // passes under either convention. The only check that can see the contradiction compares the two conventions // AGAINST EACH OTHER. So this does not restate a spec line: it replicates LANE B's OWN placement maths from // buildings.js (`toWorld(lot, 0, y, zf)` with `zf = d/2 + 0.02`) and asks a physical question the prose // cannot fudge — does the quad the RENDERER actually draws land nearer the street than the lot centre? // If either side drifts (A's ry sign, or B's zf sign), this fires. const facadeQuadWorld = l => { // buildings.js: toWorld(lot, 0, y, d/2 + 0.02) const cos = Math.cos(l.ry || 0), sin = Math.sin(l.ry || 0), zf = l.d / 2 + 0.02; return [l.x + zf * sin, l.z + zf * cos]; }; // nearest point on segment AB to point P function nearestOnSeg(px, pz, ax, az, bx, bz) { const dx = bx - ax, dz = bz - az, L2 = dx * dx + dz * dz || 1; let t = ((px - ax) * dx + (pz - az) * dz) / L2; t = Math.max(0, Math.min(1, t)); return [ax + dx * t, az + dz * t]; } const isFiniteNum = v => typeof v === 'number' && Number.isFinite(v); // The FULL structural invariant suite for ANY CityPlan — synthetic OR osm (round-8 parity: both // sources get identical coverage). `label` prefixes each check. Synthetic-only "brief" checks // (market/arcade/dept/milkbar) live at the call site, guarded, since a real town has none. function structuralSuite(plan, label) { const edgeIds = new Set(plan.streets.edges.map(e => e.id)); const nodeById = new Map(plan.streets.nodes.map(n => [n.id, n])); const nodeIds = new Set(plan.streets.nodes.map(n => n.id)); const blockIds = new Set(plan.blocks.map(b => b.id)); const lotIds = new Set(plan.lots.map(l => l.id)); const districtIds = new Set(plan.districts.map(d => d.id)); ok(plan.streets.nodes.every(n => isFiniteNum(n.x) && isFiniteNum(n.z)), `${label}: node coords finite`); ok(plan.lots.every(l => isFiniteNum(l.x) && isFiniteNum(l.z) && isFiniteNum(l.w) && isFiniteNum(l.d) && isFiniteNum(l.ry)), `${label}: lot numbers finite`); ok(plan.blocks.every(b => b.poly.every(p => isFiniteNum(p[0]) && isFiniteNum(p[1]))), `${label}: block poly finite`); ok(plan.streets.edges.every(e => nodeIds.has(e.a) && nodeIds.has(e.b)), `${label}: every edge references real nodes`); ok(plan.blocks.every(b => districtIds.has(b.district)), `${label}: every block in a real district`); ok(plan.lots.every(l => blockIds.has(l.block)), `${label}: every lot in a real block`); ok(plan.lots.every(l => edgeIds.has(l.frontEdge)), `${label}: every lot has a valid frontEdge`); ok(plan.lots.every(l => l.w > 0 && l.d > 0), `${label}: every lot has positive size`); ok(plan.shops.every(sh => lotIds.has(sh.lot)), `${label}: every shop has a real lot`); ok(plan.shops.every(sh => SHOP_TYPES[sh.type]), `${label}: every shop has a known type`); ok(plan.shops.every(sh => SHOP_TYPES[sh.type].facades.includes(sh.facadeSkin)), `${label}: every facade is in its type's pool`); ok(plan.shops.every(sh => sh.hours[0] >= 0 && sh.hours[1] <= 23 && sh.hours[1] > sh.hours[0]), `${label}: every shop has sane hours`); ok(plan.shops.every(sh => sh.name && sh.sign), `${label}: every shop is named`); ok(plan.shops.every(sh => !/[{}]|undefined/.test(sh.name + sh.sign)), `${label}: no unresolved tokens / undefined in names`); ok(plan.shops.every(sh => { const [mn, mx] = SHOP_TYPES[sh.type].storeys; const pmax = mx >= 2 ? Math.min(3, mx + 1) : mx; return sh.storeys >= mn && sh.storeys <= pmax; }), `${label}: storeys within registry range (corner-boost ≤ min(max+1,3); single-storey never boosted)`); const lotShopCounts = {}; for (const sh of plan.shops) lotShopCounts[sh.lot] = (lotShopCounts[sh.lot] || 0) + 1; ok(Object.values(lotShopCounts).every(c => c === 1), `${label}: no lot has two shops`); ok(plan.shops.every(sh => SOLID.has(plan.lots[sh.lot].use)), `${label}: every shop sits on a solid lot`); let backwards = null, drawnAway = null, worstGain = 0; for (const l of plan.lots) { const e = plan.streets.edges.find(x => x.id === l.frontEdge); const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue; const [nx, nz] = nearestOnSeg(l.x, l.z, a.x, a.z, b.x, b.z); const [fx, fz] = facing(l); if ((nx - l.x) * fx + (nz - l.z) * fz < -0.01) { if (backwards === null) backwards = l.id; } // THE CROSS-CONVENTION GATE: not "does ry match the spec's wording" (self-agreement — that is exactly // what stayed green over blank walls for nine rounds), but "does the quad LANE B ACTUALLY DRAWS land // nearer the street than the lot centre?". Physical, falsifiable, and blind to both spec lines. const [qx, qz] = facadeQuadWorld(l); const dCentre = Math.hypot(nx - l.x, nz - l.z); const dFacade = Math.hypot(nx - qx, nz - qz); if (dFacade >= dCentre) { if (drawnAway === null) drawnAway = l.id; } worstGain = Math.max(worstGain, dFacade - dCentre); } ok(backwards === null, `${label}: every lot faces its frontEdge` + (backwards !== null ? ` (lot ${backwards} backwards)` : '')); ok(drawnAway === null, `${label}: the RENDERED facade (B's +Z quad, buildings.js maths) faces the street` + (drawnAway !== null ? ` — lot ${drawnAway}'s shopfront is drawn on the far side (worst: ${worstGain.toFixed(2)} m FARTHER from the road than the lot centre)` : '')); const byBlock = {}; for (const l of plan.lots) (byBlock[l.block] ||= []).push(l); let inBlock = null; for (const arr of Object.values(byBlock)) { const cq = arr.map(lotCorners); for (let i = 0; i < arr.length && !inBlock; i++) for (let j = i + 1; j < arr.length; j++) if (obbOverlap(cq[i], cq[j])) { inBlock = [arr[i].id, arr[j].id]; break; } if (inBlock) break; } ok(!inBlock, `${label}: no overlapping lots within a block` + (inBlock ? ` (lots ${inBlock})` : '')); const solids = plan.lots.filter(l => SOLID.has(l.use)); const cs = solids.map(lotCorners); const aabb = cs.map(q => { let a = [1e9, 1e9, -1e9, -1e9]; for (const [x, z] of q) { a[0] = Math.min(a[0], x); a[1] = Math.min(a[1], z); a[2] = Math.max(a[2], x); a[3] = Math.max(a[3], z); } return a; }); let crossBlock = null; for (let i = 0; i < solids.length && !crossBlock; i++) for (let j = i + 1; j < solids.length; j++) { if (solids[i].block === solids[j].block) continue; const A = aabb[i], B = aabb[j]; if (A[2] < B[0] || B[2] < A[0] || A[3] < B[1] || B[3] < A[1]) continue; if (obbOverlap(cs[i], cs[j])) { crossBlock = [solids[i].id, solids[j].id]; break; } } ok(!crossBlock, `${label}: no overlapping building lots across blocks` + (crossBlock ? ` (lots ${crossBlock})` : '')); const idx = chunkIndex(plan); const covered = new Set(); for (const cellB of Object.values(idx.chunks)) { for (const id of cellB.lots) covered.add(id); ok(cellB.lots.every(id => lotIds.has(id)) && cellB.shops.every(id => plan.shops.some(sh => sh.id === id)) && cellB.edges.every(id => edgeIds.has(id)), `${label}: chunk buckets reference real ids`); } ok(plan.lots.every(l => covered.has(l.id)), `${label}: chunkIndex covers every lot`); const cell = v => Math.floor(v / CHUNK); let edgeGap = null; for (const e of plan.streets.edges) { const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue; const dx = b.x - a.x, dz = b.z - a.z, len = Math.hypot(dx, dz) || 1; const hw = (e.width || 0) / 2, px = -dz / len * hw, pz = dx / len * hw; const steps = Math.max(1, Math.ceil(len / 2)); for (const f of [-1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 0.75, 1]) { for (let k = 0; k <= steps && !edgeGap; k++) { const x = a.x + dx * k / steps + px * f, z = a.z + dz * k / steps + pz * f; const bk = idx.chunks[`${cell(x)},${cell(z)}`]; if (!bk || !bk.edges.includes(e.id)) edgeGap = [e.id, f, cell(x), cell(z)]; } if (edgeGap) break; } if (edgeGap) break; } ok(!edgeGap, `${label}: chunkIndex covers the full road corridor (road+verge)` + (edgeGap ? ` (edge ${edgeGap[0]} @ off ${edgeGap[1]} missing @ ${edgeGap[2]},${edgeGap[3]})` : '')); ok(JSON.stringify(JSON.parse(JSON.stringify(plan))) === JSON.stringify(plan), `${label}: JSON round-trip lossless`); // exactly one late-night landmark; openLate field ⟺ hours[1]≥22 (same shop); it's the video (or, // for a town with no video at all, the fallback landmark). Holds for synthetic AND osm. const late = plan.shops.filter(sh => sh.openLate); // The v3 gig venue (`sh.venue`) is a night place that also runs ≥22:00 by design, so it's exempt // from the single-late-shop rule. Base plans have no venue ⇒ this filter is a no-op there (goldens // unaffected); gig plans exclude the pub so the openLate video is still the lone landmark. const ge22 = plan.shops.filter(sh => sh.hours[1] >= 22 && !sh.venue); ok(late.length === 1, `${label}: exactly one open-late shop (${late.length})`); ok(ge22.length === 1, `${label}: exactly one shop closes ≥22:00 (${ge22.length})`); ok(late.length === 1 && ge22.length === 1 && late[0] === ge22[0], `${label}: openLate field ⟺ hours[1]≥22 (same shop)`); ok(late.length === 1 && late[0].hours[1] >= 22 && late[0].type !== 'stall', `${label}: open-late shop closes ≥22:00 and isn't a stall`); ok(late.length === 1 && (late[0].type === 'video' || !plan.shops.some(x => x.type === 'video')), `${label}: the open-late shop is the video rental (or fallback if no video)`); } // The v3 GIG layer (?gigs=1). Runs the full structuralSuite (venue-aware) plus gig-specific // invariants. Validates the DISTRICT interface every downstream lane (C/D/B/F) hangs off this round. function gigSuite(plan, label) { structuralSuite(plan, label); // full structural coverage; venues exempt from late-shop rule districtInvariants(plan, label); // + the v3 district contract (also run standalone by the ROUND14 sweep) } // The v3 DISTRICT contract, factored out of gigSuite (ROUND14) so the 400-seed sweep below can assert it // at SCALE without re-running the heavy structuralSuite (already covered on the 6 SEEDS + osm parity). function districtInvariants(plan, label) { const venues = plan.shops.filter(s => s.venue); const byId = new Map(venues.map(v => [v.id, v])); // ── the district: 2–4 venues, kinds from the registry, never the openLate landmark ── ok(venues.length >= 2 && venues.length <= 4, `${label}: 2–4 venues (${venues.length})`); ok(venues.every(v => VENUE_KINDS.includes(v.venueKind)), `${label}: every venue has a known venueKind`); ok(venues.every(v => v.type === v.venueKind), `${label}: venue type ≡ venueKind (C keys its recipe off type)`); ok(venues.some(v => v.venueKind === 'pub'), `${label}: the town has a pub`); ok(venues.every(v => !v.openLate), `${label}: no venue displaced the openLate landmark`); ok(venues.every(v => (v.hours || [0, 0])[1] >= 22), `${label}: every venue is open into night so its gig can run`); ok(new Set(venues.map(v => v.lot)).size === venues.length, `${label}: venues occupy distinct lots`); ok(venues.every(v => v.genreKey === genreForVenueKind(v.venueKind)), `${label}: genreKey matches the registry kind→genre map`); // debt #1: the manifest key IS the gigKey. One key, zero mapping. ok(venues.every(v => gigKeyFor(v.genreKey) === `gig-${v.genreKey}`), `${label}: gigKey ≡ 'gig-'+genreKey`); // ── the week ── ok(Array.isArray(plan.gigs) && plan.gigs.length >= 1, `${label}: has a gig schedule`); ok(plan.gigs.every(g => byId.has(g.venueShopId)), `${label}: every gig plays a real venue`); ok(plan.gigs.every(g => Number.isInteger(g.cover) && (g.cover === 0 || (g.cover >= 2 && g.cover <= 10))), `${label}: cover ∈ {0}∪[2,10]`); ok(plan.gigs.every(g => typeof g.bandName === 'string' && g.bandName && !/[{}]|undefined/.test(g.bandName)), `${label}: band names resolved`); ok(plan.gigs.every(g => Number.isInteger(g.startSeg) && Number.isInteger(g.endSeg) && g.startSeg >= 0 && g.endSeg <= 5 && g.startSeg <= g.endSeg), `${label}: gig segment window sane`); ok(plan.gigs.every(g => Number.isInteger(g.night) && g.night >= 0 && g.night < 7), `${label}: night index in [0,7)`); ok(plan.gigs.every(g => g.genreKey === byId.get(g.venueShopId).genreKey), `${label}: gig genre ≡ its venue's genre`); ok(new Set(plan.gigs.map(g => g.gigId)).size === plan.gigs.length, `${label}: gigIds unique across town`); // ~4–6 gigs per venue per week, and never two on the same night at one venue for (const v of venues) { const mine = plan.gigs.filter(g => g.venueShopId === v.id); ok(mine.length >= 4 && mine.length <= 6, `${label}: venue ${v.id} (${v.venueKind}) plays 4–6 nights (${mine.length})`); ok(new Set(mine.map(g => g.night)).size === mine.length, `${label}: venue ${v.id} plays ≤1 gig per night`); } // no band plays two venues at once (≤1 slot per name per night across town) let clash = null; for (let n = 0; n < 7 && !clash; n++) { const names = plan.gigs.filter(g => g.night === n).map(g => g.bandName); if (new Set(names).size !== names.length) clash = n; } ok(clash === null, `${label}: no band plays two venues on one night` + (clash !== null ? ` (night ${clash})` : '')); // ── posters vs THE CORRIDOR LAW — three arms, the third of which is the control (ROUND37 §0.1) ── const gigIds = new Set(plan.gigs.map(g => g.gigId)); ok(Array.isArray(plan.posters) && plan.posters.every(p => gigIds.has(p.gigId) && isFiniteNum(p.x) && isFiniteNum(p.z) && isFiniteNum(p.ry)), `${label}: posters reference a real gig with finite coords`); ok(plan.posters.every(p => plan.gigs.find(g => g.gigId === p.gigId).night === 0), `${label}: posters advertise tonight's gigs`); // // ROUND37 CORRECTION — THE VACUOUS GATE, NAMED. Until this round this block asked exactly one // question: "is the poster outside `roadWidth/2`?". But A's own placer seats spine posters at // `poleOffset(e)`, which is DERIVED FROM `roadWidth` — so the gate and its subject were the same // function agreeing with itself. It was green for 24 rounds while all 14 gig posters on the default // boot stood on painted bitumen, because Lane B's ground painted its road quad the full `e.width` // (28 m on a main, kerb nine metres inside it) and nothing in this suite could see that number. // That is precisely the R27 cross-convention lesson recurring (see facadeQuadWorld above): a // self-consistent check over a contested geometry passes under EITHER reading. So this now does what // the R27 gate does — replicate the OTHER lane's maths and compare the two against each other. // // ARM 1 no poster stands on a CARRIAGEWAY — measured through `vergeBand(e)[0]`, the published law // itself, i.e. the same call Lane B's ground now consumes. Gate and renderer cannot drift // apart silently: change the law and both move together. // ARM 2 every SPINE poster stands ON A FOOTPATH — inside `vergeBand(e)` of SOME edge that has one // (`outer > inner`; a `lane` is all carriageway, so its band is degenerate and hosts no pole). // This is the bound the old check never had: "not on the road" also passed for a poster // standing in a paddock. After the kerb ruling it asserts the poster stands on paint B lays. // ARM 3 THE CONTROL. If arms 1–2 pass under BOTH geometries they prove nothing, so count the // posters that WOULD stand on bitumen under the PRE-RULING ground and assert that count is // NOT ZERO. That is the falsifiability control living plan-side, permanently. // // Two poster classes, two floors (unchanged). A FRONTAGE poster sits on its venue's +Z street facade // (B's canon: buildings.js facade + venue.js door/queue/camera are +Z); ROUND16 pickVenues PREFERS a // band_room/rsl lot whose facade clears every carriageway, so a frontage poster must clear its nearest // kerb by ≥ POSTER_CLEAR. A SPINE poster is free-standing on the verge and keeps the plain no-bitumen // floor (≥ 0 — a `lane` has vergeBand [w/2,w/2], so its pole seats exactly ON the kerb and 0 is legal). // −0.02 absorbs r2 poster-coord rounding vs pickVenues' un-rounded clearance test. // Frontage posters are EXEMPT FROM ARM 2 by measurement, not convenience: a frontage poster rides its // venue's facade plane, and `plan_osm.js` sets a real-town lot back at `max(KERB, roadWidth/2+FOOTPATH)`, // so on 18 of 108 gig plans it legitimately stands past every corridor edge — worst 3.98 m // (launceston_real, R37 measurement). SPINE violations over the same corpus: 0/108. const lotByIdD = new Map(plan.lots.map(l => [l.id, l])); const onFacade = p => venues.some(v => { const lot = lotByIdD.get(v.lot); if (!lot) return false; const off = lot.d / 2 + 0.06; // gigs.js buildPosters item 1 (FACADE_PROUD) return Math.hypot(p.x - (lot.x + Math.sin(lot.ry) * off), p.z - (lot.z + Math.cos(lot.ry) * off)) < 0.5; }); const segs = edgeSegments(plan); let onRoad = null, offFootpath = null; for (const p of plan.posters) { const frontage = onFacade(p); let best = Infinity, bestEdge = null, onSomeFootpath = false; for (const [e, ax, az, bx, bz] of segs) { const [nx, nz] = nearestOnSeg(p.x, p.z, ax, az, bx, bz); const d = Math.hypot(p.x - nx, p.z - nz); if (d < best) { best = d; bestEdge = e; } const [inner, outer] = vergeBand(e); if (outer > inner && d >= inner - 1e-6 && d <= outer + 1e-6) onSomeFootpath = true; } if (!bestEdge) continue; const clearance = best - vergeBand(bestEdge)[0]; // ARM 1 — the kerb IS vergeBand's inner edge const floor = frontage ? POSTER_CLEAR - 0.02 : -1e-6; if (clearance < floor && onRoad === null) onRoad = [p.id, bestEdge.id, +clearance.toFixed(2), frontage ? 'frontage' : 'spine']; if (!frontage && !onSomeFootpath && offFootpath === null) offFootpath = [p.id, bestEdge.id, +best.toFixed(2)]; } ok(onRoad === null, `${label}: no poster stands on a carriageway — vergeBand inner edge (frontage ≥ ${POSTER_CLEAR} m, spine ≥ 0)` + (onRoad ? ` (${onRoad[3]} poster ${onRoad[0]} clears edge ${onRoad[1]}'s kerb by only ${onRoad[2]} m)` : '')); ok(offFootpath === null, `${label}: every spine poster stands INSIDE some edge's vergeBand (the footpath B paints in to roadWidth/2)` + (offFootpath ? ` (poster ${offFootpath[0]} is ${offFootpath[2]} m from nearest edge ${offFootpath[1]} and inside no verge band)` : '')); const preRuling = preRulingRoadPosters(plan); ok(!plan.posters.length || preRuling > 0, `${label}: CONTROL — the kerb arms DISCRIMINATE: ${preRuling}/${plan.posters.length} poster(s) stand on bitumen under the PRE-RULING ground (?verge=0)`); } const nodeById2 = (plan, id) => plan.streets.nodes.find(n => n.id === id); // Every edge resolved once to [edge, ax, az, bx, bz] — the poster arms are O(posters × edges). function edgeSegments(plan) { const nodeById = new Map(plan.streets.nodes.map(n => [n.id, n])); const out = []; for (const e of plan.streets.edges) { const a = nodeById.get(e.a), b = nodeById.get(e.b); if (a && b) out.push([e, a.x, a.z, b.x, b.z]); } return out; } // ROUND37 §0.1 — LANE B'S PRE-RULING GROUND, REPLICATED HERE ON PURPOSE (the R27 cross-lane pattern). // `ground.js` painted its road quad the FULL corridor — `hQuad(cx, cz, dir, perp, roadLen, e.width, 0)` // — i.e. bitumen out to `e.width/2`, with A's kerb at `roadWidth(e)/2` nine metres inside it on a main. // John's R37 ruling moves the paint in to the kerb; Lane B's permanent `?verge=0` flag keeps THIS // geometry in the codebase forever, so the replica is pinned to the FLAG, not to a line number that // will drift. It counts a floor, never an over-count: ground.js overruns each segment by OVERLAP=1.5 m // at both ends and this clamps to the segment. Used as the control arm above and pinned at the default // boot's exactly-14 in §3c. function preRulingRoadPosters(plan) { let n = 0; const segs = edgeSegments(plan); for (const p of plan.posters || []) { for (const [e, ax, az, bx, bz] of segs) { const [nx, nz] = nearestOnSeg(p.x, p.z, ax, az, bx, bz); if (Math.hypot(p.x - nx, p.z - nz) < ((e.width || 0) / 2) - 1e-9) { n++; break; } } } return n; } // ── 1. determinism: two runs byte-identical ───────────────────────────────────────── section('determinism (deep-equal across two runs)'); for (const s of SEEDS) { const a = JSON.stringify(generatePlan(s)), b = JSON.stringify(generatePlan(s)); ok(a === b, `seed ${s}: two runs identical`); } // ── 1b. golden fingerprint: guards against silent output DRIFT across code revisions ──── // (If you intentionally change generator output, run selfcheck, copy the printed hash here.) section('golden fingerprint (output-drift guard)'); const GOLDEN = { seed: 20261990, hash: 0x5f76e76 }; { const hash = xmur3(JSON.stringify(generatePlan(GOLDEN.seed)))() >>> 0; ok(hash === GOLDEN.hash, `seed ${GOLDEN.seed}: fingerprint 0x${hash.toString(16)} matches golden 0x${(GOLDEN.hash >>> 0).toString(16)}`); } // ── 2. performance: generate under 100ms ──────────────────────────────────────────── section('performance (<100ms per plan)'); for (const s of SEEDS) { const t0 = process.hrtime.bigint(); generatePlan(s); const ms = Number(process.hrtime.bigint() - t0) / 1e6; ok(ms < 100, `seed ${s}: generated in ${ms.toFixed(1)}ms`); } // ── 3. structural invariants — SYNTHETIC source (full suite) + synthetic-only brief checks ── section('structural invariants — synthetic'); const facadeSet = new Set(); for (const s of SEEDS) { const plan = generatePlan(s); structuralSuite(plan, `syn ${s}`); // synthetic-only brief presence (a real OSM town has none of these) ok(plan.streets.edges.some(e => e.kind === 'main'), `syn ${s}: has a main-street spine`); ok(plan.streets.edges.some(e => e.kind === 'arcade'), `syn ${s}: has an arcade`); ok(plan.districts.some(d => d.kind === 'market'), `syn ${s}: has a market district`); ok(plan.shops.some(sh => sh.type === 'stall'), `syn ${s}: market has stalls`); ok(plan.shops.some(sh => sh.type === 'dept'), `syn ${s}: has a department-store anchor`); const milkbars = plan.shops.filter(sh => sh.type === 'milkbar').length; ok(milkbars >= 2 && milkbars <= 4, `syn ${s}: 2–4 corner milk bars (${milkbars})`); for (const sh of plan.shops) facadeSet.add(sh.facadeSkin); } // ── 3b. OSM plan-source PARITY: every town runs the identical structuralSuite + its own golden ── // (round 8) Real-data towns get the same coverage as synthetic; only the synthetic-brief checks are // skipped. Goldens are pinned per town; the synthetic golden above stays 0x3fa36874. section('OSM plan source parity (fixture-driven, zero-network)'); const OSM_GOLDENS = { melbourne: 0x9c7e76b3, katoomba: 0xf3aafec8, silverton: 0x6d74c4 }; const OSM_SEEDS = [20261990, 1, 42, 777]; for (const town of osmTownKeys()) { const report = {}; for (const s of OSM_SEEDS) { const plan = generatePlanOSM(s, town, s === 20261990 ? { report } : {}); ok(plan.source === 'osm', `osm/${town} ${s}: source tagged 'osm'`); ok(JSON.stringify(generatePlanOSM(s, town)) === JSON.stringify(plan), `osm/${town} ${s}: deterministic (same fixture+seed)`); structuralSuite(plan, `osm/${town} ${s}`); for (const sh of plan.shops) facadeSet.add(sh.facadeSkin); } const hash = xmur3(JSON.stringify(generatePlanOSM(20261990, town)))() >>> 0; const want = OSM_GOLDENS[town]; // Unpinned town (just added via the "add a town" recipe) → tell the human the exact line to paste. if (want === undefined) console.log(` ⚠ osm/${town}: UNPINNED — add to OSM_GOLDENS → ${town}: 0x${hash.toString(16).padStart(8, '0')},`); ok(want !== undefined && hash === (want >>> 0), `osm/${town}: golden 0x${hash.toString(16)} matches pinned 0x${((want ?? 0) >>> 0).toString(16)}`); console.log(` ${town}: ${report.shops} shops · normalized {typesRemapped:${report.typesRemapped}, hoursClamped:${report.hoursClamped}, openLate:${report.openLate}}`); } // ── 3c. v3 GIG layer (?gigs=1): flags-off identity + gig invariants (synthetic + osm) ── section('v3 gig layer (?gigs=1) — flags-off identity + gig invariants'); // PRIME FLAG LAW: ?gigs off must be byte-identical to the base generator (goldens can't move). for (const s of SEEDS) ok(JSON.stringify(generatePlanFor(s)) === JSON.stringify(generatePlan(s)), `seed ${s}: ?gigs off ≡ base (byte-identical)`); const GIG_SEEDS = [20261990, 1, 42, 777]; for (const s of GIG_SEEDS) gigSuite(generatePlanFor(s, 'synthetic', { gigs: true }), `gig syn ${s}`); for (const town of osmTownKeys()) for (const s of GIG_SEEDS) gigSuite(generatePlanFor(s, 'osm', { gigs: true, town }), `gig osm/${town} ${s}`); // determinism of the gig layer (same seed ⇒ byte-identical gig plan) ok(JSON.stringify(generatePlanFor(42, 'synthetic', { gigs: true })) === JSON.stringify(generatePlanFor(42, 'synthetic', { gigs: true })), `gig layer deterministic`); // custom-band drop-in: passed names surface with priority, deterministically given (seed, list) { const custom = ['AAA Custom One', 'BBB Custom Two', 'CCC Custom Three']; const pc = generatePlanFor(20261990, 'synthetic', { gigs: true, customBands: custom }); ok(pc.gigs.map(g => g.bandName).filter(b => custom.includes(b)).length === custom.length, `custom bands: all ${custom.length} passed names surface`); ok(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true, customBands: custom })) === JSON.stringify(pc), `custom bands: deterministic given (seed, list)`); } // gig-layer golden (guards gig-layer output drift; pure-generator path, no custom file) const GIG_GOLDEN = 0xec7a2d39; // ROUND16 re-pin: pickVenues prefers band_room/rsl lots whose +Z facade clears crossing kerbs (ledger #6). Was 0x4f4a549d (R15 +Z flip), 0x1f636349 (R13 district), 0xa6ae5a5e (R12 alpha). { const hash = xmur3(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true })))() >>> 0; if (GIG_GOLDEN === 0) console.log(` ⚠ gig golden UNPINNED → set GIG_GOLDEN = 0x${hash.toString(16).padStart(8, '0')}`); ok(GIG_GOLDEN !== 0 && hash === (GIG_GOLDEN >>> 0), `gig layer golden 0x${hash.toString(16)} matches pinned 0x${(GIG_GOLDEN >>> 0).toString(16)}`); } // ROUND37 §0.1 — THE PRE-RULING COUNT, PINNED ON THE DEFAULT BOOT. The city-patterns audit reported // "all 14 gig posters are standing in the road"; Lane A re-measured it and it is exact — 14 posters on // the default boot (synthetic, golden seed, gigs default-ON since R16), all 14 inside `e.width/2`, and // ZERO inside the ruled kerb `roadWidth/2`. Pinned here, not just asserted `> 0`, because a control that // is allowed to drift toward zero is how a gate goes vacuous in the first place — the thing this round // exists to stop. If a future change moves posters out past `e.width/2`, this fires and the ?verge=0 // falsifiability control (Lane B's flag, Lane F's gate leg) has to be re-argued rather than quietly lost. const PRE_RULING_ROAD_POSTERS = 14; { const n = preRulingRoadPosters(generatePlanFor(GOLDEN.seed, 'synthetic', { gigs: true })); ok(n === PRE_RULING_ROAD_POSTERS, `default boot: exactly ${PRE_RULING_ROAD_POSTERS} posters stood on bitumen under the PRE-RULING ground (?verge=0) — measured ${n}`); } // ── 3d. district invariant sweep (ROUND14): 400 synthetic seeds + osm graceful placement ─────────── // Release-round Lane A gate: the district contract must hold at SCALE, not just the 4 hero seeds. Runs // districtInvariants ONLY (structuralSuite already covers the 6 SEEDS + osm parity) so 400 full gig-on // generations stay well under a second. Pure validation — NO new rng draws, no golden moves. section('district invariant sweep (ROUND14: 400 synthetic seeds + osm graceful placement)'); const SWEEP_N = 400; const sweepVenueCounts = new Set(); for (let s = 1; s <= SWEEP_N; s++) { const plan = generatePlanFor(s, 'synthetic', { gigs: true }); districtInvariants(plan, `sweep syn ${s}`); sweepVenueCounts.add(plan.shops.filter(v => v.venue).length); } // also cover the large/edge uint32 seeds (2e9, 8675309 …) — where rng edge cases hide — that fall // outside 1..400 and otherwise only get the flags-off byte-identity check, not the district contract. for (const s of SEEDS) if (s > SWEEP_N) districtInvariants(generatePlanFor(s, 'synthetic', { gigs: true }), `sweep syn ${s}`); // the seeded 2–4 count is genuinely exercised across the range, not pinned to one value ok([2, 3, 4].every(n => sweepVenueCounts.has(n)), `sweep: venue count spans {2,3,4} across ${SWEEP_N} seeds (saw {${[...sweepVenueCounts].sort((a, b) => a - b).join(',')}})`); // osm graceful placement — two branches of pickVenues' degradation, each proven by a fixture that // genuinely lacks the structure the synthetic town has: // • katoomba (19 shops): NO warehouse district ⇒ band_room/rsl hit their off-spine fallback. // • silverton (single-row): NO main spine at all ⇒ pub's onMain filter is empty and buildPosters' // spine run falls back to every edge (ROUND16 ledger #7 — closes the R14 gap where both osm // fixtures had a spine). Both still land 2–4 valid venues incl. a pub. for (const seed of GIG_SEEDS) { const kt = generatePlanFor(seed, 'osm', { gigs: true, town: 'katoomba' }); ok(kt.blocks.every(b => b.kind !== 'warehouse'), `sweep osm/katoomba ${seed}: fixture has no warehouse district (graceful-placement precondition)`); districtInvariants(kt, `sweep osm/katoomba ${seed}`); const sv = generatePlanFor(seed, 'osm', { gigs: true, town: 'silverton' }); ok(!sv.streets.edges.some(e => e.kind === 'main'), `sweep osm/silverton ${seed}: fixture has no main spine (no-spine placement precondition)`); districtInvariants(sv, `sweep osm/silverton ${seed}`); } // ── 3e. town-cache contract (ROUND17 ledger #6): validator + real-data hardening + registry seam ── // A publishes the contract E's build_towns.py builds to; plan_osm must eat real data (dead ends, no // spine, blank names, unknown types, odd densities) without new plan features. These are synthetic // adversarial caches — the committed proof of the hardening (real caches load in §3f below). section('town-cache contract (ROUND17: real data through plan_osm)'); const mkCache = (shops, extra = {}) => ({ schema: 'procity-town-cache/1', key: 'stress', town: 'Stress', source: 'osm', license: 'ODbL 1.0', attribution: '© OpenStreetMap contributors', center: { lat: -33.7, lon: 150.3 }, shops, ...extra, }); const cluster = (n, opt = {}) => Array.from({ length: n }, (_, i) => ({ id: 500 + i, name: opt.blank && i % 3 === 0 ? '' : `Real Shop ${i}`, type: opt.unknown && i % 4 === 0 ? 'op_shop_weird' : ['book', 'record', 'opshop', 'toy', 'pawn', 'video', 'milkbar'][i % 7], lat: -33.7 + (opt.samePoint ? 0 : (i % 5) * 0.0008), lon: 150.3 + (opt.samePoint ? 0 : Math.floor(i / 5) * 0.0008), suburb: 'Testville', })); // (a) VALID-but-messy caches validate ok AND boot a full valid district (structural + gig invariants) for (const [label, cache] of [ ['compact 14-shop town', mkCache(cluster(14))], ['blank names → defaulted', mkCache(cluster(14, { blank: true }))], ['unknown types → opshop', mkCache(cluster(14, { unknown: true }))], ['all shops at one point', mkCache(cluster(14, { samePoint: true }))], ['minimum shop count', mkCache(cluster(MIN_TOWN_SHOPS))], ]) { const v = validateTownCache(cache); ok(v.ok, `cache "${label}": validates ok` + (v.ok ? '' : ` — ${v.errors.join('; ')}`)); if (!v.ok) continue; for (const s of [1, 20261990]) structuralSuite(generatePlanOSM(s, 'stress', { cache }), `stress/${label} ${s}`); districtInvariants(generatePlanFor(20261990, 'osm', { gigs: true, town: 'stress', cache }), `stress/${label}`); } // (b) INVALID caches are REJECTED (they would not boot a valid district) for (const [label, cache, needle] of [ ['too few shops', mkCache(cluster(MIN_TOWN_SHOPS - 1)), 'shops required'], ['non-finite coords', mkCache(cluster(12).map((s, i) => i === 2 ? { ...s, lat: NaN } : s)), 'non-finite'], ['missing center', { schema: 'procity-town-cache/1', shops: cluster(12) }, 'center'], ['shops not an array', { ...mkCache(cluster(12)), shops: 'nope' }, 'array'], ['not an object', 'nope', 'not an object'], ]) { const v = validateTownCache(cache); ok(!v.ok && v.errors.some(e => e.includes(needle)), `cache "${label}": rejected` + (v.ok ? ' (WRONGLY ACCEPTED)' : ` — "${v.errors[0]}"`)); } // (c) a region-wide cache still boots but WARNS (the R17 mega-strip risk: bound the query to one town) { const wide = mkCache(cluster(20).map((s, i) => ({ ...s, lat: -33.7 + i * 0.05 }))); const v = validateTownCache(wide); ok(v.ok && v.warnings.some(w => /span|town/.test(w)), `wide-spread cache: valid but warns (${v.warnings.find(w => /span/.test(w)) || 'NO WARN'})`); } // (c2) the SPACING warn (ROUND23 ledger #5 — D's thin-tail finding as law). The span warn catches a town // that's too WIDE; this catches one too SPARSE to read as a town. Warn, never error — and the town must // still boot green, because that's exactly what toowoomba did through the entire v4.0 pack. { // a scattered town: 12 shops ~400 m apart (toowoomba's shape) — inside the 5 km span law, so the span // warn does NOT fire; this is the gap D found and the reason the metric exists. const scattered = mkCache(Array.from({ length: 12 }, (_, i) => ({ id: 600 + i, name: `Scattered ${i}`, type: 'opshop', lat: -33.7 + (i % 4) * 0.0036, lon: 150.3 + Math.floor(i / 4) * 0.0036, suburb: 'Testville', }))); const v = validateTownCache(scattered); ok(v.ok, `sparse cache: still VALID (warn, not error) — a flagged town boots a real district`); ok(v.warnings.some(w => /median shop spacing/.test(w)), `sparse cache: warns (${v.warnings.find(w => /spacing/.test(w)) || 'NO WARN'})`); ok(!v.warnings.some(w => /span/.test(w)), `sparse cache: the SPAN warn stays silent — spacing catches what span cannot`); structuralSuite(generatePlanOSM(20261990, 'sparse', { cache: scattered }), `sparse-warn town`); districtInvariants(generatePlanFor(20261990, 'osm', { gigs: true, town: 'sparse', cache: scattered }), `sparse-warn town`); // a compact town must NOT warn (no false positive on the shape 20 of 23 pack towns have) const tight = validateTownCache(mkCache(cluster(14))); ok(!tight.warnings.some(w => /median shop spacing/.test(w)), `compact cache: no spacing warn (no false positive)`); // the metric itself — D's definition: per shop, distance to the NEAREST OTHER shop, then the median. ok(medianShopSpacing(mkCache(cluster(14))) < MAX_MEDIAN_SPACING_M, `medianShopSpacing: compact town under the floor`); ok(medianShopSpacing(mkCache(cluster(14, { samePoint: true }))) === 0, `medianShopSpacing: coincident shops → 0 m`); ok(medianShopSpacing(mkCache(cluster(1))) === null, `medianShopSpacing: < 2 shops → null (nothing to space)`); ok(medianShopSpacing({ shops: [] }) === null, `medianShopSpacing: no shops → null, no throw`); { // exact-value check: two shops 100 m apart in latitude → both nearest-neighbours are 100 m → median 100. const pair = mkCache([{ id: 1, name: 'A', type: 'opshop', lat: -33.7, lon: 150.3 }, { id: 2, name: 'B', type: 'opshop', lat: -33.7 + 100 / 111320, lon: 150.3 }]); const got = medianShopSpacing(pair); ok(Math.abs(got - 100) < 0.5, `medianShopSpacing: 100 m pair measures ${got.toFixed(1)} m (projection sane)`); } } // (c3) the IDENTITY FIELD (ROUND24 ledger #2, with G) — `shop.godverseShopId`, the key that resolves // `stock_godverse//`. It is NOT `shop.id` and NOT "the census id": Monster Robot Party isn't in // thriftgod's census at all, so its id is its dealgod store id (3962749), while census shops use // thriftgod ids (max 2992). Two disjoint spaces, one opaque field — so these assert the FIELD's // properties, never a relationship to `id`. { const gv = shops => ({ ...mkCache(shops), source: 'godverse+osm' }); const withIds = cluster(8).map((s, i) => ({ ...s, godverseShopId: 3962749 + i })); // a well-formed godverse cache validates clean and carries no spurious warn const good = validateTownCache(gv(withIds)); ok(good.ok, `godverse cache: valid with ids` + (good.ok ? '' : ` — ${good.errors.join('; ')}`)); ok(!good.warnings.some(w => /godverseShopId/.test(w)), `godverse cache: fully-keyed cache raises no identity warn`); // MALFORMED → error. A path segment, not a label: the string "3962749" is the same folder but // different JSON, and that coercion drift is the species F caught in the atlas contract. for (const [label, bad] of [ ['string id', '3962749'], ['float id', 3962749.5], ['zero', 0], ['negative', -5], ['NaN', NaN], ]) { const v = validateTownCache(gv(withIds.map((s, i) => i === 1 ? { ...s, godverseShopId: bad } : s))); ok(!v.ok && v.errors.some(e => /malformed godverseShopId/.test(e)), `godverse cache: ${label} REJECTED`); } // DUPLICATE → error. Two shops keyed to one atlas is mis-stocking (charter risk #3: "never mis-stocked"). { const v = validateTownCache(gv(withIds.map((s, i) => i === 1 ? { ...s, godverseShopId: withIds[0].godverseShopId } : s))); ok(!v.ok && v.errors.some(e => /duplicate godverseShopId/.test(e)), `godverse cache: duplicate id REJECTED (mis-stock)`); } // RULING 2 (ROUND25): a MIXED cache — keyed GODVERSE layer + unkeyed texture layer — is the DESIGN, // not a gap. My R24 arm counted every unkeyed shop as missing and fired 54/72 on a healthy town; it // was asserting the design away. A mixed cache is now simply normal: valid, and silent. { const mixed = gv(withIds.map((s, i) => i < 2 ? { ...s, godverseShopId: undefined } : s)); const v = validateTownCache(mixed); ok(v.ok, `godverse cache: MIXED (keyed + texture) is VALID — Ruling 2 is the design`); ok(!v.warnings.some(w => /godverseShopId/.test(w)), `godverse cache: MIXED raises NO identity warn (texture shops are legitimately unkeyed)`); } // ...but a godverse cache with NO identity at all has no godverse layer — an osm cache wearing the // wrong `source`. That's the one honest requirement left at this layer, and it stays a warn. { const v = validateTownCache(gv(cluster(8))); ok(v.ok, `godverse cache: empty GODVERSE layer stays VALID (tier 0 must always boot)`); ok(v.warnings.some(w => /GODVERSE layer is empty/.test(w)), `godverse cache: empty layer WARNS (${v.warnings.find(w => /GODVERSE/.test(w)) || 'NO WARN'})`); } // a plain osm cache must never be asked for ids ok(!validateTownCache(mkCache(cluster(8))).warnings.some(w => /godverseShopId|GODVERSE/.test(w)), `osm cache: no identity warn (godverse-only requirement)`); // THE SEAM THAT MATTERS: the id survives the lift onto plan.shops. Without this the field is a // cache-only decoration and F's per-shop base can never resolve — the runtime reads the PLAN. { const cache = gv(withIds.map((s, i) => i < 2 ? { ...s, godverseShopId: undefined } : s)); // mixed, like the real ones const p = generatePlanOSM(20261990, 'gvtest', { cache }); const keyed = p.shops.filter(s => s.godverseShopId !== undefined); ok(keyed.length > 0 && keyed.length <= p.shops.length, `godverse lift: the keyed layer survives onto plan.shops (${keyed.length} of ${p.shops.length} seated)`); ok(keyed.every(s => Number.isSafeInteger(s.godverseShopId) && s.godverseShopId > 0), `godverse lift: ids survive as positive integers`); ok(p.shops.filter(s => !('godverseShopId' in s)).length === p.shops.length - keyed.length, `godverse lift: texture shops carry NO key (absent, not undefined)`); // and an unkeyed cache writes NO key — that absence is what keeps every pinned osm golden frozen const plain = generatePlanOSM(20261990, 'plaintest', { cache: mkCache(cluster(8)) }); ok(plain.shops.every(s => !('godverseShopId' in s)), `plain lift: the key is ABSENT, not undefined (byte-identical JSON ⇒ goldens frozen)`); } // THE UNIQUENESS CHECK, REBUILT SO IT CAN ACTUALLY FAIL (ROUND25 ledger #1). The R24 version compared // `new Set(all ids).size === shops.length`, which collapses every `undefined` into ONE entry — so on a // mixed cache it fails structurally (54 texture shops → 1 entry) while a REAL duplicate among two // defined ids could hide behind that same collapse. Uniqueness now runs over DEFINED ids only. // // And per the vacuous-gate law: a check nobody can see fail is not a check. `validateTownCache` // rejects duplicates, so a dup can never reach the plan through the front door — `generatePlanOSM` // does NOT validate, so this feeds one straight to the lift and proves the assert bites. { const dupCache = gv(withIds.map((s, i) => i === 3 ? { ...s, godverseShopId: withIds[0].godverseShopId } : s)); ok(!validateTownCache(dupCache).ok, `uniqueness: the validator still rejects a duplicate at the front door`); const p = generatePlanOSM(20261990, 'duptest', { cache: dupCache }); // bypasses validation, as the lift does const ids = p.shops.map(s => s.godverseShopId).filter(id => id !== undefined); ok(new Set(ids).size < ids.length, `uniqueness: a duplicate that BYPASSES the validator is visible on the plan (${ids.length} ids → ${new Set(ids).size} unique) — the check can fail`); // the same predicate on a healthy mixed cache must pass — no false positive from the undefineds const okIds = generatePlanOSM(20261990, 'gvtest2', { cache: gv(withIds.map((s, i) => i < 2 ? { ...s, godverseShopId: undefined } : s)) }) .shops.map(s => s.godverseShopId).filter(id => id !== undefined); ok(new Set(okIds).size === okIds.length, `uniqueness: a healthy MIXED cache passes the same predicate (no undefined-collapse false positive)`); } } // (d) the registry seam: register → osmTownKeys() sees it → generatePlanOSM resolves it { registerTownCache('regtest', mkCache(cluster(14))); ok(osmTownKeys().includes('regtest'), `registerTownCache: osmTownKeys() includes 'regtest'`); ok(generatePlanOSM(1, 'regtest').shops.length === 14, `registerTownCache: generatePlanOSM resolves the registered cache`); } // (e) v4 REAL ROADS (schema v2, ROUND18): a v2 cache with roads[] builds the CityPlan street graph FROM // the real ways and STILL passes the full structural + gig suites. Synthetic irregular graph — a main // street, two cross streets (a real intersection + an acute-ish angle), a dead-end lane, a slight curve — // the committed proof of the graph lift; `katoomba_real` validates + pins its golden when E's roads land. section('v4 real-roads graph lift (schema v2)'); { const roadsCache = { schema: 'procity-town-cache/2', key: 'roadstest', town: 'Roadstest', source: 'osm', license: 'ODbL 1.0', attribution: '© OpenStreetMap contributors', center: { lat: -33.71, lon: 150.31 }, roads: [ { kind: 'primary', name: 'Main St', pts: [[-33.7150, 150.3110], [-33.7130, 150.31108], [-33.7110, 150.31116], [-33.7090, 150.31124], [-33.7070, 150.31132]] }, { kind: 'residential', name: 'Cross A', pts: [[-33.7130, 150.31108], [-33.7130, 150.3090]] }, { kind: 'residential', name: 'Cross B', pts: [[-33.7100, 150.3112], [-33.7095, 150.3130]] }, { kind: 'service', name: 'Back Lane', pts: [[-33.7120, 150.31112], [-33.7122, 150.3122]] }, ], shops: Array.from({ length: 18 }, (_, i) => ({ id: 700 + i, name: `Real St Shop ${i}`, type: ['book', 'record', 'opshop', 'toy', 'pawn', 'video', 'milkbar'][i % 7], lat: -33.7150 + i * 0.00045, lon: 150.3110 + ((i % 3) - 1) * 0.0005, suburb: 'Katoomba', })), }; const v = validateTownCache(roadsCache); ok(v.ok, `roads cache: validates as schema v2` + (v.ok ? '' : ` — ${v.errors.join('; ')}`)); for (const s of [1, 42, 20261990]) { const p = generatePlanOSM(s, 'roadstest', { cache: roadsCache }); ok(p.streets.edges.length >= 3, `roads ${s}: multi-edge real graph (${p.streets.edges.length} edges from real ways)`); ok(p.streets.edges.some(e => e.kind === 'main'), `roads ${s}: a main spine exists (OSM class or shop-density promoted)`); ok(p.shops.length >= 6, `roads ${s}: ${p.shops.length} shops seated on real edges (overlap-resolved)`); structuralSuite(p, `roads syn ${s}`); } ok(JSON.stringify(generatePlanOSM(20261990, 'roadstest', { cache: roadsCache })) === JSON.stringify(generatePlanOSM(20261990, 'roadstest', { cache: roadsCache })), `roads: deterministic (byte-identical re-run)`); districtInvariants(generatePlanFor(20261990, 'osm', { gigs: true, town: 'roadstest', cache: roadsCache }), `roads syn`); // roads-absent (v1) marches — proven identical to before by the frozen osm goldens above ok(generatePlanOSM(1, 'x', { cache: { ...roadsCache, roads: undefined } }).streets.edges.some(e => e.kind === 'main'), `roads absent: marched fallback still boots`); } // ── THE ADDRESS LAYER's independent CONTROL RESOLVER (ROUND39 item 39.1) ──────────────────────── // `address.js` answers "which named way is this edge ON?" by DISTANCE. This answers the same question // by POINT MEMBERSHIP: a plan edge is built from two consecutive points of ONE simplified way // (plan_osm.js:388-394), and plan nodes are those way points snapped on a 3 m lattice — so the way the // edge came from must CONTAIN both endpoints' snap keys. Two different questions over the same data, // which is the whole point: this is the R27 cross-convention pattern (see facadeQuadWorld above), not // a restatement. If A's distance resolver drifts, or the shift recovery breaks, membership disagrees // and the gate below fires. It deliberately replicates plan_osm's SNAP constant; that coupling IS the // gate — change the lift's snapping and this must be re-derived rather than silently trusted. const ADDR_SNAP = 3; // plan_osm.js:353 `SNAP` function membershipStreets(plan, cache, shift) { const cosLat = Math.cos(cache.center.lat * Math.PI / 180); const projX = lon => (lon - cache.center.lon) * 111320 * cosLat; const projZ = lat => (lat - cache.center.lat) * 111320; const skey = (x, z) => `${Math.round(x / ADDR_SNAP)},${Math.round(z / ADDR_SNAP)}`; const waysAtKey = new Map(), wayName = []; (cache.roads || []).forEach((rd, wi) => { if (!rd || !Array.isArray(rd.pts) || rd.pts.length < 2) { wayName.push(null); return; } wayName.push(typeof rd.name === 'string' && rd.name.trim() ? rd.name.trim() : null); for (const p of rd.pts) { if (!Array.isArray(p) || !isFiniteNum(p[0]) || !isFiniteNum(p[1])) continue; const k = skey(projX(p[1]), projZ(p[0])); const s = waysAtKey.get(k); if (s) s.add(wi); else waysAtKey.set(k, new Set([wi])); } }); const nodeById = new Map(plan.streets.nodes.map(n => [n.id, n])); const out = new Map(); // edgeId → [name…] (empty ⇒ no membership answer) for (const e of plan.streets.edges) { const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue; const A = waysAtKey.get(skey(a.x - shift.shx, a.z - shift.shz)); const B = waysAtKey.get(skey(b.x - shift.shx, b.z - shift.shz)); if (!A || !B) continue; const nms = [...new Set([...A].filter(w => B.has(w)).map(w => wayName[w]).filter(Boolean))]; if (nms.length) out.set(e.id, nms); } return out; } // Compare A's shipped answer against the control over the edges `localityOf` actually reads (the ones // a shop fronts). Returns { checked, agree, disagree, examples[] } — plus the all-edge disagreement // count, which is pinned in the corpus roll-up rather than asserted zero (see the note there). function addressVsControl(plan, addr, ctrl) { const lotById = new Map(plan.lots.map(l => [l.id, l])); const fronts = new Set(plan.shops.map(s => (lotById.get(s.lot) || {}).frontEdge)); let checked = 0, agree = 0, disagree = 0, allDis = 0; const examples = []; for (const [eid, nms] of ctrl) { const got = addr.streetOf(eid); if (!got) continue; const same = nms.includes(got); if (!same) allDis++; if (!fronts.has(eid)) continue; checked++; if (same) agree++; else { disagree++; if (examples.length < 3) examples.push(`edge ${eid}: address "${got}" vs control "${nms.join('|')}"`); } } return { checked, agree, disagree, allDis, examples }; } // ── 3f. real town caches — E's build_towns.py output under web/assets/towns/ (ROUND17 ledger #6) ── // Empty until E's caches land; each is validated, run through the full structural + gig suites, and // pinned. Guarded so the gate is green before E's first cache — A pins goldens as caches arrive. section('real town caches (web/assets/towns/*.json)'); const TOWNS_DIR = join(HERE, '..', '..', 'assets', 'towns'); // ROUND20: re-pinned once for E's DENSITY WIDENING (5 caches at 3-6x shops) + A's poster cap + // capacity widen. All five real towns are on REAL ROADS and pinned on the // real-roads graph output — the A→E→A finalization, katoomba's golden included. Re-pinned this round after // the fragmentation ruling (junction-protected DP + cull/bridge) + E's spine-poster fix. Amendment law: // the alpha changed nothing outside the cache-schema path; synthetic/fixtures/classic/default stay frozen. const REAL_TOWN_GOLDENS = { adelaide_real: 0x4b9137a5, ballarat_real: 0xf0a9e8a1, bendigo_real: 0x44f5ef2d, bowral_real: 0x5574436b, braddon_real: 0xf03da36c, brunswick_real: 0xb548149e, castlemaine_real: 0xf3118387, darwin_real: 0xd45d7042, daylesford_real: 0x96a92068, fitzroy_real: 0x1cb08059, fremantle_real: 0x17954f0a, geelong_real: 0xf90f4463, glebe_real: 0xa9418848, hobart_real: 0xbcee1ccb, katoomba_real: 0x126a66cb, launceston_real: 0xa2bb4e71, marrickville_real: 0x8c8d39aa, newcastle_real: 0x61de3375, newtown_godverse: 0x1e8d49b9, newtown_real: 0x64a98d7e, northbridge_real: 0xb0c24e12, // toowoomba_real RETIRED by E in R23 (cache deleted) — pin removed with it. A golden for a town that // cannot load is dead config: it can never fire, so it can never fail. Same species as the round's // vacuous-gate law, one layer down. (E tried the re-bbox first and it GAMED my spacing metric — median // NN 395.7 → 89.4 m "passes" while hub density fell 3/12 → 2/12, the pack's worst. Right call.) redhill_godverse: 0x40d30a31, redhill_real: 0x5b851696, westend_real: 0x6032d73a, }; // ROUND20: the GIG-layer golden per real town (district + capped posters) — the base golden above is // gigs-off so it never captured the poster cap. Pinned here so the ROUND20 poster cap (and the density // absorb to come) are regression-guarded byte-exact, not just by districtInvariants. // ROUND21: re-pinned for the venue cluster-adjacency bias (D's relocation finding). Only the GIG goldens // move — venue selection is gig-layer, so the base town goldens above are untouched. const REAL_TOWN_GIG_GOLDENS = { adelaide_real: 0xda729bae, ballarat_real: 0xf097c331, bendigo_real: 0xe3e8d892, bowral_real: 0x3f37bbcd, braddon_real: 0xbc51aed2, brunswick_real: 0xc0a90188, castlemaine_real: 0x3c3cf400, darwin_real: 0xd37b37cd, daylesford_real: 0x7b24c5c5, fitzroy_real: 0xcd31b1ab, fremantle_real: 0xc6449dc, geelong_real: 0xcd0ceac4, glebe_real: 0x8d2b12ca, hobart_real: 0xda3211c9, katoomba_real: 0x5eece2d4, launceston_real: 0xb619b65b, marrickville_real: 0x5dc00b5f, newcastle_real: 0xfc3cb144, newtown_godverse: 0x4153dee2, newtown_real: 0xb6a75bb1, northbridge_real: 0x49fe0f2b, // toowoomba_real RETIRED by E in R23 — see the note on the base map above. redhill_godverse: 0xb53f695d, redhill_real: 0x21db6d1, westend_real: 0xac9e69ea, }; // `index.json` is E's towns INDEX (key/town/state/shops/roads for B's selector), not a town cache — skip it. const townFiles = (existsSync(TOWNS_DIR) ? readdirSync(TOWNS_DIR) : []).filter(f => f.endsWith('.json') && f !== 'index.json'); if (!townFiles.length) console.log(" (none yet — the contract + hardening are live, ready for E's build_towns.py)"); const ADDR = { towns: 0, shops: 0, withStreet: 0, edges: 0, edgesNamed: 0, ctrlChecked: 0, ctrlAgree: 0, ctrlDisagree: 0, allEdgeDisagree: 0, shiftExact: 0, waysSupplier: 0, worstTown: null }; for (const f of townFiles) { const key = f.replace(/\.json$/, ''); let cache; try { cache = JSON.parse(readFileSync(join(TOWNS_DIR, f), 'utf8')); } catch (e) { ok(false, `real/${key}: parses as JSON — ${e.message}`); continue; } const v = validateTownCache(cache); ok(v.ok, `real/${key}: valid cache` + (v.ok ? '' : ` — ${v.errors.join('; ')}`)); if (v.warnings.length) console.log(` ⚠ real/${key}: ${v.warnings.join(' · ')}`); if (!v.ok) continue; for (const s of OSM_SEEDS) structuralSuite(generatePlanOSM(s, key, { cache }), `real/${key} ${s}`); for (const s of GIG_SEEDS) districtInvariants(generatePlanFor(s, 'osm', { gigs: true, town: key, cache }), `real/${key} ${s}`); const hash = xmur3(JSON.stringify(generatePlanOSM(20261990, key, { cache })))() >>> 0; const want = REAL_TOWN_GOLDENS[key]; if (want === undefined) console.log(` ⚠ real/${key}: base UNPINNED — add REAL_TOWN_GOLDENS['${key}'] = 0x${hash.toString(16).padStart(8, '0')}`); else ok(hash === (want >>> 0), `real/${key}: base golden 0x${hash.toString(16)} matches pinned 0x${(want >>> 0).toString(16)}`); // ── ROUND39 item 39.1 — THE ADDRESS LAYER, per real town ───────────────────────────────────── // Every arm here reads a NUMBER, not a claim, and every arm has something that can make it fail. { const report = {}; const p = generatePlanOSM(20261990, key, { cache, report }); const before = JSON.stringify(p); const addr = createAddresses(p, cache); // recovery path: NO shift handed in const st = addr.stats(); // (i) NO PLAN MUTATION. The whole value of this item is that it is free; prove it locally, not // just by the golden downstream. If this fires, every pinned hash in this file is at risk. ok(JSON.stringify(p) === before, `real/${key}: createAddresses does not mutate the plan (byte-identical after the call)`); // (ii) the real-ways supplier ran. A silent fall to district labels on a real town is the failure // mode a consumer would never notice, so it is an assert, not a log. ok(st.supplier === 'ways', `real/${key}: address supplier is 'ways' (${st.supplier}) — ${st.namedWays}/${st.roads} named ways`); // (iii) THE CROSS-SOURCE ARM. `plan_osm` publishes its centring translation into the caller's // report sink; `address.js` re-derives the same number from the node↔waypoint lattice with // no knowledge of it. Two independent derivations compared against each other — if either // drifts, this fires. Asserted through the module's own cross-check field as well. const exact = !!(st.shift && report.shift && st.shift.shx === report.shift.shx && st.shift.shz === report.shift.shz); ok(exact, `real/${key}: shift recovered EXACTLY without the report (${st.shift ? `${st.shift.shx},${st.shift.shz}` : 'null'} vs plan_osm's ` + `${report.shift ? `${report.shift.shx},${report.shift.shz}` : 'NOTHING — plan_osm stopped publishing norm.shift'})`); ok(createAddresses(p, cache, { shift: report.shift }).stats().shiftCheck === 'agree', `real/${key}: opts.shift fast path agrees with the recovery (stats().shiftCheck)`); // (iv) THE WRONG-NAME ARM. A wrong street name is worse than no street name, so this is measured // against the independent membership resolver over exactly the edges a shop fronts. const ctrl = membershipStreets(p, cache, report.shift); const cmp = addressVsControl(p, addr, ctrl); ok(cmp.disagree === 0, `real/${key}: 0 wrong street names on shop-bearing edges — ${cmp.agree}/${cmp.checked} agree with the independent membership control` + (cmp.examples.length ? ` (${cmp.examples.join(' ; ')})` : '')); // (v) coverage. Per-town floor is loose on purpose (adelaide is the corpus worst at 80.9%, and it // is honestly worst — 21 of its 27 unresolved shops front an `arcade` edge, i.e. an OSM // footway with no name). The tight number is the corpus roll-up after this loop. const pctStreet = st.shops ? 100 * st.shopsWithStreet / st.shops : 0; ok(pctStreet >= 70, `real/${key}: ${st.shopsWithStreet}/${st.shops} shops resolve to a street (${pctStreet.toFixed(1)}%) · ${st.distinctStreets} distinct names`); // (vi) honesty of the null. Never a guess: a shop with no street must carry no label either. ok(addr.cohort(l => l.street === null && l.label !== null).length === 0, `real/${key}: an unresolved shop is labelled null, never guessed`); // (vii) THE CONTROL — the arm that proves the gate DISCRIMINATES. Feed the pre-change state: the // same cache with `roads[].name` stripped, which is exactly what `plan_osm.js:365` has been // handing downstream for 21 rounds. The layer must resolve ZERO streets and say so. const stripped = { ...cache, roads: (cache.roads || []).map(r => ({ kind: r.kind, pts: r.pts })) }; const blind = createAddresses(p, stripped).stats(); ok(blind.edgesNamed === 0 && blind.shopsWithStreet === 0 && blind.supplier === 'district', `real/${key}: CONTROL — with roads[].name stripped (the pre-change state) the layer resolves 0 streets, not ${blind.edgesNamed}`); // (viii) …and so does a caller who forgets the cache. Same failure, different cause, still loud. ok(createAddresses(p, null).stats().supplier === 'district', `real/${key}: CONTROL — no cache ⇒ district supplier (a consumer that drops the cache is visible in stats(), not silent)`); ADDR.towns++; ADDR.shops += st.shops; ADDR.withStreet += st.shopsWithStreet; ADDR.edges += st.edges; ADDR.edgesNamed += st.edgesNamed; ADDR.ctrlChecked += cmp.checked; ADDR.ctrlAgree += cmp.agree; ADDR.ctrlDisagree += cmp.disagree; ADDR.allEdgeDisagree += cmp.allDis; if (exact) ADDR.shiftExact++; if (st.supplier === 'ways') ADDR.waysSupplier++; if (!ADDR.worstTown || pctStreet < ADDR.worstTown[1]) ADDR.worstTown = [key, pctStreet]; } const ghash = xmur3(JSON.stringify(generatePlanFor(20261990, 'osm', { gigs: true, town: key, cache })))() >>> 0; const gwant = REAL_TOWN_GIG_GOLDENS[key]; if (gwant === undefined) console.log(` ⚠ real/${key}: gig UNPINNED — add REAL_TOWN_GIG_GOLDENS['${key}'] = 0x${ghash.toString(16).padStart(8, '0')}`); else ok(ghash === (gwant >>> 0), `real/${key}: gig golden 0x${ghash.toString(16)} matches pinned 0x${(gwant >>> 0).toString(16)}`); // ROUND25 ledger #1 — the identity gate, RECONCILED WITH RULING 2. A godverse cache is deliberately // MIXED: a keyed GODVERSE layer + an inherited OSM texture layer that G measured to be load-bearing. // So every assert runs over the GODVERSE LAYER ONLY (shops carrying an id); an unkeyed texture shop is // the design, not a defect. My R24 arm asserted every-shop-keyed and failed 54/72 on a healthy town — // it was asserting Ruling 2 away. Still written to the vacuous-gate law: names its subject, proves it // touched it with a count, SKIPs loudly rather than passing when the subject is absent. if (cache.source === 'godverse+osm') { const cacheKeyed = cache.shops.filter(s => Number.isSafeInteger(s && s.godverseShopId) && s.godverseShopId > 0); if (!cacheKeyed.length) { console.log(` ⊘ SKIP real/${key}: godverse identity — 0/${cache.shops.length} shops carry godverseShopId. ` + `Subject absent (the GODVERSE layer is empty). This gate binds the moment one id lands.`); } else { const seated = generatePlanOSM(20261990, key, { cache }).shops; const seatedIds = seated.filter(s => s.godverseShopId !== undefined).map(s => s.godverseShopId); const cacheIds = new Set(cacheKeyed.map(s => s.godverseShopId)); ok(seatedIds.length > 0, `real/${key}: GODVERSE layer reaches the plan — ${seatedIds.length}/${seated.length} seated shops keyed (${cache.shops.length - cacheKeyed.length} texture shops legitimately unkeyed)`); // uniqueness over DEFINED ids only: the R24 Set-collapse version folded every `undefined` into one // entry, so it failed structurally on a mixed cache AND could hide a real duplicate behind them. ok(new Set(seatedIds).size === seatedIds.length, `real/${key}: godverse ids unique on the plan (${new Set(seatedIds).size}/${seatedIds.length} distinct — no shared atlas)`); ok(seatedIds.every(id => cacheIds.has(id)), `real/${key}: every seated godverse id came from the cache (the lift invents no identity)`); // ORPHANED-ATLAS GATE. The lift legitimately drops shops (overlap-resolve, counted since R19) — but // dropping a shop that HAS a committed atlas means real stock keyed to a shop that isn't in the // town: exactly the failure R24 predicted and F's #7b would only catch at the end of a round. // Subject = the atlas dirs on disk, so it SKIPs by name when this town has none. const atlasIds = (existsSync(STOCK_GODVERSE_DIR) ? readdirSync(STOCK_GODVERSE_DIR) : []) .filter(d => /^\d+$/.test(d)).map(Number).filter(id => cacheIds.has(id)); if (!atlasIds.length) { console.log(` ⊘ SKIP real/${key}: orphaned-atlas check — no committed atlas under stock_godverse/ keys to this town.`); } else { const seatedSet = new Set(seatedIds); const orphans = atlasIds.filter(id => !seatedSet.has(id)); ok(orphans.length === 0, `real/${key}: no orphaned atlas — ${atlasIds.length} stocked shop(s) on disk, all seated` + (orphans.length ? ` (ORPHANED: ${orphans.join(', ')} — real stock keyed to a shop the lift dropped)` : '')); } // a dropped keyed shop is legitimate but must never be silent (the R19/R20 drop-and-count ruling) const dropped = cacheKeyed.filter(s => !new Set(seatedIds).has(s.godverseShopId)); if (dropped.length) console.log(` ⚠ real/${key}: ${dropped.length} keyed shop(s) dropped by the lift — ${dropped.map(s => `${s.godverseShopId} "${s.name}"`).join(', ')} (unstocked today; would orphan an atlas if stocked)`); } } } // ── 3g. THE ADDRESS LAYER (ROUND39 item 39.1) — corpus roll-up + the synthetic supplier ───────── // The per-town arms live inside the 3f loop (where the cache is already parsed). This is the number // Lane F's gate quotes, plus the OTHER supplier — the one with no cache and no named way in sight. section('the address layer (ROUND39 39.1: streetOf / localityOf / cohort)'); if (ADDR.towns) { const pct = 100 * ADDR.withStreet / ADDR.shops; ok(pct >= 97, `corpus: ${ADDR.withStreet}/${ADDR.shops} shops resolve to a real street name (${pct.toFixed(1)}%) across ${ADDR.towns} caches`); ok(ADDR.shiftExact === ADDR.towns, `corpus: the plan shift is recovered EXACTLY on ${ADDR.shiftExact}/${ADDR.towns} towns with no help from plan_osm`); ok(ADDR.waysSupplier === ADDR.towns, `corpus: ${ADDR.waysSupplier}/${ADDR.towns} towns resolve through real named ways`); ok(ADDR.ctrlDisagree === 0, `corpus: ${ADDR.ctrlAgree}/${ADDR.ctrlChecked} shop-bearing edges agree with the independent membership control, ${ADDR.ctrlDisagree} wrong`); // The ALL-EDGE disagreement count is PINNED rather than asserted zero, for the R37 reason: a control // allowed to drift is how a gate goes vacuous. Four edges in 30,986 disagree, and all four are the // same honest thing — two named ways genuinely stacked in 2D, where "which street is this?" has no // single answer: geelong's two adjacent malls (Little Malop Street Mall / Market Square Mall), and a // motorway crossing OVER a street on glebe (Western Distributor / Bank Street) and redhill ×2 // (Legacy Way / Guthrie Street). NONE of the four carries a shop, which is why the arm above is 0. // If a cache is rebuilt this re-pins loudly, exactly like the 23 goldens above. const PINNED_ALL_EDGE_DISAGREEMENTS = 4; ok(ADDR.allEdgeDisagree === PINNED_ALL_EDGE_DISAGREEMENTS, `corpus: exactly ${PINNED_ALL_EDGE_DISAGREEMENTS} of ${ADDR.edgesNamed} named edges disagree with the control (measured ${ADDR.allEdgeDisagree}) — all four are 2D-stacked ways carrying no shop`); console.log(` address: ${ADDR.edgesNamed}/${ADDR.edges} edges named (${(100 * ADDR.edgesNamed / ADDR.edges).toFixed(1)}%) · ` + `${ADDR.withStreet}/${ADDR.shops} shops (${pct.toFixed(1)}%) · tolerance ${STREET_TOLERANCE_M} m · worst town ${ADDR.worstTown[0]} ${ADDR.worstTown[1].toFixed(1)}%`); } else { console.log(' ⊘ SKIP corpus address roll-up — no town caches on disk. This gate binds the moment one lands.'); } // ── the SYNTHETIC supplier: no cache, no roads[], 22 edges. Same fields, same contract. ────────── // The law this section exists to protect: a consumer must be able to read `label` on either town type // without ever testing `plan.source`. So the synthetic must fill `label` for EVERY shop, from // `district.kind` + block, while leaving `street` honestly null. for (const s of [20261990, 42]) { const plan = generatePlan(s); const before = JSON.stringify(plan); const addr = createAddresses(plan, null); const st = addr.stats(); ok(JSON.stringify(plan) === before, `addr syn ${s}: createAddresses does not mutate the plan`); ok(st.supplier === 'district', `addr syn ${s}: the district supplier runs (no cache, no named ways)`); ok(st.shopsWithStreet === 0 && addr.streets().length === 0, `addr syn ${s}: no street NAMES are invented (${st.shopsWithStreet} claimed)`); ok(st.shopsLabelled === plan.shops.length, `addr syn ${s}: every one of ${plan.shops.length} shops carries a label (${st.shopsLabelled})`); const locs = plan.shops.map(sh => addr.localityOf(sh.id)); ok(locs.every(l => l && typeof l.label === 'string' && l.label && !/undefined|null|\[object/.test(l.label)), `addr syn ${s}: every label is a resolved phrase (no undefined/null leaking into player-facing text)`); ok(locs.every(l => l.district && DISTRICT_KINDS.includes(l.district)), `addr syn ${s}: every locality names a registry district kind`); ok(locs.every(l => Number.isInteger(l.block) && ['north', 'south', 'east', 'west'].includes(l.side)), `addr syn ${s}: every locality carries an integer block and a cardinal side`); // the two sides of a street must be TWO tokens, or `side` adds nothing to a clue ok(new Set(locs.map(l => l.side)).size >= 2, `addr syn ${s}: side-of-street discriminates (${[...new Set(locs.map(l => l.side))].join('/')})`); ok(JSON.stringify(JSON.parse(JSON.stringify(locs))) === JSON.stringify(locs), `addr syn ${s}: localities are JSON round-trip lossless`); ok(addr.localityOf(999999) === null && addr.localityOf(undefined) === null, `addr syn ${s}: an unknown shop id returns null, it does not throw`); // cohort: ids ascending, a subset of the plan's shops, and the predicate is actually consulted const all = addr.cohort(() => true), none = addr.cohort(() => false); const shopIds = new Set(plan.shops.map(sh => sh.id)); ok(all.length === plan.shops.length && none.length === 0, `addr syn ${s}: cohort spans the town (${all.length}) and can select nothing (${none.length})`); ok(all.every((v, i) => i === 0 || all[i - 1] < v) && all.every(id => shopIds.has(id)), `addr syn ${s}: cohort returns real shop ids, ascending, no duplicates`); const market = addr.cohort(l => /market/.test(l.label)); ok(market.length > 0 && market.length < plan.shops.length, `addr syn ${s}: a district cohort is a real subset — "the market end" holds ${market.length} of ${plan.shops.length} shops`); ok(JSON.stringify(createAddresses(plan, null).cohort(l => /market/.test(l.label))) === JSON.stringify(market), `addr syn ${s}: deterministic across two constructions`); } { // the shape of the synthetic's cells, printed so the round can argue about it with numbers const plan = generatePlan(GOLDEN.seed); const addr = createAddresses(plan, null); const byLabel = new Map(); for (const sh of plan.shops) { const k = addr.localityOf(sh.id).label; byLabel.set(k, (byLabel.get(k) || 0) + 1); } const cells = [...byLabel.values()].sort((a, b) => a - b); console.log(` synthetic: ${byLabel.size} label cells over ${plan.shops.length} shops — median ${cells[cells.length >> 1]}, max ${cells[cells.length - 1]} ` + `(e.g. "${[...byLabel.keys()].sort()[0]}"); block cells are finer — use \`block\` when a label is too coarse`); } // ── 4. every facade skin referenced by the registry exists on disk ────────────────── section('assets on disk'); for (const f of allFacadeSkins()) ok(existsSync(join(ASSETS, f)), `registry facade exists: ${f}`); for (const f of facadeSet) ok(existsSync(join(ASSETS, f)), `used facade exists: ${f}`); // ── report ────────────────────────────────────────────────────────────────────────── console.log(`\n${failures ? '✗ FAIL' : '✓ ALL GREEN'} — ${checks - failures}/${checks} checks passed`); const sample = generatePlan(20261990); console.log(` sample seed 20261990 → "${sample.name}": ` + `${sample.streets.nodes.length} nodes, ${sample.streets.edges.length} edges, ` + `${sample.blocks.length} blocks, ${sample.lots.length} lots, ${sample.shops.length} shops`); console.log(` fingerprint hash: 0x${(xmur3(JSON.stringify(sample))() >>> 0).toString(16)} (paste into GOLDEN.hash)`); process.exit(failures ? 1 : 0);