Lane A round 21: venue cluster-adjacency bias — D's relocation finding (ledger #1)

D found katoomba's pub 1,000m from the retail cluster — a gig with no passing crowd. Measuring all
five towns first showed it was SYSTEMIC: 11 of 15 venues sat with <3 shops within 160m (newtown's were
0/0/1 — every venue stranded). After the bias: 0 of 15. Katoomba's pub 1 → 9. Castlemaine cleared without
needing the floor exception the brief anticipated.

- pickVenues builds a CLUSTER-ADJACENT field first (>= CLUSTER_MIN 3 shops within CLUSTER_R 160m), then the
  kind flavors (pub corner / band_room fringe / rsl off-spine) bias WITHIN that field rather than overriding
  it (per the brief: biases, not absolutes) — a fringe band_room in the cluster beats one a kilometre from
  anyone. A town with no cluster at all falls back to the pool.
- SYNTHETIC FROZEN BY CONSTRUCTION, not by luck. The brief assumed a blanket filter would be a no-op because
  'the synthetic town is one dense cluster anyway'. Measurement says otherwise: 4 synthetic candidates PER
  SEED sit under the threshold (min cluster 0-1), so a blanket filter WOULD have moved the frozen golden.
  Gated on plan.source === 'osm' (undefined on synthetic) — impossible rather than unlikely. The marched
  fixtures are 'osm' but genuinely dense (min cluster 3/11/13) ⇒ verified no-op.
- Only the GIG goldens moved (venue selection is gig-layer; base town goldens untouched): katoomba
  0xbf586b77, newtown 0xacdb3eee, fremantle 0xcf3426fc, bendigo 0x646b2c23, castlemaine 0xf9713a69.
  classic 0x3fa36874 / gig 0xb1d48ea1 / marched fixtures FROZEN. selfcheck ALL GREEN 66419/66419;
  scaffold + consistency GREEN; cross-process deterministic.
- FYI: E's v4.0 pack (glebe/marrickville/newcastle) already boots clean through my loader (validates + full
  suites pass), listed UNPINNED — I pin the pack in R22; it does not gate the beta tag.

Handshake → C + D: cluster bias landed, goldens re-pinned, start your verifies (before/after table in
LANE_A_NOTES §R21).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-16 20:37:45 +10:00
parent 4b0144989b
commit 110c82833b
4 changed files with 103 additions and 13 deletions

View File

@ -1,11 +1,35 @@
# LANE A — CITYGEN · progress (PROCITY-A)
*Status: **all deliverables landed; self-check all-green (51866/51866); rounds 220 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 36× shops, all place 3 venues, drops 5.5% counted by cause); all 10
real-town goldens (base + gig) pinned. classic/gig/marched-fixtures frozen.*
*Status: **all deliverables landed; self-check all-green (66419/66419); rounds 221 closed** (see below).
R21: venue cluster-adjacency bias — D's relocation finding resolved (venues under 3 shops/160 m: 11 of 15
→ 0); gig goldens re-pinned; synthetic frozen by construction. R20: poster cap (katoomba 438→31) + E's
density widening absorbed (5 towns at 36× shops). classic/gig/marched-fixtures frozen.*
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
## Round 21 (2026-07-16) — v4.0-beta: the venue cluster-adjacency bias (ledger #1, the round's spine)
D filed it: katoomba's pub sat 1,000 m from the retail cluster — a gig with no passing crowd. Measuring all
five towns first showed it was **systemic: 11 of 15 venues** had <3 shops within 160 m (newtown's were
0/0/1). After the bias: **0 of 15**. Katoomba's pub went **1 → 9**; castlemaine cleared without needing the
floor exception Fable anticipated.
- `pickVenues` now builds a **cluster-adjacent field** (≥`CLUSTER_MIN` 3 shops within `CLUSTER_R` 160 m),
then the kind flavors (pub corner / band_room fringe / rsl off-spine) bias **within** it rather than
overriding it (per the brief: biases, not absolutes). No cluster at all → falls back to the pool.
- **Synthetic frozen BY CONSTRUCTION**, not by luck. The brief assumed a blanket filter was a no-op because
"the synthetic town is one dense cluster anyway" — measurement says otherwise: **4 synthetic candidates
per seed** sit under the threshold (min cluster 01), so a blanket filter *would* have moved the frozen
golden. Gated on `plan.source === 'osm'` (undefined on synthetic). Marched fixtures are 'osm' but genuinely
dense (min 3/11/13) ⇒ verified no-op.
- **Only the GIG goldens moved** (venue selection is gig-layer): katoomba `0xbf586b77`, newtown `0xacdb3eee`,
fremantle `0xcf3426fc`, bendigo `0x646b2c23`, castlemaine `0xf9713a69`. Base goldens untouched.
classic/gig/marched fixtures frozen. selfcheck ALL GREEN **66419/66419**; scaffold + consistency GREEN;
cross-process deterministic.
- FYI: E's v4.0 pack (`glebe`, `marrickville`, `newcastle`) already boots clean through my loader (validates
+ full suites pass), listed UNPINNED — **I pin the pack in R22**; it does not gate the beta tag.
Handshake fired → C + D: cluster bias landed, goldens re-pinned, start your verifies.
## Round 20 (2026-07-16) — v4.0-beta THE FIVE TOWNS: the poster cap (density absorb pends E)
Two Lane-A deliverables; the first (independent) landed this commit, the second waits on E's widened caches.

