Lane A R40 §40.2: D1-D4 settled — the poster that stood in the road, and no silent carry
D4 CLOSED with a fix: 8/108 gig plans placed a spine poster inside a NON-nearest edge's carriageway (worst 1.78m inside a main, northbridge seed 42, behind a kerbless arcade at 1.45m) — the exact filing, reproduced before touching anything. gigs.js onCarriageway now tests inside-ANY-edge; selfcheck's poster arm asserts minimum clearance over ALL edges. After: 0/108. Poster counts 0 diffs across all 108 plans; synthetic byte-identical. The 7 REAL_TOWN_GIG_GOLDENS re-pinned (D4 explicitly required it; old hashes in comments). D1 A-side CLOSED: chunkIndexByCoverage is canonical (chunkIndex kept as verified alias); the naive swap would multi-build 258/681 lots — measured, documented in CITY_SPEC. D2 RE-FILED to F (isOpen still 0 importers; divergent predicate at index.html:541). D3 RE-FILED to B (SHOP_REGISTRY missing pub/band_room/rsl; 81 venue lots wear op-shop signage; bite drifted to buildings.js:465). Both re-filings carry numbers and a reason. selfcheck ALL GREEN 157,647/157,647; fingerprint 0x5f76e76 and GIG_GOLDEN frozen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
97b98b15af
commit
dab5ecd273
@ -1,6 +1,10 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check ALL GREEN (157,647/157,647); rounds 2–30 + 37 + 39 closed**.
|
||||
*Status: **all deliverables landed; self-check ALL GREEN (157,647/157,647); rounds 2–30 + 37 + 39–40 closed**.
|
||||
R40 (§40.2): the v8 debt ledger settled — D4 CLOSED (all-edges poster bound; 8/108 plans → 0; poster
|
||||
counts frozen on all 108; synthetic byte-identical; 7 town gig goldens re-pinned), D1 A-side CLOSED
|
||||
(`chunkIndexByCoverage` canonical + compat alias), D1(B)/D2/D3 formally re-filed with re-measured
|
||||
numbers and owners in `LANE_A_NOTES §40`.
|
||||
R39 (v9 item 39.1): THE ADDRESS LAYER — 17,835 named ways stop being dropped on the floor. New pure
|
||||
module `address.js`; **1,192/1,219 corpus shops (97.8%) resolve to a real street name**, 0 wrong names
|
||||
on 884 shop-bearing edges against an independent resolver, **all goldens frozen, zero re-pins.**
|
||||
@ -9,6 +13,28 @@ law and stayed green while all 14 posters on the default boot stood on B's bitum
|
||||
arms with a permanent control; proven to fail on the pre-ruling geometry where the old gate cannot.
|
||||
All goldens frozen, zero re-pins.*
|
||||
|
||||
## Round 40 (§40.2) — (2026-08-04) THE DEBT ROUND: D1–D4 closed or re-filed, every verdict with a number
|
||||
|
||||
Per the R40 brief: no third option, no silent carry. Every number below was produced by a command run on
|
||||
today's tree (the §37 filing's numbers were re-derived, and two of them changed — which is the point).
|
||||
Full prose + owner handoffs in `LANE_A_NOTES §40`.
|
||||
|
||||
| item | close/re-file | number | evidence command |
|
||||
|---|---|---|---|
|
||||
| **D4** poster gate inner bound nearest-edge; `arcade` camouflages a main | **CLOSED** — `gigs.js onCarriageway` + selfcheck arm 1 both now ALL-EDGES | before: **8/108** gig plans w/ spine poster inside a non-nearest carriageway, worst **1.78 m** inside a main (northbridge_real 42, nearest edge `arcade` @ 1.45 m, kerb 0); after: **0/108**; frontage all-edges clean **238/238**; poster counts **0 diffs on 108 plans** (old vs new placer pairwise); synthetic **byte-identical 0/4** | scratch `d4_measure.mjs` / `d4_frontage.mjs` / `d4_counts.mjs` (corpus sweep over 4 syn seeds + 3 fixtures + 23 towns × 4 seeds), then `node web/js/citygen/selfcheck.js` |
|
||||
| **D1(A)** `chunkIndex` two shapes, false swap-comment invites a streamer break | **CLOSED (A-side)** — `chunkIndexByCoverage` canonical in `plan.js`/barrel/selfcheck; `chunkIndex` kept as compat alias (map.html imports it); CITY_SPEC §chunk-key + producer API amended | a naive swap would multi-build **258/681 lots** (A coverage: 258 lots in ≥2 chunks; B centre: 0/681); container `Object` vs `Map`, elements ids vs objects — all three axes live today; alias verified `chunkIndex === chunkIndexByCoverage` | `node -e` three-axis diff over seed 20261990 (in-session, printed) |
|
||||
| **D1(B)** `planutil.js:4` false comment + B-side name | **RE-FILED → B** | same 258/681; comment still present at `world/planutil.js:4` (grep) — can wait because both consumers are correct today and the trap only fires on the invited future swap, now contradicted by the shipped canonical name | `grep -n chunkIndex web/js/world/planutil.js web/js/world/chunks.js web/map.html` |
|
||||
| **D2** `isOpen` ×3 impls + ad-hoc 4th; shell inverts A's no-hours law | **RE-FILED → F** | A's canonical still **0 importers**; divergent predicate live at `index.html:541` + `audio.js:516` + **19 call sites / 5 tools files**; `sim.js:363` third impl; `dbg.js:241,279,286` fourth site; still **latent: 0/1,838 corpus shops lack hours** (27 plans @ golden seed) — can wait until the first hours-less lot kind ships, fix is ~5 lines in F's `index.html` | grep census + node corpus sweep (27 gig plans), printed in-session |
|
||||
| **D3** two shop registries; venues wear op-shop signage | **RE-FILED → B** | `SHOP_REGISTRY` still **9 types**, missing `pub`,`band_room`,`rsl` (A's `SHOP_TYPES`: **12**, all with `sign:{bg,fg,accent,style}`); bite line now `buildings.js:465`; **81 venue lots** corpus-wide @ golden seed render op-shop palette — can wait because cosmetic-only (no gate, no plan field), and the fix may move a **screenshot** golden (F re-pins), never a plan golden | node corpus sweep (venue count) + `grep -n SHOP_REGISTRY web/js/world/buildings.js web/js/world/fixture_plan.js` |
|
||||
|
||||
**GOLDENS:** synthetic fingerprint **`0x5f76e76` FROZEN** · `GIG_GOLDEN 0xec7a2d39` FROZEN · all base
|
||||
town goldens + 3 osm fixtures FROZEN · **7 `REAL_TOWN_GIG_GOLDENS` re-pinned under D4's explicit
|
||||
requirement** (adelaide/bendigo/fitzroy/fremantle/geelong/marrickville/northbridge — old hashes kept in
|
||||
the pin table comments; the §37 filing predicted 11-of-26 on the R37 tree, today it is 7-of-24).
|
||||
Selfcheck **ALL GREEN 157,647/157,647** after all edits. Files touched: `web/js/citygen/gigs.js`,
|
||||
`web/js/citygen/plan.js`, `web/js/citygen/index.js`, `web/js/citygen/selfcheck.js`,
|
||||
`docs/CITY_SPEC.md`, `docs/LANES/LANE_A_NOTES.md`, this file. Nothing outside Lane A's set.
|
||||
|
||||
## Round 39 (2026-08-03) — v9 item 39.1: THE ADDRESS LAYER (the approved thing; B and C consume it)
|
||||
|
||||
**The full contract, per-town table and reasoning live in `LANE_A_NOTES.md §39`** — published in the
|
||||
|
||||
@ -133,9 +133,15 @@ Schema v1 (Lane A owns it; extend, don't break):
|
||||
> **Full frozen v2 producer contract below** (§"CityPlan v2 — frozen producer contract").
|
||||
|
||||
Chunk key: `cx = floor(x/64)`, `cz = floor(z/64)`. **Chunk size 64m.** Lane A ships a
|
||||
`chunkIndex(plan)` helper: chunk key → { lots, shops, edges } touching it. Edges are bucketed across
|
||||
the full road **corridor** (centreline ± `width/2`), so anything placed in the verge (street furniture,
|
||||
footpath NPCs) resolves to a chunk whose `edges` list includes its edge.
|
||||
`chunkIndexByCoverage(plan)` helper (R40 canonical name; `chunkIndex` remains as a compat alias):
|
||||
chunk key → { lots, shops, edges } **touching** that cell — the COVERAGE question. A lot lands in every
|
||||
chunk its rotated-footprint AABB covers; buckets are plain objects keyed `"cx,cz"`; elements are **ids**.
|
||||
Edges are bucketed across the full road **corridor** (centreline ± `width/2`), so anything placed in the
|
||||
verge (street furniture, footpath NPCs) resolves to a chunk whose `edges` list includes its edge.
|
||||
**This is NOT the runtime streamer's index**: Lane B's `world/planutil.js chunkIndex(plan)` answers the
|
||||
CENTRE question (each lot in exactly the chunk holding its centre; `Map` container; object elements) and
|
||||
the two are not interchangeable on any of the three axes — measured R40: a swap would multi-build
|
||||
258/681 lots on the default synthetic town.
|
||||
|
||||
## CityPlan v2 — frozen producer contract
|
||||
|
||||
@ -150,7 +156,8 @@ footpath NPCs) resolves to a chunk whose `edges` list includes its edge.
|
||||
- `generatePlan(seed) → CityPlan` — synthetic producer (unchanged, byte-identical).
|
||||
- `generatePlanOSM(seed, town = 'melbourne', { report } = {}) → CityPlan` — osm producer; pass a
|
||||
`report` object to receive the normalization log (below). `osmTownKeys() → string[]`.
|
||||
- `chunkIndex(plan, chunkSize = 64) → { chunkSize, chunks }`; `CHUNK = 64`; `chunkKey(cx, cz)`.
|
||||
- `chunkIndexByCoverage(plan, chunkSize = 64) → { chunkSize, chunks }` (coverage form; `chunkIndex`
|
||||
is a compat alias of it — see the chunk-key note above); `CHUNK = 64`; `chunkKey(cx, cz)`.
|
||||
- Geometry helpers (shared with the harness): `lotCorners(lot)`, `obbOverlap(a, b)`.
|
||||
|
||||
**Determinism & goldens.** Same inputs ⇒ byte-identical plan, forever. The determinism gate keys on
|
||||
@ -378,7 +385,8 @@ bitumen, **`pickVenues` prefers a lot whose facade clears every carriageway by
|
||||
(ROUND16 ledger #6, **widened from band_room/rsl to every venue kind in ROUND20** — exported from the
|
||||
barrel; the selfcheck asserts the same floor). Plus a seeded run along
|
||||
the spine — 2 per main edge (or, when a town has no main spine, every edge), at `t ∈ [0.2,0.8]` of the block, on **either verge**;
|
||||
a candidate that would overhang a **real** crossing carriageway is skipped (ROUND19, E's finding), and the
|
||||
a candidate that would overhang a **real** crossing carriageway is skipped (ROUND19, E's finding; the
|
||||
test is against **all edges**, not the nearest — R40 §40.2 D4, see the amendment under ROUND37 below), and the
|
||||
run is **capped at `POSTER_PER_SHOP × shops`** (ROUND20) — a sparse real town has few shops but a big edge
|
||||
graph, so the raw "2 per edge" run reads busy (katoomba 438 → 31). The cap never binds on the shop-dense
|
||||
synthetic town, so its gig golden is frozen. Different venues' posters share the same pole
|
||||
@ -406,8 +414,16 @@ it as footpath rather than as road. The selfcheck's poster-clearance check used
|
||||
"is the poster outside `roadWidth/2`?" — which A's own placer already guaranteed, because `poleOffset` is
|
||||
*derived from* `roadWidth`. Gate and subject were the same function agreeing with itself, so it stayed
|
||||
green for 24 rounds while all **14** gig posters on the default boot stood on painted bitumen. It is now
|
||||
**three arms**: (1) no poster on a carriageway, measured through `vergeBand(e)[0]` — the published law
|
||||
itself, the same call B's ground consumes, so gate and renderer cannot drift apart; (2) every **spine**
|
||||
**three arms**: (1) no poster on **any** carriageway, measured through `vergeBand(e)[0]` — the published
|
||||
law itself, the same call B's ground consumes, so gate and renderer cannot drift apart. **R40 amendment
|
||||
(§40.2 D4): the clearance is the MINIMUM over ALL edges, not the nearest edge's.** The nearest edge is
|
||||
not always the binding one — a kerbless `arcade` (roadWidth 0) wins the nearest-edge race and always
|
||||
"clears", camouflaging a main whose carriageway the poster stands inside. Measured on the R40 tree:
|
||||
8/108 gig plans carried a spine poster up to **1.78 m inside a non-nearest main** (northbridge_real
|
||||
seed 42, nearest edge an arcade at 1.45 m) that the nearest-edge form could not see by construction.
|
||||
The placer's `onCarriageway` skip is now all-edges too; poster **counts** are unchanged on all 108
|
||||
plans (the cap re-subsamples a smaller pool to the same size), the synthetic town is byte-identical
|
||||
(0/4 plans moved — goldens frozen), and 7 real-town **gig** goldens re-pinned; (2) every **spine**
|
||||
poster stands *inside* some edge's `vergeBand` (the bound the old check never had — "not on the road"
|
||||
also passed for a poster in a paddock); (3) **the control** — count the posters that would stand on
|
||||
bitumen under the pre-ruling ground (`?verge=0`, road quad = full `e.width`) and assert that count is
|
||||
|
||||
@ -2,6 +2,94 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 40 (2026-08-04) — §40.2: THE v8 DEBT LEDGER, SETTLED — D1–D4 each closed or re-filed with a number
|
||||
|
||||
The four entries filed in §37 below, three rounds carried. Every number here was re-measured on the
|
||||
R40 tree (commands in `A-progress.md §Round 40`), not quoted from the filing. Verdicts: **D4 CLOSED**
|
||||
(A-owned, fix landed) · **D1 A-side CLOSED, B-side re-filed** · **D2 re-filed (owner F)** · **D3
|
||||
re-filed (owner B)** — the re-filed three all live in files A may not edit this round; measurements and
|
||||
recommendations are below so the owning lane can land each without re-deriving anything.
|
||||
|
||||
### D4 — CLOSED: the poster gate's inner bound is now ALL-EDGES, and the arcade can no longer camouflage a main
|
||||
|
||||
Re-measured on today's tree before touching anything: **8/108 gig plans** carried a spine poster inside
|
||||
a *non-nearest* edge's carriageway, invisible to the nearest-edge rule **by construction** — worst
|
||||
**1.78 m inside a main on northbridge_real seed 42**, where the nearest edge is an `arcade` at 1.45 m
|
||||
whose `roadWidth` is 0 (kerb 0 ⇒ it always wins the nearest race and always passes). Bendigo, fitzroy
|
||||
×2, geelong ×3 seeds, glebe also affected; depths 0.08–1.51 m; every violating far edge was a `main`.
|
||||
|
||||
**Landed, both halves:**
|
||||
- `gigs.js onCarriageway(x,z)` — spine-candidate skip is now inside-ANY-edge, not inside-nearest-edge.
|
||||
- `selfcheck.js` poster arm 1 — clearance is now the **minimum over all edges** of
|
||||
`d(e) − vergeBand(e)[0]`, frontage floor unchanged (`POSTER_CLEAR − 0.02`), spine floor unchanged (0).
|
||||
Frontage posters pass the all-edges floor **238/238 measured** — no exemption needed, because
|
||||
`pickVenues.facadeClear` was *already* all-edges; the gate now asks the placer's own question.
|
||||
|
||||
**The numbers after the fix:** violations **8/108 → 0/108** · poster **counts unchanged on all 108
|
||||
plans** (old placer vs new, compared pairwise — the R20 cap re-subsamples a smaller candidate pool to
|
||||
the same size) · **synthetic byte-identical, 0/4 plans moved** ⇒ fingerprint `0x5f76e76`, `GIG_GOLDEN
|
||||
0xec7a2d39`, all base town goldens, all 3 osm fixtures **frozen** · **7 real-town GIG goldens re-pinned**
|
||||
(adelaide `0x1a00e20f` · bendigo `0x9f82e059` · fitzroy `0x60b8bba0` · fremantle `0x8d7bcb63` · geelong
|
||||
`0x1f866ae7` · marrickville `0xac4c34d0` · northbridge `0x4ea89275` — old hashes in the table's comments;
|
||||
the §37 filing predicted 11 of 26 on the R37 tree, today's tree re-pins 7 of 24: two towns retired and
|
||||
plan drift since, which is exactly why the brief said measure, don't quote). Selfcheck **ALL GREEN
|
||||
157,647/157,647**. CITY_SPEC amended in the same change (poster-law §ROUND37 block + spine-run note).
|
||||
|
||||
### D1 — A-side CLOSED (the rename supplied) · B-side RE-FILED, owner B, number attached
|
||||
|
||||
**A's side, landed:** `chunkIndexByCoverage` is the canonical export (`plan.js`, barrel, selfcheck call
|
||||
sites all renamed; header states which question it answers). `chunkIndex` **remains exported as a compat
|
||||
alias** — `map.html:89,320` imports it and map.html is not A's file to edit this round. Alias verified
|
||||
live: `chunkIndex === chunkIndexByCoverage` and map.html's import path resolves. CITY_SPEC §chunk-key +
|
||||
producer API updated to the two-question wording.
|
||||
|
||||
**The number that keeps this honest:** the divergence is not stylistic — on the default synthetic town
|
||||
(seed 20261990, 681 lots) a naive swap of B's streamer onto A's form would build **258/681 lots in ≥2
|
||||
chunks** (A coverage-buckets; B centre-buckets, 0/681 multi-chunk). Container (`Object`-in-wrapper vs
|
||||
`Map`) and element type (ids vs objects) divergences re-confirmed live on today's tree too.
|
||||
|
||||
**RE-FILED for B (cannot close from A):** `world/planutil.js:4` still says *"If Lane A later ships an
|
||||
official chunkIndex(plan) we swap the import; the shapes match"* — **the shapes do not match on any of
|
||||
three axes, and the swap would multi-build 258/681 lots.** B should delete the false comment, and may
|
||||
rename its own to `chunkIndexByCentre` if it wants the symmetry (A's alias means no rush). Why it can
|
||||
wait: both consumers are correct *today* — the trap only fires on the invited future swap, and the
|
||||
comment is now contradicted by A's shipped canonical name + CITY_SPEC. Half a line of work.
|
||||
|
||||
### D2 — RE-FILED, owner F, still latent — the number is 0/1838 and the day it stops being 0 is the deadline
|
||||
|
||||
Re-measured today: A's canonical `isOpen` (`plan.js:28`, no-hours ⇒ open) still has **zero importers**
|
||||
(grep over `web/` + `tools/`: only the barrel re-export). The live predicate is still
|
||||
`index.html:541` — `shop && shop.hours && …` ⇒ **falsy for a hours-less shop, the exact inversion of
|
||||
A's law** — published as `window.PROCITY.isOpen` and consumed by `world/audio.js:516` + **19 call sites
|
||||
across 5 tools files** (`flags_check.py` 13, `v2_tour.py` 2, `interior_scale_check.py` 2, `shots.py` 1,
|
||||
`qa/r39_runtime.py` 1). `citizens/sim.js:363 _openAt` is still the third implementation (A's semantics,
|
||||
its own clock), `world/dbg.js:241,279,286` still carry the ad-hoc inline hour predicates.
|
||||
|
||||
**Why it can wait, measured:** **0 of 1,838 corpus shops** (27 gig plans: synthetic + 3 fixtures + 23
|
||||
towns at the golden seed) lack `hours` — the divergence cannot fire on any shippable plan today, and
|
||||
the selfcheck asserts sane hours per shop, so a hours-less shop cannot arrive silently. **Why it must
|
||||
not ride another epoch:** v9+ keeps adding lot kinds (carts, stalls, civic) and the first one that
|
||||
ships hours-less flips every consumer of the shell's predicate to "permanently closed". **The fix is
|
||||
still ~5 lines, all in F's files:** `index.html` imports A's `isOpen` from the barrel and passes
|
||||
`currentHour()`; sim keeps its clock, same predicate. A's export needs no change.
|
||||
|
||||
### D3 — RE-FILED, owner B — the bite re-measured: 81 venue lots wear op-shop signage today
|
||||
|
||||
Re-measured today: `world/fixture_plan.js SHOP_REGISTRY` still has **9 types** — `pub`, `band_room`,
|
||||
`rsl` still missing (A's `core/registry.js SHOP_TYPES`: 12, each with full `sign:{bg,fg,accent,style}`).
|
||||
The bite is now `buildings.js:465` (was :344 — line drifted, defect identical):
|
||||
`SHOP_REGISTRY[shop.type] || SHOP_REGISTRY.opshop` ⇒ every venue falls to op-shop signage
|
||||
(`#2c2820`/`#f0e2c0`) instead of the registry's gilt/stencil/club palettes. **Corpus-wide at the golden
|
||||
seed that is 81 venue lots across 27 towns**, ~3/town, on the town's most photographed buildings — the
|
||||
gig venues. The stale header (`fixture_plan.js:26-27`, "registry.js hasn't landed yet") is now **25
|
||||
rounds** old; registry.js landed R13.
|
||||
|
||||
**Why it can wait one more round (and no more):** cosmetic-only — no gate red, no plan field wrong, no
|
||||
golden guarding sign colours. **Why B should do it in R41:** the fix is two imports and a header
|
||||
(`buildings.js` reads `SHOP_TYPES[type].sign.bg/.fg`; `SHOP_REGISTRY` shrinks to the honest
|
||||
fallback-only table), and F should expect **a screenshot golden may move — never a plan golden**. A
|
||||
supplies nothing further; the registry has carried the three venue sign palettes since R13.
|
||||
|
||||
## Round 39 (2026-08-03) — v9 item 39.1: THE ADDRESS LAYER — the contract, published
|
||||
|
||||
**`web/js/citygen/address.js` is landed, green, and yours to consume.** Pure module: **zero imports**,
|
||||
|
||||
@ -297,20 +297,21 @@ function buildPosters(plan, citySeed, venues, gigs) {
|
||||
|
||||
// ROUND19 (E's finding): on a REAL street graph a spine edge is crossed by side streets, so a pole
|
||||
// placed on the spine verge can overhang a crossing carriageway. Skip any candidate that lands on a
|
||||
// carriageway — measured EXACTLY as the selfcheck's floor (nearest edge, `roadWidth/2`). This is a
|
||||
// no-op on the synthetic town (its spine posters already clear every carriageway, so nothing skips and
|
||||
// the gig golden is frozen); it only drops the overhanging ones on real graphs. `pr()` is consumed
|
||||
// before the check, so the rng stream — hence the synthetic output — is unchanged.
|
||||
// carriageway. ROUND40 (§40.2 D4): the test is ALL EDGES, not nearest-edge — the nearest edge is not
|
||||
// always the binding one. A kerbless `arcade` (roadWidth 0) can sit nearer than a main whose kerb the
|
||||
// candidate is INSIDE, and it always "clears": measured 8/108 gig plans with a pole up to 1.78 m inside
|
||||
// a non-nearest main (northbridge_real 42) that the nearest-edge rule structurally could not see. A
|
||||
// no-op on the synthetic town (0/4 plans, before AND after — measured); `pr()` is consumed before the
|
||||
// check, so the rng stream — hence the synthetic output — is unchanged.
|
||||
const onCarriageway = (x, z) => {
|
||||
let best = Infinity, bestE = null;
|
||||
for (const e2 of plan.streets.edges) {
|
||||
const a2 = nodeById.get(e2.a), b2 = nodeById.get(e2.b); if (!a2 || !b2) continue;
|
||||
const ex = b2.x - a2.x, ez = b2.z - a2.z, L2 = ex * ex + ez * ez || 1;
|
||||
let tt = ((x - a2.x) * ex + (z - a2.z) * ez) / L2; tt = Math.max(0, Math.min(1, tt));
|
||||
const d = Math.hypot(x - (a2.x + ex * tt), z - (a2.z + ez * tt));
|
||||
if (d < best) { best = d; bestE = e2; }
|
||||
if (d < roadWidth(e2) / 2 - 1e-6) return true;
|
||||
}
|
||||
return bestE && best < roadWidth(bestE) / 2 - 1e-6;
|
||||
return false;
|
||||
};
|
||||
const pr = rng(citySeed, 'posters', 0);
|
||||
const PER_EDGE = 2;
|
||||
|
||||
@ -7,7 +7,11 @@
|
||||
// (Originated as a Lane F integration shim because the shell imports ./js/citygen/index.js while the
|
||||
// generator ships as plan.js; adopted here as the real Lane A entry point, per the shim's own note.)
|
||||
|
||||
export { generatePlan, chunkIndex, chunkKey, CHUNK, lotCorners, obbOverlap, isOpen } from './plan.js';
|
||||
// R40 (§40.2 D1): `chunkIndexByCoverage` is the canonical name — AABB-coverage bucketing, plain-object
|
||||
// container, id elements; answers "which cells does this thing touch?". It is NOT shape-compatible with
|
||||
// Lane B's runtime `world/planutil.js chunkIndex(plan)` (centre-cell / Map / objects — the streamer's
|
||||
// question). `chunkIndex` remains as a compat alias of the coverage form for existing importers.
|
||||
export { generatePlan, chunkIndexByCoverage, chunkIndex, chunkKey, CHUNK, lotCorners, obbOverlap, isOpen } from './plan.js';
|
||||
export { shopName, townName, bandName } from './names.js';
|
||||
export { generatePlanOSM, osmTownKeys, registerTownCache, getTownCache, validateTownCache, MIN_TOWN_SHOPS,
|
||||
medianShopSpacing, MAX_MEDIAN_SPACING_M } from './plan_osm.js';
|
||||
|
||||
@ -408,10 +408,19 @@ export function generatePlan(citySeed) {
|
||||
}
|
||||
|
||||
// ── chunk index: chunk key "cx,cz" → { lots, shops, edges } touching that 64m cell ──────
|
||||
// ROUND40 (§40.2 D1): this is the COVERAGE form — canonical name `chunkIndexByCoverage`. It answers
|
||||
// "which cells does this thing TOUCH?": a lot lands in EVERY chunk its rotated-footprint AABB covers,
|
||||
// buckets are plain objects keyed "cx,cz", elements are IDS. That is the right shape for coverage
|
||||
// questions (the selfcheck's gap-free corridor assert, map.html's cell census) and the WRONG shape for
|
||||
// a streamer, which wants each thing built exactly once. Lane B's `world/planutil.js chunkIndex(plan)`
|
||||
// is the CENTRE form the runtime streamer uses — centre-cell bucketing, Map container, object elements
|
||||
// — and the two are NOT interchangeable on any of those three axes: a one-line import swap would build
|
||||
// AABB-spanning lots up to four times. `chunkIndex` stays exported as a compat alias for existing
|
||||
// importers (map.html); new code should import the name that states its question.
|
||||
export const CHUNK = 64;
|
||||
export const chunkKey = (cx, cz) => `${cx},${cz}`;
|
||||
|
||||
export function chunkIndex(plan, chunkSize = CHUNK) {
|
||||
export function chunkIndexByCoverage(plan, chunkSize = CHUNK) {
|
||||
const cell = v => Math.floor(v / chunkSize);
|
||||
const chunks = {};
|
||||
const bucket = k => (chunks[k] ||= { lots: [], shops: [], edges: [] });
|
||||
@ -464,3 +473,5 @@ export function chunkIndex(plan, chunkSize = CHUNK) {
|
||||
}
|
||||
return { chunkSize, chunks };
|
||||
}
|
||||
// Compat alias (pre-R40 name) — same function, question-free name. Prefer chunkIndexByCoverage.
|
||||
export const chunkIndex = chunkIndexByCoverage;
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
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 { generatePlan, chunkIndexByCoverage, 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';
|
||||
@ -128,13 +128,13 @@ function structuralSuite(plan, label) {
|
||||
}
|
||||
ok(!crossBlock, `${label}: no overlapping building lots across blocks` + (crossBlock ? ` (lots ${crossBlock})` : ''));
|
||||
|
||||
const idx = chunkIndex(plan);
|
||||
const idx = chunkIndexByCoverage(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`);
|
||||
ok(plan.lots.every(l => covered.has(l.id)), `${label}: chunkIndexByCoverage covers every lot`);
|
||||
|
||||
const cell = v => Math.floor(v / CHUNK);
|
||||
let edgeGap = null;
|
||||
@ -153,7 +153,7 @@ function structuralSuite(plan, label) {
|
||||
}
|
||||
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(!edgeGap, `${label}: chunkIndexByCoverage 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`);
|
||||
|
||||
@ -261,25 +261,31 @@ function districtInvariants(plan, label) {
|
||||
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;
|
||||
});
|
||||
// ROUND40 (§40.2 D4): ARM 1 clearance is the MINIMUM over ALL edges, not the nearest edge's. The
|
||||
// nearest edge is not always the binding one — a kerbless `arcade` (vergeBand inner 0) wins the
|
||||
// nearest-edge race and always passes, camouflaging a main whose carriageway the poster stands INSIDE
|
||||
// (measured: 8/108 gig plans, worst 1.78 m inside a main on northbridge_real 42, invisible to the
|
||||
// nearest-edge form by construction). Frontage posters pass the all-edges floor too, measured, 238/238
|
||||
// — pickVenues' facadeClear was ALREADY all-edges, so the gate now asks the placer's own question.
|
||||
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;
|
||||
let best = Infinity, bestEdge = null, minClear = Infinity, bindEdge = 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 (d - inner < minClear) { minClear = d - inner; bindEdge = e; } // ARM 1 — binding kerb over ALL edges
|
||||
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 (minClear < floor && onRoad === null) onRoad = [p.id, bindEdge.id, +minClear.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)` +
|
||||
ok(onRoad === null, `${label}: no poster stands on ANY carriageway — binding vergeBand inner edge over all edges (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)` : ''));
|
||||
@ -764,28 +770,32 @@ const REAL_TOWN_GOLDENS = {
|
||||
// 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.
|
||||
// ROUND40 (§40.2 D4): 7 towns re-pinned for the all-edges onCarriageway rule (adelaide, bendigo,
|
||||
// fitzroy, fremantle, geelong, marrickville, northbridge). Dropping the candidates that stood inside a
|
||||
// non-nearest carriageway re-shuffles the postercap subsample; poster COUNTS are unchanged on all 108
|
||||
// plans (measured old-placer vs new, 0 count diffs), synthetic byte-identical 0/4, base goldens frozen.
|
||||
const REAL_TOWN_GIG_GOLDENS = {
|
||||
adelaide_real: 0xda729bae,
|
||||
adelaide_real: 0x1a00e20f, // R40 D4 re-pin (was 0xda729bae)
|
||||
ballarat_real: 0xf097c331,
|
||||
bendigo_real: 0xe3e8d892,
|
||||
bendigo_real: 0x9f82e059, // R40 D4 re-pin (was 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,
|
||||
fitzroy_real: 0x60b8bba0, // R40 D4 re-pin (was 0xcd31b1ab)
|
||||
fremantle_real: 0x8d7bcb63, // R40 D4 re-pin (was 0xc6449dc)
|
||||
geelong_real: 0x1f866ae7, // R40 D4 re-pin (was 0xcd0ceac4)
|
||||
glebe_real: 0x8d2b12ca,
|
||||
hobart_real: 0xda3211c9,
|
||||
katoomba_real: 0x5eece2d4,
|
||||
launceston_real: 0xb619b65b,
|
||||
marrickville_real: 0x5dc00b5f,
|
||||
marrickville_real: 0xac4c34d0, // R40 D4 re-pin (was 0x5dc00b5f)
|
||||
newcastle_real: 0xfc3cb144,
|
||||
newtown_godverse: 0x4153dee2,
|
||||
newtown_real: 0xb6a75bb1,
|
||||
northbridge_real: 0x49fe0f2b,
|
||||
northbridge_real: 0x4ea89275, // R40 D4 re-pin (was 0x49fe0f2b)
|
||||
// toowoomba_real RETIRED by E in R23 — see the note on the base map above.
|
||||
redhill_godverse: 0xb53f695d,
|
||||
redhill_real: 0x21db6d1,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user