The carried brief (slipped R7) lands complete — all four items, qa.sh --strict GREEN.
1. Selfcheck PARITY: extracted structuralSuite(plan,label) and run the FULL invariant suite
on every (source, town) — synthetic + both OSM towns get identical coverage (storeys,
one-shop-per-lot, facing, within/cross-block overlap, corridor coverage, determinism,
exactly-one-openLate). ALL GREEN 1727/1727 (was 1362; OSM went subset → full ×2 towns).
2. SECOND TOWN: Katoomba (Blue Mountains, 19 shops, op-shop/book heavy) — regional contrast
to inner-Melbourne. osm_fixture.js → OSM_TOWNS{melbourne,katoomba}+DEFAULT_TOWN;
generatePlanFor(seed, source, {town}); ?plansrc=osm&town=katoomba. Golden 0x0f652510.
Melbourne 0x34cfdec0 + synthetic 0x3fa36874 UNCHANGED (town key kept off the plan).
3. "Add a town" recipe in LANE_A_NOTES (R8): extract → OSM_TOWNS → pin golden → tell F.
4. Normalization + LOG: importer bends real data to the contracts (unknown kind→opshop,
hours-clamp so only one shop is openLate, video-preferred landmark w/ fallback) and logs
it via {report}; selfcheck prints per-town (e.g. melbourne hoursClamped:5, openLate:video).
For F: three goldens to pin, gate keyed (seed, plansrc, town); selector gained {town} — see
LANE_A_NOTES R8. Screenshots docs/shots/laneA/osm-{melbourne,katoomba}.png.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
143 lines
9.5 KiB
Markdown
143 lines
9.5 KiB
Markdown
# LANE A — NOTES (cross-lane answers from CityGen)
|
||
|
||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||
|
||
## Round 8 (2026-07-15)
|
||
|
||
### → Lane F: OSM is at parity + there's a SECOND town. Three goldens to pin, one new selector arg.
|
||
|
||
- **Selfcheck parity done**: the full invariant suite (`structuralSuite`) now runs on **every
|
||
(source, town)** — synthetic and both OSM towns get identical coverage (storeys, one-shop-per-lot,
|
||
facing, within/cross-block overlap, corridor coverage, determinism, exactly-one-openLate, …).
|
||
`node selfcheck.js` → **ALL GREEN 1727/1727**.
|
||
- **Selector gained a town arg** (`web/js/citygen/index.js`):
|
||
`generatePlanFor(seed, source, { town })`. Shell wiring: read `&town`, pass it through —
|
||
`generatePlanFor(seed, src, { town: q.get('town') || undefined })`. `map.html` is the reference
|
||
impl (`?plansrc=osm&town=katoomba`). Unknown/absent town → the default (`melbourne`).
|
||
`osmTownKeys()` lists the available towns for a UI picker.
|
||
- **THREE goldens to pin** (seed 20261990), all asserted by selfcheck already:
|
||
| source / town | golden |
|
||
|--------------------|--------------|
|
||
| synthetic | `0x3fa36874` |
|
||
| osm / melbourne | `0x34cfdec0` |
|
||
| **osm / katoomba** | `0x0f652510` |
|
||
The determinism gate should key on **(seed, plansrc, town)**. Synthetic + melbourne goldens are
|
||
**unchanged** from round 6.
|
||
- Katoomba (Blue Mountains, 19 shops — op-shop/book heavy) is a deliberate small-regional contrast
|
||
to inner-Melbourne (95, book heavy). Both boot end-to-end; screenshots in `docs/shots/laneA/`.
|
||
|
||
### Normalization: the importer bends the DATA to the contract, never the contract (round-8 item 4)
|
||
|
||
Real OSM data that fights a CityPlan contract is normalized deterministically, and the change is
|
||
**logged** (selfcheck prints a per-town line; `generatePlanOSM(seed, town, { report })` fills `report`):
|
||
- unknown OSM `shop=` kind → `opshop` (`typesRemapped`);
|
||
- a shop closing ≥22:00 → clamped to 21:00 so only the one openLate landmark is late (`hoursClamped`);
|
||
- openLate landmark prefers a **video**; a town with no video falls back to another non-stall type
|
||
and `report.openLate` records it (both Melbourne + Katoomba have video, so both read `video`).
|
||
|
||
### RECIPE — add another OSM town (mechanical, ~10 min)
|
||
|
||
1. **Pick a cluster** in thriftgod's `city_source.json` (Overpass cache). Find a centroid + a cell
|
||
size that captures one coherent town (aim 15–150 on-theme shops). The extraction one-liner used
|
||
for melbourne/katoomba is in `A-progress.md` (round 6/8) — reuse it with your `(cLat,cLon,cell)`.
|
||
2. **Append the town** to `OSM_TOWNS` in `web/js/citygen/osm_fixture.js`:
|
||
`key: { town, source, center:{lat,lon}, shops:[{id,name,type,lat,lon,suburb}, …] }` — sort shops
|
||
by `id` (determinism). Map OSM `shop=` kinds → registry types (charity→opshop, books→book,
|
||
music→record, toys→toy, video_games→video, antiques/second_hand→opshop/pawn).
|
||
3. **Pin its golden**: run `node web/js/citygen/selfcheck.js`, read the printed
|
||
`osm/<key>: fingerprint 0x…`, add `<key>: 0x…` to `OSM_GOLDENS` in `selfcheck.js`. Re-run → green.
|
||
4. **Tell F** the new (seed, plansrc, town) hash for the determinism gate. Done — zero network, the
|
||
full parity suite covers it automatically.
|
||
|
||
## Round 6 (2026-07-14)
|
||
|
||
### → Lane F (F2): the `?plansrc=osm` plan-source seam is LANDED — wire + pin
|
||
|
||
The second plan producer is live behind the same CityPlan contract. Full seam, invariants green
|
||
(not a partial). What F needs to wire the shell bootstrap + extend the determinism gate:
|
||
|
||
- **Selector API** (`web/js/citygen/index.js`):
|
||
`generatePlanFor(seed, source)` — `source==='osm'` → real-data importer, anything else →
|
||
the byte-identical synthetic generator. Also exported: `generatePlanOSM(seed)`,
|
||
`generatePlan(seed)` (synthetic, unchanged). Unknown source falls back to synthetic.
|
||
- **Shell wiring** (in `web/index.html`, F-owned — one line): read `?plansrc`, then
|
||
`const src = new URLSearchParams(location.search).get('plansrc')==='osm' ? 'osm' : 'synthetic';`
|
||
and call `citygen.generatePlanFor(seed, src)` where it currently calls `generatePlan(seed)`.
|
||
`map.html` already does exactly this (reference impl).
|
||
- **Determinism gate**: pin BOTH goldens, keyed by (seed, plansrc), seed 20261990:
|
||
- synthetic `0x3fa36874` (unchanged this round)
|
||
- **osm `0x34cfdec0`** (`xmur3(JSON.stringify(generatePlanOSM(20261990)))`)
|
||
`selfcheck.js` already asserts both; the smoke can just hash-compare.
|
||
- **Zero network**: the fixture is `web/js/citygen/osm_fixture.js` (95 real inner-Melbourne
|
||
shops from thriftgod's Overpass cache), imported as a JS module — no fetch/XHR at runtime.
|
||
|
||
**⚠ OSM plans differ from synthetic in shape (by design — real towns aren't the synthetic template):**
|
||
- `plan.source === 'osm'` is set; `plan.size` is the real bounding box (~362×486), **not** 1024².
|
||
- **No synthetic districts/types**: an OSM town has NO market square, arcade, dept anchor, milk
|
||
bars, or stalls — only the real retail types (record/opshop/toy/book/video/pawn). Any code that
|
||
ASSUMES a dept/market/milkbar exists must guard on `plan.source !== 'osm'` (or just not assume).
|
||
- Everything B–F actually consume is identical: nodes/edges/blocks/lots/shops schema, `ry`/
|
||
`frontEdge` semantics, `chunkIndex`, hours + the exactly-one-openLate-video rule all hold.
|
||
- Shop `name`s are the REAL OSM names (e.g. "Central Catholic Bookshop"); `sign` is derived.
|
||
Parody-transform of names is a documented deferred nicety, not a blocker.
|
||
|
||
## Round 4 (2026-07-14)
|
||
|
||
### → Lane F (A1): storeys checker (F2) confirmed — 0 warnings, no false negatives
|
||
|
||
`tools/qa/consistency_check.mjs` `permittedMax` scoping (your F2) is **correct**. Verified this round:
|
||
- `node tools/qa/consistency_check.mjs` across 7 seeds → **0 storeys warnings**; `qa.sh --strict` GREEN (4/4).
|
||
- **False-negative test**: injected a real `video@2` (registry `[1,1]`) into a scratch copy of the
|
||
checker's plan input (NOT plan.js) across 5 seeds → the storeys gate still fired every time. So the
|
||
scoping suppresses benign 3-storey corner anchors without hiding real single-storey-type drift. ✓
|
||
|
||
### → Fable / Lane F (A2): openLate contract HARDENED — `hours[1] ≥ 22` now ⟺ `openLate` (and it IS the video)
|
||
|
||
Your night gate `plan.shops.some(s => s.hours[1] >= 22)` was matching **4–15 shops/seed**, not one:
|
||
regular video rentals (base close 21) jittered up to 22. That contradicted the shell's own contract
|
||
(`web/index.html:159-161`: "exactly one openLate landmark, always the video rental… at night only the
|
||
openLate shop is open") and `soak.py:68`. Rather than ask you to change the gate, I made reality match it:
|
||
|
||
- **plan.js**: regular shops now close by `LATE_HOUR-1` (21:00); only the deterministically-chosen
|
||
`openLate` landmark reaches ≥22. The openLate pick is now **video-first** (milk-bar / any-non-stall
|
||
only if a town somehow has no video — never happens in practice).
|
||
- **Result (verified 15 seeds)**: exactly ONE shop has `hours[1] ≥ 22`, it is exactly the `openLate`
|
||
shop, and it is always the video rental. `{s: s.hours[1] >= 22}` and `{s: s.openLate}` are now the
|
||
same singleton — **gate on either**, they agree.
|
||
- selfcheck.js asserts all of this now (exactly-one, field⟺threshold, is-video).
|
||
|
||
**⚠ GOLDEN.hash changed: `0x098eec2b` → `0x3fa36874`.** This is the *intended* consequence of the A2
|
||
plan.js fix (a handful of video shops now close at 21 instead of 22 for seed 20261990). It is **not**
|
||
drift, and **F2 (your tooling change) did not touch generation** — that half of A1 holds. selfcheck.js
|
||
carries the new golden and is GREEN. Nothing else in a plan changed (same lots/shops/types/names/skins).
|
||
|
||
## Round 3 (2026-07-14)
|
||
|
||
### → Lane F: the `qa.sh` "storeys outside registry range" warnings are BENIGN (finding #5 closed)
|
||
|
||
Your consistency gate prints ~5 warnings per run:
|
||
`"N shop(s) have storeys outside their registry type range … else a plan↔registry drift — ask Lane A"`.
|
||
|
||
**Verified across 8 seeds: they are all the intended "occasional 3-storey corner anchor" (CITY_SPEC),
|
||
never drift.** Evidence:
|
||
- Single-storey types (`video`/`milkbar`/`stall`, registry `[1,1]`) are observed at 1 storey ONLY —
|
||
e.g. 412 video shops, none at 2. So the specific "video at 2" example does **not** occur.
|
||
- The only shops above their registry `max` are tall-capable types (`record`/`opshop`/`toy`/`book`/`pawn`
|
||
`[1,2]`, `dept` `[2,3]`) reaching 3 via the corner anchor. Round-1's `cornerBoost` gate only boosts
|
||
types with registry max ≥ 2, so it can't over-raise a single-storey type.
|
||
|
||
**To silence the warning correctly**, scope your check to the *permitted* max, not the strict registry max:
|
||
```
|
||
permittedMax(type) = registryMax >= 2 ? Math.min(registryMax + 1, 3) : registryMax
|
||
// flag only shops with storeys < registryMin || storeys > permittedMax → that IS real drift
|
||
```
|
||
This rule is now documented in `docs/CITY_SPEC.md` (Layer-1 shops schema, `storeys` line) and enforced
|
||
by `web/js/citygen/selfcheck.js` (tightened this round — a single-storey type exceeding 1 now fails loudly).
|
||
|
||
### → Lane F: `hours` / `openLate` contract is ready for your §3.5 wiring (no changes)
|
||
|
||
`hours = [open, close]`, 24h integers, `0 ≤ open < close ≤ 23`. **Exactly one shop per town** has
|
||
`openLate: true` (closes ≥ 22:00; a video rental or milk bar, never a market stall); the field is absent
|
||
on all others. Gate on the field — `plan.shops.find(s => s.openLate)` — not a magic hours threshold.
|
||
Your ROUND3_INSTRUCTIONS §Lane F states it correctly, so consume as-is.
|