View File

@ -2,6 +2,45 @@
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
## Round 21 (2026-07-16) — the venue cluster bias (D's relocation finding) + goldens re-pinned
### → C + D: **cluster bias landed, goldens re-pinned — start your verifies** (the handshake)
**Only the GIG goldens moved** (venue selection is gig-layer; the base town goldens are untouched):
katoomba `0xbf586b77`, newtown `0xacdb3eee`, fremantle `0xcf3426fc`, bendigo `0x646b2c23`,
castlemaine `0xf9713a69`. Base unchanged (katoomba `0x420f677d`, …). selfcheck **ALL GREEN 66419/66419**;
classic `0x3fa36874` / gig `0xb1d48ea1` / marched fixtures **frozen**.
### → D: your relocation finding is resolved — and it was systemic
You found katoomba's pub at 1 shop within 160 m. Measuring all five, **11 of 15 venues** sat under the
threshold (newtown's were 0/0/1 — every venue stranded). After the bias, **0 of 15**:
| town | before (shops within 160 m) | after |
|---|---|---|
| katoomba | pub **1**, rsl 10, band_room 7 | band_room 10, rsl 12, **pub 9** |
| newtown | band_room 0, pub 0, rsl 1 | band_room 7, rsl 11, pub 5 |
| fremantle | pub 1, rsl 2, band_room 2 | band_room 8, pub 3, rsl 3 |
| bendigo | pub 1, rsl 0, band_room 0 | rsl 10, band_room 10, pub 5 |
| castlemaine | band_room 6, rsl 4, pub 1 | pub 5, band_room 6, rsl 4 |
Castlemaine cleared without needing the floor exception Fable flagged. Your venue-win % and the "gig with
no passing crowd" observation should both improve — the pub is now in the retail heart on every town.
### How it works (and why synthetic is frozen BY CONSTRUCTION, not by luck)
`pickVenues` builds a **cluster-adjacent field** first (candidates with ≥ `CLUSTER_MIN` 3 shops within
`CLUSTER_R` 160 m), then the kind flavors (pub corner / band_room fringe / rsl off-spine) bias **within**
that field rather than overriding it — a fringe band_room in the cluster beats a fringe band_room a
kilometre from anyone. A town with no cluster falls back to the whole pool.
**Gated on `plan.source === 'osm'`** — real-data towns only. Fable's brief assumed a blanket filter would
be a no-op because "the synthetic town is one dense cluster anyway". **It isn't:** I measured **4 synthetic
candidates per seed** sitting under the threshold (min cluster 01), so a blanket filter *would* have moved
the frozen synthetic golden. The `source` gate makes it impossible instead of unlikely. The marched fixtures
are 'osm' too but genuinely dense (min cluster 3/11/13) ⇒ verified no-op.
### FYI: E's v4.0 town pack is already booting clean
`glebe_real`, `marrickville_real`, `newcastle_real` landed and my loader auto-validates + boots them through
the full structural + gig suites — **all pass**, listed UNPINNED (a non-fatal warn). That's per the brief:
**I pin the pack in R22**, and it must not gate the beta tag. Early signal: the pack needs no new hardening.
## Round 20 (2026-07-16) — v4.0-beta: the poster cap (density absorb pending E's widened caches)
### → F: **poster cap landed — re-measure `venue_night`** (your +965-tri edge should close)

View File

@ -44,6 +44,9 @@ const GIGS_PER_VENUE = [4, 6]; // per week — the rest are dark nights, whic
// Kind order as venues are added. A town of 4 gets a second pub at the far end of the spine —
// two pubs and a footy club is the most Australian town there is.
const VENUE_SEQUENCE = ['pub', 'band_room', 'rsl', 'pub'];
// ROUND21 (D's relocation finding): a venue 1 km from the retail cluster is a gig with no passing crowd
// (katoomba's pub had 1 shop within 160 m; the RSL had 13). Venue candidates prefer cluster adjacency.
const CLUSTER_R = 160, CLUSTER_MIN = 3;
// Cover law (ROUND13 §A2): ~half free town-wide; the RSL skews CHEAP (it's a club, the bistro pays the
// bills), the band room skews FREE (the door is a hat on a milk crate). `cover` stays an integer in
@ -92,6 +95,18 @@ function pickVenues(plan, citySeed) {
}
return best;
};
// ROUND21: how many OTHER shops sit within CLUSTER_R of this candidate — its retail-cluster adjacency.
// Real-data plans only: `plan.source === 'osm'` is undefined on the synthetic generator, so the bias
// below can never touch the frozen synthetic goldens BY CONSTRUCTION (not by luck — measured: 4
// synthetic candidates per seed sit under CLUSTER_MIN, so a blanket filter WOULD have moved them).
const realData = plan.source === 'osm';
const shopLots = realData ? plan.shops.map(lotOf).filter(Boolean) : [];
const clusterCount = s => {
const l = lotOf(s); if (!l) return 0;
let n = 0;
for (const o of shopLots) if (o !== l && Math.hypot(o.x - l.x, o.z - l.z) <= CLUSTER_R) n++;
return n;
};
const base = plan.shops.filter(s => {
if (s.openLate) return false; // must not displace the night landmark
@ -108,15 +123,24 @@ function pickVenues(plan, citySeed) {
const pool = base.filter(s => !taken.has(s.id));
if (!pool.length) break; // degenerate town — fewer venues, still valid
// ROUND21 ledger #1: prefer CLUSTER-ADJACENT candidates (the retail heart), then let the kind flavors
// bias WITHIN that set rather than override it — a fringe band_room in the cluster beats a fringe
// band_room a kilometre from anyone. A town with no cluster at all falls back to the whole pool.
let field = pool;
if (realData) {
const clustered = pool.filter(s => clusterCount(s) >= CLUSTER_MIN);
if (clustered.length) field = clustered;
}
let cand;
if (kind === 'band_room') {
cand = pool.filter(s => blockKindOf(s) === 'warehouse');
if (!cand.length) cand = pool.filter(s => !onMain(s)); // osm: no warehouse district
cand = field.filter(s => blockKindOf(s) === 'warehouse');
if (!cand.length) cand = field.filter(s => !onMain(s)); // osm: no warehouse district
} else if (kind === 'rsl') {
cand = pool.filter(s => !onMain(s) && blockKindOf(s) !== 'warehouse');
if (!cand.length) cand = pool.filter(s => !onMain(s));
cand = field.filter(s => !onMain(s) && blockKindOf(s) !== 'warehouse');
if (!cand.length) cand = field.filter(s => !onMain(s));
} else {
cand = pool.filter(onMain);
cand = field.filter(onMain);
// a SECOND pub belongs at the other end of the spine, not next door to the first
const firstPub = out.find(v => v.kind === 'pub');
if (firstPub) {
@ -133,10 +157,11 @@ function pickVenues(plan, citySeed) {
// 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) clear = field.filter(s => !onMain(s) && facadeClear(s) >= POSTER_CLEAR); // stay in the cluster
if (clear.length) cand = clear;
}
if (!cand.length) cand = pool; // last resort: anything left
if (!cand.length) cand = field; // last resort: anything cluster-adjacent
if (!cand.length) cand = pool; // truly degenerate: anything left
const shortlist = farthestFirst(cand).slice(0, Math.min(5, cand.length));
const shop = pick(rng(citySeed, 'venuepick', i), shortlist);
if (!shop) break;

View File

@ -461,9 +461,11 @@ const REAL_TOWN_GOLDENS = {
// 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 = {
katoomba_real: 0x2115732a, newtown_real: 0x17ab31bb, fremantle_real: 0x82c27397,
bendigo_real: 0xf5ce58c4, castlemaine_real: 0x48272da9,
katoomba_real: 0xbf586b77, newtown_real: 0xacdb3eee, fremantle_real: 0xcf3426fc,
bendigo_real: 0x646b2c23, castlemaine_real: 0xf9713a69,
};
// `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');