PROCITY/docs/A-progress.md
m3ultra f952f513d0 Lane A round 8: OSM to parity + a second town (Katoomba) — carried R7 brief, full
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>
2026-07-15 04:15:12 +10:00

22 KiB
Raw Blame History

LANE A — CITYGEN · progress (PROCITY-A)

Status: all deliverables landed; self-check all-green (1727/1727); rounds 28 closed (see below). qa.sh --strict GREEN. Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)

Round 8 (2026-07-15) — OSM to parity + a second town (carried R7 brief, FULLY LANDED)

The carried brief (slipped R7) is done — all four items, not a partial.

  1. Selfcheck parity: extracted the full invariant suite into structuralSuite(plan, label) and run it on every (source, town) — synthetic + both OSM towns now get identical coverage (storeys, one-shop-per-lot, solid-use, facing, within/cross-block overlap, corridor coverage, determinism, exactly-one-openLate). ALL GREEN 1727/1727 (was 1362 — OSM went from a subset to the full suite ×2 towns).
  2. Second town: Katoomba (Blue Mountains, 19 shops, op-shop/book heavy) — a small-regional contrast to inner-Melbourne (95, book heavy). ?plansrc=osm&town=katoomba. osm_fixture.js is now OSM_TOWNS { melbourne, katoomba } + DEFAULT_TOWN; generatePlanFor(seed, source, { town }). Golden pinned 0x0f652510. Melbourne golden 0x34cfdec0 and synthetic 0x3fa36874 unchanged.
  3. "Add a town" recipe documented in LANE_A_NOTES.md (R8) — extraction → OSM_TOWNS entry → pin golden → tell F. Mechanical.
  4. Normalization + logging: importer normalizes real data to the contracts (unknown kind→opshop, hours clamp so only one shop is openLate, video-preferred landmark w/ fallback) and logs what it changed via { report } — selfcheck prints e.g. melbourne: 95 shops · normalized {typesRemapped:0, hoursClamped:5, openLate:video}. The contract never bends; the data does.

Extraction one-liner (Katoomba, reusable — see recipe): cell 0.05 around (-33.714, 150.312), same KIND map + dedupe + sort-by-id as Melbourne (cell 0.06 around -37.803, 144.967). Screenshots: docs/shots/laneA/osm-{melbourne,katoomba}.png. qa.sh --strict GREEN.

Round 6 (2026-07-14) — the second plan source (?plansrc=osm), FULLY LANDED

The carried-over OSM plan-source seam (no-showed rounds 45) is done — not the escape-hatch partial, the whole thing, invariants green. A second CityPlan producer behind the same contract, so Lanes BF consume it unchanged.

  • osm_fixture.js — 95 REAL inner-Melbourne secondhand/charity/music/book/toy/antique shops (book 36, toy 14, opshop 13, record 12, video 11, pawn 9), extracted deterministically from thriftgod's city_source.json Overpass cache. Checked in, imported as a module → zero network.
  • plan_osm.jsgeneratePlanOSM(seed): projects real lat/lon → metres, bins shops into latitude-band avenues off a spine (real row + real eastwest order preserved), marches them into a valid, non-overlapping CityPlan, centres on origin, reports its true 362×486 bbox. Real shop names kept ("Central Catholic Bookshop"). Exactly-one-openLate-video rule matches synthetic.
  • index.jsgeneratePlanFor(seed, source) selector; default 'synthetic' is byte-identical (golden 0x3fa36874 untouched). 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 (market/arcade/dept/milkbar) deliberately skipped.
  • map.html?plansrc=osm renders it; screenshot docs/shots/laneA/osm-melbourne.png.
  • Seam + goldens + shape-differences documented for F's determinism gate in LANE_A_NOTES.md (R6).

Verified: 95/95 shops placed, 0 bad frontEdges / 0 overlaps / 0 backwards / full chunk coverage, deterministic, synthetic path byte-identical. Self-check ALL GREEN 1362/1362; qa.sh --strict GREEN. Honest cut (documented): street geometry is regularized (real row+order drive it, exact projected metres do not) and shop-name parody is deferred.

Round 4 (2026-07-14) — v1-tag round: 2 confirmations, 1 hardening fix

