From a3911a45900524f08e7bdeb7d10963d922100064 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Thu, 16 Jul 2026 08:36:31 +1000 Subject: [PATCH] =?UTF-8?q?Lane=20A=20round=2014:=20v3.0=20release=20?= =?UTF-8?q?=E2=80=94=20400-seed=20district=20invariant=20sweep=20+=20CITY?= =?UTF-8?q?=5FSPEC=20=C2=A7v3=20frozen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release round: harden + freeze, no new systems. No golden moved (synthetic 0x3fa36874, gig 0x1f636349 both re-verified frozen); selfcheck.js is the only code touched, so no seeded generation stream gained a draw (release-round law). - selfcheck.js: factored the district checks out of gigSuite into districtInvariants(plan,label) (behaviour-preserving — hero-seed gigSuite runs byte-identical), then added a 400-seed synthetic sweep + the large edge seeds asserting the full district contract each (2-4 venues, >=1 pub, never the openLate shop, no band twice/night town-wide, per-venue 4-6 nights, every poster off the carriageway). Asserts the seeded count spans {2,3,4} across 400 seeds (146/121/133 — range is exercised, not pinned). osm graceful placement: katoomba (19 shops, no warehouse district) still lands 2-4 valid venues incl. a pub for every hero seed. Poster check kept at roadWidth/2 (a literal poleOffset compare would false-fail corner posters); added -1e-6 epsilon on the strict boundary. ALL GREEN 14264/14264 in ~0.7s. - CITY_SPEC §v3: final wording pass, marked FROZEN at v3.0 (round 14) with a v2-style marker + an explicit carve-out for F's pending alias-line deletion. Fixed the roadWidth prose (full carriageway width, not 'half-corridor'). Genres final (pubrock/grunge/covers). Coordination: the R12 single-venue alias line stays in §v3 for F to delete atomically with the gig_state.js getters (debt #1) — deleting now would be doc-ahead-of-code (alias still live) and race F. Flagged to F that the runtime block omits nightOf(id); to B that venue.js:228 is a 2nd bare-string site. Verified: mutation-tested the sweep (non-vacuous — catches 5-venue/7-night/on-road violations) + adversarial pass (goldens frozen, freeze matches code, deterministic). Co-Authored-By: Claude Opus 4.8 --- docs/A-progress.md | 38 ++++++++++++++++++++++++++++++---- docs/CITY_SPEC.md | 13 ++++++++++-- docs/LANES/LANE_A_NOTES.md | 41 +++++++++++++++++++++++++++++++++++++ web/js/citygen/selfcheck.js | 41 +++++++++++++++++++++++++++++++++++-- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/docs/A-progress.md b/docs/A-progress.md index 81608b2..6a7945c 100644 --- a/docs/A-progress.md +++ b/docs/A-progress.md @@ -1,9 +1,39 @@ # LANE A — CITYGEN · progress (PROCITY-A) -*Status: **all deliverables landed; self-check all-green (3273/3273); rounds 2–13 closed** (see below). -Node QA gates (selfcheck · scaffold · consistency) GREEN. v3.0-beta DISTRICT interface published (2–4 -venues, week schedule, town-wide posters); gig golden `0x1f636349`. Strict 6/6 is F's end-of-round gate.* -*Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)* +*Status: **all deliverables landed; self-check all-green (14264/14264); rounds 2–14 closed** (see below). +v3.0 RELEASE round: 400-seed district invariant sweep landed + CITY_SPEC §v3 frozen at `v3.0`. Goldens +unchanged (synthetic `0x3fa36874`, gig `0x1f636349`) — no golden moved, no rng draw added this round.* +*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)* + +## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 freeze + +Release round, Lane A scope = harden + freeze (no new systems). **No golden moved** (synthetic +`0x3fa36874`, gig `0x1f636349` both re-verified frozen); the only code touched is `selfcheck.js` +(test-only), so no seeded stream gained a draw — the release-round law holds. + +Delivered: +1. **400-seed district invariant sweep** (`selfcheck.js`, ALL GREEN **14264/14264** in ~0.7s). Factored + the district checks out of `gigSuite` into `districtInvariants(plan,label)` (behaviour-preserving — the + 4 hero-seed gigSuite runs are byte-identical), then swept **seeds 1–400 + the large edge seeds** + asserting the whole contract each: 2–4 venues, ≥1 pub, never the openLate shop, no band twice per night + town-wide, per-venue 4–6 nights, every poster off the carriageway. Added a distribution assert that the + seeded count **spans {2,3,4}** across 400 seeds (146/121/133) — proves the range is exercised, not pinned. + `structuralSuite` still runs on the 6 hero SEEDS + osm parity (unchanged); the sweep runs the light + district checks only, so 400 gig-on generations stay ~0.5s. +2. **osm graceful placement** in the sweep: `katoomba` (19 shops, **no warehouse district**) still lands + 2–4 valid venues incl. a pub for every hero seed — proving `pickVenues`' off-spine fallback. Honest + scope noted: both osm fixtures keep a main spine, so this proves the no-warehouse branch (not no-spine). +3. **Poster-check hardening**: kept the `roadWidth/2` nearest-edge formulation (a literal `poleOffset` + compare would false-fail corner posters); added a `−1e-6` epsilon on the strict `<` to immunise the + exact kerb-line boundary against r2 rounding (only reachable on a no-main-spine fallback; not hit today). +4. **CITY_SPEC §v3 frozen at `v3.0`** — §v2-style FROZEN marker, final wording pass over venues table, + genre map, gigKey contract, corridor law, gig golden. Genres final (pubrock/grunge/covers, Fable's ruling). + +Coordination: the R12 single-venue **alias line stays in §v3 for F to delete atomically** with the +`gig_state.js` getters (debt #1) — deleting it now would be doc-ahead-of-code (alias still live) and would +race F on the same lines; the freeze marker carves it out explicitly. Flagged to F that the runtime block +omits `nightOf(id)`, and to B that `venue.js:228` is a second bare-string edit site. **Not touched:** +`web/assets/manifest.json` shows a concurrent Lane E `glb_law` edit in the shared tree — left for E. ## Round 13 (2026-07-15) — v3.0-beta THE DISTRICT: 2–4 venues + the week + town-wide posters + verge fix diff --git a/docs/CITY_SPEC.md b/docs/CITY_SPEC.md index 2f50cfd..221af70 100644 --- a/docs/CITY_SPEC.md +++ b/docs/CITY_SPEC.md @@ -204,7 +204,15 @@ Interiors are **not** street geometry. Walking into a doorway triggers exactly where you stood. Street windows show a cheap fake (tinted glass + emissive warm glow at night in v1; interior-mapping shader is a stretch goal). -## CityPlan v3 — gig layer (`?gigs=1`) — v3.0-beta (THE DISTRICT) +## CityPlan v3 — gig layer (`?gigs=1`) — THE DISTRICT + +> **FROZEN at `v3.0` (round 14).** The authoritative Layer-1 **gig-layer** contract — venues, the week's +> schedule, posters, the `gigKey` map, the corridor law, and the runtime surface. `web/js/citygen/selfcheck.js` +> enforces every law here, including the round-14 **400-seed district invariant sweep** + an osm +> graceful-placement case. Changes are a **CITY_SPEC amendment in the same commit + a gig-golden re-pin**, +> never silent drift. **One edit is still pending and lands with Lane F's alias-retirement commit** (R13 +> debt #1): the R12 single-venue compat alias in the runtime block below retires; everything else here is +> final for the `v3.0` tag. > **Prime flag law.** The gig layer is a **post-hoc augmentation** applied by the selector only when > `?gigs=1`. With the flag OFF, `generatePlanFor(seed, src)` returns the byte-identical v2 base plan — @@ -281,7 +289,8 @@ places E's poster skin at `(x, z)` rotated `ry`; F wires "poster → that venue' the **full corridor** (carriageway + verge/footpath, centreline ± width/2), never the carriageway alone — so a naive `node.x + 2` seats furniture mid-bitumen. `web/js/core/registry.js` now exports the split as pure functions over an edge (no plan fields change ⇒ base goldens stay frozen), re-exported from the -barrel: **`roadWidth(edge)`** (carriageway half-corridor: main 10 m, side 6–8, lane = all, arcade 0), +barrel: **`roadWidth(edge)`** (full carriageway width — main 10 m, side 6–8, lane = all, arcade 0; the +kerb sits at `roadWidth/2` from the centreline), **`vergeBand(edge) → [inner,outer]`** (where footpath furniture lives), **`poleOffset(edge)`** (where a pole/poster seats — just behind the kerb). Posters, poles, verandah posts belong in the verge band; the selfcheck asserts no poster stands within `roadWidth/2` of any edge centreline. diff --git a/docs/LANES/LANE_A_NOTES.md b/docs/LANES/LANE_A_NOTES.md index e6bfc53..157edf2 100644 --- a/docs/LANES/LANE_A_NOTES.md +++ b/docs/LANES/LANE_A_NOTES.md @@ -2,6 +2,47 @@ Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`. +## Round 14 (2026-07-16) — v3.0 RELEASE: invariant sweep + CITY_SPEC §v3 frozen + +Release round — no new systems, no golden moves. **Synthetic `0x3fa36874` and gig `0x1f636349` are +unchanged**; the only code I touched is `selfcheck.js` (test-only), so no seeded stream gained a draw. + +### The 400-seed district sweep (`selfcheck.js`, ALL GREEN 14264/14264 in ~0.7s) +- Factored the district-contract checks out of `gigSuite` into `districtInvariants(plan, label)` so the + sweep asserts them at SCALE without re-running the heavy `structuralSuite` (that still runs on the 6 + hero `SEEDS` + osm parity). Behaviour-preserving — the 4 hero-seed `gigSuite` runs are byte-identical. +- Sweeps **seeds 1–400** synthetic + the large edge seeds (2e9, 8675309) asserting the full district + contract each: 2–4 venues, ≥1 pub, never the openLate shop, no band twice in one night town-wide, + per-venue 4–6 nights, every poster off the carriageway. Also asserts the seeded count **spans {2,3,4}** + across the range (measured 146/121/133) — proves the 2–4 range is genuinely exercised, not pinned. +- **osm graceful placement**: `katoomba` (smallest fixture, 19 shops, **no warehouse district**) still + lands 2–4 valid venues incl. a pub for every hero seed — proving `pickVenues`' off-spine fallback. + Honest scope: both osm fixtures keep a main spine, so this proves the *no-warehouse* branch, not + *no-spine* (a genuine no-spine town would need a single-row fixture that doesn't exist yet — v3.1). +- Poster check kept as "≥ `roadWidth/2` from the nearest edge centreline" (not a literal `poleOffset` + compare, which would false-fail legit corner posters seated behind their placement edge's `poleOffset` + but inside a crossing edge's clearance band). Added a `−1e-6` epsilon on the strict `<` to immunise the + exact kerb-line boundary against r2 rounding (only reachable on a no-main-spine fallback; not hit today). + +### CITY_SPEC §v3 frozen at v3.0 +Final wording pass; marked **FROZEN at `v3.0` (round 14)** with a §v2-style marker. Venues table, genre +map, gigKey contract, corridor law, and gig golden `0x1f636349` are all final (genres are pubrock/grunge/ +covers — Fable's R14 ruling, John did not re-flavour). + +### → Lane F: the alias-retirement handshake (R13 debt #1) — I deliberately did NOT touch it +The R12 single-venue compat alias is **still live** (`gig_state.js:141-151`, read by `audio.js`/`venue.js`), +so I left the §v3 alias sentences (`CITY_SPEC.md` ~lines 271–272) **intact** — deleting them at freeze +time would be doc-ahead-of-code and would race you on the same lines. The freeze marker explicitly carves +this out as your pending edit. In your alias-retirement commit (after B drops its readers), delete both +`gig_state.js:141-151` and those two spec sentences **together** — one atomic step, one deleter, no race. +- While you're in that runtime block: it enumerates `stateOf/onOf/openOf/gigOf/coverOf/bandNameOf/paidOf` + + `markPaidOf` + `.venueShopIds`, but **omits `nightOf(id)`** (`gig_state.js:131`). Add it when you edit. + +### → Lane B: second edit site for the bare-string arm +When you remove the `venue.update()` bare-string alias arm, note `venue.js:228` also self-calls +`update('quiet')` at construction through that same arm — fix both or it falls through (harmless today: +still yields `'quiet'`, but it's a live second site). + ## Round 13 (2026-07-15) — v3.0-beta THE DISTRICT (published FIRST; C/D/B/F hang off this) Supersedes the R12 alpha interface below. Same flag (`?gigs=1`), same prime-flag law: flag OFF ⇒ plan diff --git a/web/js/citygen/selfcheck.js b/web/js/citygen/selfcheck.js index 8260cda..9765aa0 100644 --- a/web/js/citygen/selfcheck.js +++ b/web/js/citygen/selfcheck.js @@ -145,7 +145,13 @@ function structuralSuite(plan, label) { // 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 coverage; venues exempt from late-shop rule + 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])); @@ -199,7 +205,9 @@ function gigSuite(plan, label) { const d = Math.hypot(p.x - nx, p.z - nz); if (d < best) { best = d; bestEdge = e; } } - if (bestEdge && best < roadWidth(bestEdge) / 2) { onRoad = [p.id, bestEdge.id, +best.toFixed(2)]; break; } + // −1e-6 immunises the exact kerb-line boundary against r2 coord rounding (only reachable on a + // no-main-spine fallback where poleOffset == roadWidth/2; never hit by the current sweep). + if (bestEdge && best < roadWidth(bestEdge) / 2 - 1e-6) { onRoad = [p.id, bestEdge.id, +best.toFixed(2)]; break; } } ok(onRoad === null, `${label}: no poster stands on a carriageway` + (onRoad ? ` (poster ${onRoad[0]} is ${onRoad[2]}m from edge ${onRoad[1]}'s centreline)` : '')); } @@ -294,6 +302,35 @@ const GIG_GOLDEN = 0x1f636349; // ROUND13 re-pin: multi-venue district changed t ok(GIG_GOLDEN !== 0 && hash === (GIG_GOLDEN >>> 0), `gig layer golden 0x${hash.toString(16)} matches pinned 0x${(GIG_GOLDEN >>> 0).toString(16)}`); } +// ── 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: katoomba is the smallest fixture (19 shops) with NO warehouse district, so +// pickVenues' band_room/rsl rules hit their off-spine fallback — yet still land 2–4 valid venues incl. +// a pub. (Both osm fixtures keep a main spine, so this proves the no-warehouse branch, not no-spine.) +for (const seed of GIG_SEEDS) { + const plan = generatePlanFor(seed, 'osm', { gigs: true, town: 'katoomba' }); + ok(plan.blocks.every(b => b.kind !== 'warehouse'), + `sweep osm/katoomba ${seed}: fixture has no warehouse district (graceful-placement precondition)`); + districtInvariants(plan, `sweep osm/katoomba ${seed}`); +} + // ── 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}`);