PROCITY/docs/LANES/LANE_A_NOTES.md
m3ultra 52eb1090ef Lane A round 6: second plan source ?plansrc=osm (real-data OSM import) — full seam
The carried-over OSM plan source (no-showed R4–R5) lands complete, invariants green — a
second CityPlan producer behind the same contract, so B–F consume it unchanged.

- osm_fixture.js: 95 REAL inner-Melbourne secondhand/charity/music/book/toy/antique shops,
  extracted deterministically from thriftgod city_source.json (Overpass cache), imported as a
  module ⇒ ZERO network.
- plan_osm.js: generatePlanOSM(seed) — project lat/lon→metres, bin shops into latitude-band
  avenues off a spine (real row + real E–W order preserved), march into a valid non-overlapping
  CityPlan, centre on origin, report true 362×486 bbox, real shop names, one openLate video.
- index.js: generatePlanFor(seed, source) selector — default 'synthetic' BYTE-IDENTICAL
  (golden 0x3fa36874 untouched); 'osm' → the importer. generatePlanOSM exported for F.
- selfcheck.js: parameterized by source — OSM runs the full structural invariant suite
  (frontEdge/facing/no-overlap/chunk-coverage/finite/JSON/determinism/openLate) + its own
  golden 0x34cfdec0; synthetic-only brief checks skipped. ALL GREEN 1362/1362.
- map.html: ?plansrc=osm renders it (reference wiring for F). Screenshot in docs/shots/laneA/.
- CITY_SPEC: plan-source note; LANE_A_NOTES R6: seam + goldens + shape-diffs for F's gate (F2).

Honest cut: street geometry regularized (real row+order drive it, exact metres do not);
name-parody deferred. qa.sh --strict GREEN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 22:47:38 +10:00

96 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 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 BF 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 **415 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.