diff --git a/docs/A-progress.md b/docs/A-progress.md index 8a1d45e..45afb59 100644 --- a/docs/A-progress.md +++ b/docs/A-progress.md @@ -1,9 +1,9 @@ # LANE A — CITYGEN · progress (PROCITY-A) -*Status: **all deliverables landed; self-check all-green (51675/51675); rounds 2–19 closed, R20 poster-cap -landed** (see below). v4.0-beta: spine-poster cap scaled by shop count (katoomba 438→31, closes F's -venue_night edge); real-town gig goldens pinned. classic/gig/marched-fixtures frozen. Density absorb pends -E's widened caches.* +*Status: **all deliverables landed; self-check all-green (51866/51866); rounds 2–20 closed** (see below). +v4.0-beta: spine-poster cap scaled by shop count (katoomba 438→31, closes F's venue_night edge) + E's +density widening absorbed (5 towns at 3–6× shops, all place 3 venues, drops 5.5% counted by cause); all 10 +real-town goldens (base + gig) pinned. classic/gig/marched-fixtures frozen.* *Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)* ## Round 20 (2026-07-16) — v4.0-beta THE FIVE TOWNS: the poster cap (density absorb pends E) @@ -20,10 +20,20 @@ Two Lane-A deliverables; the first (independent) landed this commit, the second `REAL_TOWN_GIG_GOLDENS` (the full district+poster plan per town: katoomba `0xbd332e83`, …) to regression-guard the capped output byte-exact. selfcheck ALL GREEN **51675/51675**; scaffold + consistency GREEN; cross-process deterministic. → F: re-measure `venue_night`, the +965 edge should close. -2. **Density absorb (ledger #2) — PENDING E.** E re-fetches general retail (C's class→archetype table) and - re-emits the five caches at 3–6× shop count. When they land I load-test seating/overflow/venue placement - and re-pin the real goldens once. The poster cap already reads the live shop count → auto-scales to the - widened towns. +2. **Density absorb (ledger #2) — LANDED.** E's five caches at **3–6× shops** (292 total: katoomba 20→80, + fremantle 21→80, newtown 18→72, bendigo 9→36, castlemaine 6→24) absorbed; **all five place 3 venues**; + the poster cap auto-scaled off the live shop count as designed. **Capacity widened** — at 4× the dominant + drop cause was overflow (edge-side full), so `NODE_CLEAR` 6→3 + a **roads-only `RGAP`** 2→0.5 (shared + marched `GAP` untouched ⇒ fixtures frozen): drops **7.9% → 5.5%** (16/292), residual **counted by cause** + (`dropped_overflow`/`_overlap`/`_stranded`). Stopped there deliberately — the next lever (spilling + overflow onto a different street) buys ~1% by putting a shop on a road it isn't on. **Venue facade bias + extended to `pub`** (R16 exempted it; on a real graph at density the pub's frontage poster overhung a + cross street — newtown −0.91 m); still a no-op on synthetic. **Goldens re-pinned once** (base + gig, all + five). **Fixed:** the real-cache loader now skips E's new `index.json` (a selector index, not a cache). + selfcheck ALL GREEN **51866/51866**; scaffold + consistency GREEN; cross-process deterministic. + +Handshake fired → all: **densities absorbed, goldens pinned.** → F: re-measure `venue_night` (poster cap) +and gate the five-town matrix. ## Round 19 (2026-07-16) — v4.0-alpha CLOSE: fragmentation ruling + findings hardened + goldens pinned diff --git a/docs/LANES/LANE_A_NOTES.md b/docs/LANES/LANE_A_NOTES.md index 6d5d92b..9d499cb 100644 --- a/docs/LANES/LANE_A_NOTES.md +++ b/docs/LANES/LANE_A_NOTES.md @@ -17,12 +17,26 @@ Posters live in the **gig layer** (`withGigs`), not the base `plan_osm` output district + capped posters) — katoomba `0xbd332e83`, etc. Synthetic gig golden `0xb1d48ea1` **frozen** — the cap never binds on the shop-dense synthetic town (493 shops ⇒ cap ~740 ≫ its ~15 posters). -### Density absorb — PENDING E's widened caches (ledger #2) -E re-fetches general retail (per C's class→archetype table) and re-emits the five caches at 3–6× shop count. -When they land (E→A "caches re-emitted"), I load-test seating capacity + overflow accounting (`norm.dropped` -stays honest) + venue placement, then re-pin the real goldens **once** (base + gig). The R18 overflow fence -is exactly the load-bearing part now — I'll widen edge capacity / split runs if a dense edge overwhelms. My -poster cap already reads the LIVE shop count, so it auto-scales to the widened towns (no change needed). +### → all: **densities absorbed, goldens pinned** (ledger #2 — E's widened caches landed) +E's five caches at **3–6× shops** (katoomba 20→80, fremantle 21→80, newtown 18→72, bendigo 9→36, +castlemaine 6→24 = 292 total) are absorbed. **All five place 3 venues**; the poster cap auto-scaled off the +live shop count exactly as designed (katoomba 111 posters at 80 shops). selfcheck **ALL GREEN 51866/51866**. +- **Capacity widened** (the R18 overflow fence, load-tested). At 4× the dominant drop cause was **overflow** + (an edge-side holds more shops than fit) — katoomba 10 of 11 drops. Tightened the run: `NODE_CLEAR` 6→3 + and a **roads-only `RGAP` 2→0.5** (the shared marched `GAP` is untouched ⇒ marched fixtures stay frozen). + Drops **7.9% → 5.5%** (16/292). 0.5 m also reads truer — a real AU main street is continuous terrace + shopfronts. Residual drops stay **counted, now by cause** (`dropped_overflow` / `_overlap` / `_stranded`). + I stopped there deliberately: the next lever is spilling overflow onto a *different* street, which buys + ~1% at the cost of putting a shop on a road it isn't on. Dropping a crowded shop is the honest trade. +- **Venue facade bias now covers `pub`.** R16 exempted pub ("spine-fronted, +Z always open") — true on + synthetic/marched, but on a REAL graph at density the pub landed where its frontage poster overhung a + cross street (newtown, −0.91 m). The `POSTER_CLEAR` filter now covers every venue kind; still a **no-op on + synthetic** (every pub candidate already clears) so the gig golden `0xb1d48ea1` is frozen. +- **Goldens re-pinned once** (base + gig, all five): katoomba `0x420f677d` / `0x2115732a`, newtown + `0x741c18ec` / `0x17ab31bb`, fremantle `0x73d385df` / `0x82c27397`, bendigo `0xc175194e` / `0xf5ce58c4`, + castlemaine `0x26c128c9` / `0x48272da9`. classic/gig/marched fixtures **frozen**. +- **Fixed:** my real-cache loader globbed `*.json` and tried to validate E's new `index.json` as a town + cache. It now skips it (it's B's selector index, not a cache). ## Round 19 (2026-07-16) — the alpha close: fragmentation resolved + findings hardened + goldens pinned diff --git a/web/js/citygen/gigs.js b/web/js/citygen/gigs.js index 7763550..d6b1bc0 100644 --- a/web/js/citygen/gigs.js +++ b/web/js/citygen/gigs.js @@ -125,12 +125,13 @@ function pickVenues(plan, citySeed) { if (far.length) cand = far; } } - // ROUND16 ledger #6: keep the off-spine kinds off a back-street's bitumen. Prefer candidates whose - // +Z facade clears every carriageway by ≥ POSTER_CLEAR; if the warehouse fringe is uniformly narrow, - // broaden to ANY off-spine clear lot before falling back. (pub is spine-fronted — its +Z is open — - // so it keeps its placement untouched; the hero shot doesn't move.) Always a clear lot exists in the - // synthetic town; the fallback only ever fires on a degenerate/osm plan. - if (kind === 'band_room' || kind === 'rsl') { + // ROUND16 ledger #6 (widened ROUND20): keep a venue's frontage poster off a crossing street's bitumen. + // Prefer candidates whose +Z facade clears every carriageway by ≥ POSTER_CLEAR; if the primary set has + // none, broaden to any off-spine clear lot before falling back. R16 exempted `pub` because on the + // synthetic/marched towns its +Z is always open — but on a REAL road graph at density the pub can land + // where its facade overhangs a cross street (newtown, R20). The filter now covers every venue kind: on + // the synthetic town every pub/rsl candidate already clears, so it stays a no-op there (golden frozen). + { let clear = cand.filter(s => facadeClear(s) >= POSTER_CLEAR); if (!clear.length) clear = pool.filter(s => !onMain(s) && facadeClear(s) >= POSTER_CLEAR); if (clear.length) cand = clear; diff --git a/web/js/citygen/plan_osm.js b/web/js/citygen/plan_osm.js index 60095b2..c3d8caa 100644 --- a/web/js/citygen/plan_osm.js +++ b/web/js/citygen/plan_osm.js @@ -238,9 +238,18 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) { // stringent structuralSuite still holds). Fidelity is the alpha knob (charter risk #4): real edge, // real order, real side, regularised spacing — not pixel positions (real shops overlap). function buildRealRoads() { - const EPS = 6, SNAP = 3, KERB = 4, NODE_CLEAR = 6; // fidelity + geometry knobs (metres) + // ROUND20 capacity (the density widening load-tested the R18 overflow fence): at 3–6× shops the + // dominant drop cause was OVERFLOW — more shops assign to an edge-side than fit. Widened by tightening + // the run: NODE_CLEAR 6→3 and a roads-only RGAP 2→0.5 (the shared marched `GAP` is untouched, so the + // marched fixtures stay frozen). 0.5 m also reads truer — a real AU main street is continuous terrace + // shopfronts, not detached boxes. Drops 7.9% → 5.5% across the five; the residual stays COUNTED, and + // dropping a crowded shop beats teleporting it onto a street it isn't on. + const EPS = 6, SNAP = 3, KERB = 4, NODE_CLEAR = 3, RGAP = 0.5; // fidelity + geometry knobs (metres) const JOIN_TOL = 12, BRIDGE_MAX = 200, SEAT_MAX = 200; // ROUND19 fragmentation ruling (metres) const WKIND = { main: SPINE_W, side: AV_W, lane: 8, arcade: 6 }; + // every dropped shop is counted (Fable's ROUND19 ruling) — and counted BY CAUSE, so the density + // accounting stays honest as real towns widen (ROUND20). + const drop = (why, n = 1) => { norm.dropped = (norm.dropped || 0) + n; norm['dropped_' + why] = (norm['dropped_' + why] || 0) + n; }; const skey = p => `${Math.round(p.x / SNAP)},${Math.round(p.z / SNAP)}`; const rawWays = []; for (const rd of roads) { @@ -316,7 +325,7 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) { const perEdge = new Map(); for (const s of fx.shops) { const a = nearest(projX(s.lon), projZ(s.lat)); - if (!a || a.d > SEAT_MAX) { norm.dropped = (norm.dropped || 0) + 1; continue; } // stranded past bridging → drop + COUNT (ruling) + if (!a || a.d > SEAT_MAX) { drop('stranded'); continue; } // stranded past bridging → drop + COUNT (ruling) const key = a.e.id * 2 + (a.side > 0 ? 1 : 0); (perEdge.get(key) || perEdge.set(key, { e: a.e, side: a.side, list: [] }).get(key)).list.push({ s, t: a.t }); } @@ -337,16 +346,16 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) { let along = NODE_CLEAR; for (let k = 0; k < list.length; k++) { const s = list[k].s, f = footOf(SHOP_TYPES[s.type] ? s.type : 'opshop'); - if (along + f > len - NODE_CLEAR) { norm.dropped = (norm.dropped || 0) + (list.length - k); break; } // edge full — overflow drops COUNTED (ruling) + if (along + f > len - NODE_CLEAR) { drop('overflow', list.length - k); break; } // edge-side full — overflow COUNTED cands.push({ block, x: e.a.x + ux * (along + f / 2) + nx * (off + dep / 2), z: e.a.z + uz * (along + f / 2) + nz * (off + dep / 2), w: f, d: dep, ry, frontEdge: e.id, s }); - along += f + GAP; + along += f + RGAP; } } cands.sort((p, q) => p.s.id - q.s.id); // real streets crowd at intersections → deterministic overlap-resolve const placed = []; for (const c of cands) { const corners = lotCorners(c); - if (placed.some(p => obbOverlap(p.corners, corners))) { norm.dropped = (norm.dropped || 0) + 1; continue; } + if (placed.some(p => obbOverlap(p.corners, corners))) { drop('overlap'); continue; } placed.push({ corners, c }); } for (const { c } of placed) addShop(addLot(c.block, c.x, c.z, c.w, c.d, c.ry, c.frontEdge, 'shop'), c.s); diff --git a/web/js/citygen/selfcheck.js b/web/js/citygen/selfcheck.js index 6962a3c..6809970 100644 --- a/web/js/citygen/selfcheck.js +++ b/web/js/citygen/selfcheck.js @@ -449,22 +449,24 @@ section('v4 real-roads graph lift (schema v2)'); // 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'); -// ROUND19: all five real towns are now on REAL ROADS (E's schema-v2 roads[] landed) and pinned on the +// 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 = { - katoomba_real: 0xb7ffbca2, newtown_real: 0x6f984c81, fremantle_real: 0xfb197000, - bendigo_real: 0x21179b3b, castlemaine_real: 0x7c47f639, + katoomba_real: 0x420f677d, newtown_real: 0x741c18ec, fremantle_real: 0x73d385df, + bendigo_real: 0xc175194e, castlemaine_real: 0x26c128c9, }; // 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. const REAL_TOWN_GIG_GOLDENS = { - katoomba_real: 0xbd332e83, newtown_real: 0xd36f09ca, fremantle_real: 0xca42f569, - bendigo_real: 0x93ae8765, castlemaine_real: 0x9a4e904c, + katoomba_real: 0x2115732a, newtown_real: 0x17ab31bb, fremantle_real: 0x82c27397, + bendigo_real: 0xf5ce58c4, castlemaine_real: 0x48272da9, }; -const townFiles = (existsSync(TOWNS_DIR) ? readdirSync(TOWNS_DIR) : []).filter(f => f.endsWith('.json')); +// `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)"); for (const f of townFiles) { const key = f.replace(/\.json$/, '');