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:
parent
4b0144989b
commit
110c82833b
@ -1,11 +1,35 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*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.*
|
||||
*Status: **all deliverables landed; self-check all-green (66419/66419); rounds 2–21 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 3–6× 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 0–1), 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.
|
||||
|
||||
@ -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 0–1), 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)
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user