Per ROUND3_INSTRUCTIONS 'Lane A — on-call + one verification': - F finding #5 (shops with storeys outside registry range) verified STALE across 8 seeds: zero violations. Single-storey types (video/milkbar/stall) never exceed 1; the only shops above registry max are the intentional 3-storey corner anchors (tall-capable types), exactly per CITY_SPEC. Round-1 cornerBoost gate already prevents the reported bug. This is also the source of qa.sh's 5 benign storeys warnings (flagged for Lane F to scope to permittedMax). - Hardened the selfcheck storeys assertion from the too-lax <=max(mx,3) to the exact permitted range (max>=2 ? min(max+1,3) : max), so a single-storey type exceeding 1 now fails loudly. Proven non-vacuous. selfcheck.js only — plan output/fingerprint unchanged (golden 0x098eec2b still matches, confirmed per task 2). - Documented the storeys corner-anchor rule in the CITY_SPEC schema. - hours/openLate contract unchanged; ready for Lane F's §3.5 consumer. qa.sh --strict GREEN (4/4). Committed atomically by pathspec (shared-tree race). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 KiB
LANE A — CITYGEN · progress (PROCITY-A)
Status: all deliverables landed; self-check all-green (1283/1283); round-2 (B/E/F requests) and
round-3 (F finding #5 verified stale + golden hash confirmed) closed (see bottom). qa.sh --strict
GREEN. Awaiting Fable review.
Date: 2026-07-14 · owner: PROCITY-A (Opus 4.8)
⚠ 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 concurrentgit commitgrabbed my staged index between myaddand mycommit). 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 withgit 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 (6–9m) retail both sides. - Cross streets at 4 stations; the two central ones are wider "second high streets", the outer
two are grittier
backstreets. Allkind:'side', ±8° jitter. - Market square west of the origin — a tidy 5×8 grid of
stalllots + thedeptanchor fronting the spine. - Arcade — a
kind:'arcade'(width 5) pedestrian lane cutting east through a mid-spine block, lined both sides with tiny 3–5m record-heavy shops. - Warehouse fringe — sparse big lots beyond one spine end (N or S, seeded), mostly
infillwith the odd pawn shop. - Residential collar — a loose ring road of
houselots with 2–4 corner milk bars embedded. - Laneways (
kind:'lane') behind the central blocks, withyardlots 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, 2–4 milkbars) ·
all registry + used facades exist in web/assets/gen/. Run over 6 seeds → 1082/1082.
House-law compliance
- Zero
Math.randomand zero THREE imports injs/citygen/*andregistry.js(grep-clean). All randomness flows throughweb/js/core/prng.js. (map.html usesMath.randomonly 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
web/package.jsonadded. Needed sonode web/js/citygen/selfcheck.jsruns the ES modules (browsers use<script type=module>; node needs"type":"module"). It's additive infra — pythonhttp.serverand 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.- Schema extensions (non-breaking). Added
kindto eachblock(its district-kind string, alongside the spec'sdistrictid) so the map & Lane B can theme a block without a district lookup.block.districtis the district id;districts[id].kindis the kind. Everything else matches schema v1 exactly. ryconvention (Lane B please read): a lot'sryis the Y-rotation so a GLB modelled facing −Z ends up with its facade's outward normal pointing at itsfrontEdgestreet. Derivation + a worked example are inplan.js(marchStrip).frontEdgeis a valid edge id for every lot (stalls/dept front the nearest spine edge).pipeline/gen_names.pyintentionally NOT shipped. The Lane A brief offered an optional Ollama word-expansion script, butpipeline/*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 | Amanatides–Woo 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 (Macca’s’s) — {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 B–F 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
0xb5d5cc13 → 0x098eec2b; 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.edgesnow 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)towardfrontEdge; 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 topermittedMax = max≥2 ? min(max+1,3) : maxinstead of the strict registrymax. - 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 pastmin(max+1,3)) now fails loudly. Proved non-vacuous (rejectsvideo@2,record@4,dept@1; allowsrecord@3). Documented thestoreysrule 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.