A1 — storeys checker (post-F2): CONFIRMED. tools/qa/consistency_check.mjs permittedMax scoping (Lane F's F2, matching my R3 spec) gives 0 storeys warnings across 7 seeds; qa.sh --strict GREEN (4/4). False-negative test: injected a real video@2 into a scratch copy of the checker's input → caught on every seed. No benign-anchor false positives, no real-drift false negatives.

A2 — openLate contract: AUDITED + HARDENED. F's night gate hours[1] >= 22 was matching 415 shops/seed (regular videos jittered from base-21 to 22), not the single landmark it assumes. Fixed in plan.js so hours[1] >= 22openLate (exactly one, always the video rental): regular shops close by 21:00; only the deterministically-chosen video landmark reaches ≥22 (LATE_HOUR). This also makes the generator finally match what web/index.html:159-161 and soak.py:68 already assumed ("at night only the openLate shop is open"). Added selfcheck assertions; verified across 15 seeds.

⚠ GOLDEN.hash: 0x098eec2b0x3fa36874 — intended output change from the A2 fix (not drift; F2 is tooling-only and didn't touch generation). Same lots/shops/types/names/skins; only a few videos' close hour moved 22→21. Details for Fable in LANE_A_NOTES.md (round 4).

Self-check: ✓ ALL GREEN — 1301/1301 (was 1283; +18 are the new openLate exactly-one / field⟺threshold / is-video assertions). Seed 20261990 → "Boolarra Heads": 681 lots, 493 shops, unchanged.

⚠ Fable — commit-label mixup (no action needed on the code): my round-2 changes are committed and correct, but a shared-working-tree race landed them under Lane C's message — commit 4235060 "Lane C: keeper stand pose…" actually contains only my 5 Lane A files (a concurrent git commit grabbed my staged index between my add and my commit). Lane C's real keeper-pose changes are still uncommitted & intact. Nothing is pushed, so relabel at your leisure when the tree is quiet (git commit --amend). Systemic: lanes should commit atomically with git commit -- <paths> -m … to avoid this. Self-check green 1283/1283; content is safe.

TL;DR

generatePlan(citySeed) → CityPlan is done: a deterministic, JSON-serializable, <1ms pure-data town generator matching CITY_SPEC schema v1, plus chunkIndex, the shared shop-type registry, a seeded 90s-AU name generator, a Canvas-2D map debugger, and a node self-check that asserts the whole acceptance contract. node web/js/citygen/selfcheck.js prints ALL GREEN (1082/1082). Five seeds render as recognisable towns (screenshots in docs/shots/laneA/).

Deliverables (all present)

# file what
1 web/js/citygen/plan.js generatePlan(citySeed), chunkIndex(plan), CHUNK, chunkKey
2 web/js/citygen/names.js + wordlists.js seeded shop+town names, 50+ patterns, short signboard forms
3 web/js/core/registry.js SHOP_TYPES (all 9), district weights, enums, pure helpers
4 web/map.html standalone Canvas-2D plan viewer (pan/zoom/hover, layer toggles, seed box, regen, export JSON, chunk grid)
5 web/js/citygen/selfcheck.js node web/js/citygen/selfcheck.js — full acceptance harness
+ web/package.json {"type":"module"} so node runs the self-check as ESM (see Treaty notes)
+ docs/shots/laneA/seed-*.png 5 seeds screenshotted (20261990, 1, 42, 777, 8675309)

What the generator produces (matches the CITY_SPEC design brief)

  • Main-street spine — 7 stations S→N through the origin, x-jitter ±30m, 28m corridor, kind:'main'. Continuous narrow-frontage (69m) retail both sides.
  • Cross streets at 4 stations; the two central ones are wider "second high streets", the outer two are grittier backstreets. All kind:'side', ±8° jitter.
  • Market square west of the origin — a tidy 5×8 grid of stall lots + the dept anchor fronting the spine.
  • Arcade — a kind:'arcade' (width 5) pedestrian lane cutting east through a mid-spine block, lined both sides with tiny 35m record-heavy shops.
  • Warehouse fringe — sparse big lots beyond one spine end (N or S, seeded), mostly infill with the odd pawn shop.
  • Residential collar — a loose ring road of house lots with 24 corner milk bars embedded.
  • Laneways (kind:'lane') behind the central blocks, with yard lots for back-door flavour.

Sample (seed 20261990 → "Boolarra Heads"): 27 nodes, 22 edges, 35 blocks, 711 lots, 523 shops, 152 chunks.

Self-check coverage (node web/js/citygen/selfcheck.js)

Determinism (two runs byte-identical) · <100ms (actually <1ms) · every edge→real nodes · every block→real district · every lot→real block + valid frontEdge + positive size · every shop→real lot + known type + facade skin exists on disk + sane hours + named · one-shop-per-lot · no overlapping lots within a block (rotated-rect OBB / SAT test) · chunkIndex covers every lot & buckets reference real ids · JSON round-trip lossless · design-brief presence (spine, arcade, market, stalls, dept anchor, 24 milkbars) · all registry + used facades exist in web/assets/gen/. Run over 6 seeds → 1082/1082.

House-law compliance

  • Zero Math.random and zero THREE imports in js/citygen/* and registry.js (grep-clean). All randomness flows through web/js/core/prng.js. (map.html uses Math.random only to pick a random seed to view — the generation it then runs is fully seeded; the map is pure Canvas 2D, no THREE.)
  • JSON-serializable, deterministic, <100ms — verified.
  • Registry is flat data every lane imports; facade pools are real files in web/assets/gen/.

Treaty / cross-lane notes for Fable

  1. web/package.json added. Needed so node web/js/citygen/selfcheck.js runs the ES modules (browsers use <script type=module>; node needs "type":"module"). It's additive infra — python http.server and browsers ignore it, no deps, no build. Not in the ownership table; flagging for your blessing. Other lanes can now node-test their pure modules too.
  2. Schema extensions (non-breaking). Added kind to each block (its district-kind string, alongside the spec's district id) so the map & Lane B can theme a block without a district lookup. block.district is the district id; districts[id].kind is the kind. Everything else matches schema v1 exactly.
  3. ry convention (Lane B please read): a lot's ry is the Y-rotation so a GLB modelled facing Z ends up with its facade's outward normal pointing at its frontEdge street. Derivation + a worked example are in plan.js (marchStrip). frontEdge is a valid edge id for every lot (stalls/dept front the nearest spine edge).
  4. pipeline/gen_names.py intentionally NOT shipped. The Lane A brief offered an optional Ollama word-expansion script, but pipeline/* is Lane E's owned dir per the CITY_SPEC ownership table. Wordlists are hand-authored and checked in; runtime does zero network calls. If you want the Ollama expansion, it should live in Lane E's pipeline or be re-assigned.

How to eyeball it

cd web && python3 -m http.server 8130
# http://localhost:8130/map.html            (default seed)
# http://localhost:8130/map.html?seed=42    (any uint32 seed; ?seed= drives the screenshot harness)
node web/js/citygen/selfcheck.js             # acceptance harness, prints ALL GREEN

Open questions for Fable

  • Retail density: a core seed yields ~500 shops (continuous frontage, as briefed). Happy with that, or dial down for Lane B's draw-call budget? (It's one constant per band; trivial to tune.)
  • Want the residential collar to be a true closed ring vs the current 4-edge loop? (Current reads fine on the map; a fuller street graph is a later enhancement.)

Adversarial review + fixes (round 1)

Ran a 6-dimension multi-agent review of the whole lane (27 agents: determinism · schema/spec · geometry · chunk/JSON/perf · cross-lane contract · code quality), each finding independently verified by a skeptic agent that ran the real generator to confirm or refute. 21 findings raised, 15 confirmed real, 6 refuted. All confirmed defects are now fixed and re-verified; the self-check grew new assertions so none of these classes can regress silently.

# defect (confirmed) sev fix
D1 market stalls + dept anchor faced backwards (ryEast sign inverted → facades pointed west, away from the spine they front) HIGH ryEast = atan2(-1,0); self-check now asserts every lot faces its frontEdge
D2 cross-block lot overlaps at spine×cross-street corners (≈45 building-lot pairs/seed, ≤8.7m) — self-check only tested within a block HIGH reserve corner (rung near-spine inset 14→34m) + a deterministic overlap-resolution pass (demote later lot to infill); self-check now runs global cross-block OBB/SAT over building lots
D3 chunkIndex skipped chunks (32m point-sampling on a 64m grid); the origin chunk under the spine was omitted; road width ignored HIGH AmanatidesWoo grid supercover + kerb rails (centreline ± half-width); self-check now asserts edge coverage vs a dense supercover
D4 market stalls' frontEdge pointed at the wrong spine segment (z-band mismatch, up to 122m off) MED stalls → spineEdges[2], dept → spineEdges[3]
D5 name generator emitted doubled possessives (Maccass) — {First}*s fired both the * and the literal s HIGH dropped the redundant *; simplified fill(); self-check rejects unresolved tokens
D6 map.html drew the main road with var(--roadmain), which canvas can't parse → the spine rendered near-invisible MED pass the bare custom-property name to getcss() (now a proper brown spine)
D7 registry facades/fittings drifted from the CITY_SPEC table LOW amended CITY_SPEC's shop-types table (in this commit, flagged) to match the registry lanes import
+ _sbl Map cache was memoized onto the plan → polluted exported JSON with "_sbl":{} LOW moved the cache to a module-level var, reset per regen
+ cornerBoost could push single-storey types (video/milkbar) above their registry max LOW gated to types with registry max ≥ 2
+ no golden-fingerprint / cross-revision drift guard; lot coords never checked finite added a committed golden hash for seed 20261990 + finiteness assertions
+ undocumented block.kind / block.district-is-id; dead stall guard documented the schema in CITY_SPEC; removed dead code

Refuted (6) — investigated and dismissed with reasons, e.g. "the determinism test is a tautology" (true that it only catches in-process nondeterminism — so I added the golden fingerprint for real drift detection) and "package.json isn't in the ownership table" (correct, but it's required infra — now added to the table).

Post-fix self-check: ✓ ALL GREEN — 1271/1271 (was 1082; +189 checks are the new facing/cross-block-overlap/edge-coverage/finiteness/fingerprint assertions). Seed 20261990 → "Boolarra Heads": 27 nodes, 22 edges, 35 blocks, 681 lots, 493 shops, 174 chunks. All 5 seed screenshots re-captured. Golden fingerprint 0xb5d5cc13.

Round 2 — cross-lane integration responses (2026-07-14)

Picked the lane back up after BF integrated against it. Worked through the notes those lanes left for Lane A (docs/LANES/LANE_F_NOTES.md §8/§3.5/§7, B-progress.md, E-progress.md) and closed every request that lands in my files. Self-check green at 1283/1283; golden fingerprint refreshed 0xb5d5cc130x098eec2b; map.html re-verified in-browser (renders a town, chunk-grid overlay works, zero console errors). No screenshot refresh needed — the changes don't alter the plan's 2D appearance (the map colours by type/use, not by facade or hours).

# request (who asked) what I changed files
R1 "Which field marks the open-late shop, and what's the hours encoding?" (Lane F §8); "exactly one late-night shop per town" (F §3.5) The old sr()<0.06 per-shop dice yielded 0…many late shops and — because its formula was max(close,19)+0..2 — usually none past 22:00, so F's hours[1]>=22 gate was flaky/false. Replaced it with exactly one deterministic openLate:true landmark per town, chosen after de-confliction from a naturally-late type (video/milk bar, never a stall), closing 22:00 or 23:00. Encoding documented: hours=[open,close], 24h ints, 0≤open<close≤23. plan.js, CITY_SPEC.md
R2 Skin-name double-prefix trap — registry stores full filenames, skins.js expected bare keys → facade-facade-…jpg.jpg 404s (Lane F §7); "pick one and say so" on field types (Lane B) Added a Field-type contract to CITY_SPEC: all ids + id-refs are integers; facadeSkin/registry facades entries are full filenames under web/assets/gen/, loaded verbatim (extended to E's ground/awning/sky/wall/interior skins). Lane A is the source of truth; consumer fixtures conform to it. CITY_SPEC.md
R3 chunkIndex furniture-drop — verge props at a perpendicular offset bucketed into a chunk that omitted their edge (Lane B) Rewrote edge bucketing from a 3-rail supercover to a gap-free corridor rasterisation: each edge is split into ≤64m pieces and every cell of each piece's ±width/2 corridor-rect AABB is bucketed. Now provably covers the full road+verge band. Self-check samples 9 offsets across the band (incl. between-rail) to prove it. plan.js, selfcheck.js
R4 stall pool has only 1 facade (Lane E validator wants ≥2) Added facade-corrugated.jpg (market-shed backdrop) to the stall pool — matches Lane E's manifest facade→type map, which already tags corrugated for stall. Every type now has ≥2 facades. registry.js, CITY_SPEC.md

New self-check assertions (regression guards): exactly one openLate shop per seed; the late shop closes ≥22:00 and isn't a stall; chunkIndex covers the full road corridor sampled densely across the verge band (not just the centreline).

Empirical result (5 seeds): every town gets one late-night video store — e.g. seed 20261990 → "Yass VIDEO" open till 23:00, seed 42 → "MOVIES" till 23:00, seed 1 → "RENTALS" till 22:00. Both stall facades now appear in the market square.

Notes for Fable / other lanes

  • Lane B can drop its private furniture-rasterisation workaround if it likes — chunkIndex.edges now covers the whole corridor at the source, so any consumer (incl. Lane D footpath NPCs) can trust it.
  • Orientation: CITY_SPEC (§Units/axes + Layer-1 schema) already specifies GLB front = Z at ry=0, world facing (sin ry, cos ry) toward frontEdge; the self-check enforces it and B renders it correctly. Lane B's note prose says "+Z" informally — flagging only so the Z spec stays the single reference for Lane C/E when modelling facades. No code change; it works.
  • Still-open design questions from round 1 (retail density ~493/core vs Lane B's draw budget; true closed residential ring) remain parked for your call — both are one-constant tunes, not rework.

Round 3 — verification round (2026-07-14, per ROUND3_INSTRUCTIONS "Lane A — on-call + one verification")

Small round as briefed. All three tasks done; bash tools/qa.sh --strict GREEN (4/4 gates, selfcheck 1283/1283, manifest 0 errors).

1. F finding #5 — "shops with storeys outside the registry type range" → verified STALE / CLOSED. Scanned every shop across 8 seeds against the correct permitted range. Result: zero violations. video/milkbar/stall (registry [1,1]) are observed at 1 storey only (e.g. 412 video shops, none at 2 — so F's specific "video at 2" case does not occur). The only shops above their registry max are the tall-capable types (record/opshop/toy/book/pawn [1,2], dept [2,3]) reaching 3 via the intentional corner anchor — exactly the CITY_SPEC "occasional 3-storey corner anchor". My round-1 cornerBoost gate (only boosts registry max ≥ 2) already prevents the reported bug.

  • This is the source of qa.sh's 5 consistency warnings ("N shops have storeys outside their registry range … ask Lane A"). They are benign — all intended corner anchors, not plan↔registry drift. Lane F: you can silence them by scoping the check to permittedMax = max≥2 ? min(max+1,3) : max instead of the strict registry max.
  • Hardened anyway (the finding's real lesson): the selfcheck's storeys assertion was too lax (<= Math.max(mx,3) would have passed a video at 3). Tightened to the exact permitted range, so a single-storey type exceeding 1 (or any type past min(max+1,3)) now fails loudly. Proved non-vacuous (rejects video@2, record@4, dept@1; allows record@3). Documented the storeys rule in the CITY_SPEC schema so the intended-vs-drift question can't recur.

2. Golden fingerprint confirmed. Committed GOLDEN.hash = 0x098eec2b matches the current selfcheck fingerprint on main. No output drift. (selfcheck.js assertion tightening doesn't touch the plan, so the hash is unchanged.)

3. Hours / openLate contract — ready for its first real consumer (Lane F §3.5). No changes needed: hours=[open,close] (24h ints, 0≤open<close≤23), exactly one openLate:true landmark per town (closes ≥22:00, video/milkbar, never a stall). ROUND3_INSTRUCTIONS §Lane F restates it correctly, so F's night-gate wiring can consume it as-is. Full contract in CITY_SPEC schema + the round-2 notes above.