PROCITY/web/js/citygen/selfcheck.js
m3ultra f628bf581a Lane A R39 39.1: THE ADDRESS LAYER — 17,835 street names stop being dropped on the floor
The contract, published for B and C in LANE_A_NOTES §39 (first commit, per the half-day rule).

  createAddresses(plan, cache|null) → streetOf(edgeId) · localityOf(shopId) · cohort(pred)
                                     · streets() · stats()

New pure module web/js/citygen/address.js: ZERO imports, no THREE, no fetch, no DOM, no module
state, no plan mutation. Two suppliers, ONE consumer contract — a real town fills `label` from the
cache's named ways, the synthetic fills it from district.kind + block. Consumers must never branch
on town type; that constraint is the design.

MEASURED (Fable's binding facts re-derived independently, all three land exactly):
· 19,132 road ways, 17,835 named (93.2%)
· median plan-edge-sample → nearest named way 0.03–0.90 m (worst katoomba)
· 1,192 / 1,219 corpus shops resolve to a street name (97.8%); 29,865/30,986 edges named (96.4%)
· all 27 unresolved shops front a way OSM genuinely leaves unnamed (adelaide 21 arcade) — null is
  the honest answer and the module returns it rather than borrowing a neighbour's name

TWO DEPARTURES FROM THE SYNTHESIS, both with numbers:
· the shift is recovered EXACTLY (node↔waypoint lattice intersection), not voted for. The proposed
  centroid-align + nearest-waypoint vote returns the WRONG shift on 5 of 23 towns (braddon,
  fremantle, hobart, northbridge, westend) — island culling drags the plan centroid off the
  cache's. A wrong shift renames every street in the town. Exact on 23/23, nothing to tune.
· the resolver samples FIVE points along an edge, not the midpoint — a midpoint cannot tell a
  street from the street that crosses it.

THE ONE-LINE UPGRADE: TAKEN. plan_osm.js writes norm.shift = {shx,shz} into the normalization log,
reaching the caller only via the existing opts.report sink — never onto the plan. It is a
cross-check, not a dependency: selfcheck compares plan_osm's published shift against address.js's
independently recovered one and fires on a 0.01 m disagreement.

GATES (+240 checks), each proven to fire on a broken world:
· 0 wrong street names on 884 shop-bearing edges, against an INDEPENDENT way-membership resolver
  (857 agree, 0 disagree). Corpus-wide 4/30,986 disagree — all 2D-stacked ways, none carrying a
  shop — PINNED at 4, not asserted > 0.
· CONTROL: with roads[].name stripped (exactly the pre-change state) the layer resolves 0 streets.
· CONTROL: no cache ⇒ supplier 'district', visible in stats(), never a silent wrong name.
· createAddresses does not mutate the plan — asserted byte-for-byte, per town.
· tolerance 8 m, and the finding that sets it: correctness SATURATES AT 6 m (857 agreeing frontage
  names at 6, 8, 12, 16 and 24 m). Every metre past 6 buys only a name borrowed from an unnamed
  way; it can never buy a correction.

Additive: getTownCache(key) — index.html registers the cache and drops its reference, so roads[]
was unreachable downstream. Read-only, no new state, no shell change.

GOLDENS: NOTHING MOVED, ZERO RE-PINS. selfcheck 157,407 → 157,647 ALL GREEN, fingerprint 0x5f76e76.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:32:43 +10:00

999 lines
71 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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: 24 venues, kinds from the registry, never the openLate landmark ──
ok(venues.length >= 2 && venues.length <= 4, `${label}: 24 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`);
// ~46 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 46 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 12 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}: 24 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 24 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 24 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/<id>/`. 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);