PROCITY/docs/A-progress.md
m3ultra 42350607a0 Lane C: keeper stand pose on the counter contract (Lane D/F integration)
Answers Lane F §8 (the counter is kind:'counter' in `places`) and closes the
keeper-spawn gap: buildInterior now returns room.counter = { mesh, pose, stand }
and tags the counter interactable with userData.keeperStand. `stand` = {x,z,ry}
where a shopkeeper stands (service side, facing the customer), in room-local
space, rig-front = local -Z per keepers.js. Deterministic; purely additive.

Verified in-browser: 36-build type sweep + 50-room soak — stand in-bounds,
faces customer (dot>0 all), carried on `places` (deep-equal), deterministic;
no change to existing placement, path, leaks, or perf (avg 3.6ms / worst 7.7ms,
geo/tex leak 0).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:37:26 +10:00

162 lines
13 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 — CITYGEN · progress (PROCITY-A)
*Status: **all deliverables landed; self-check all-green (1283/1283); round-2 cross-lane requests from
B/E/F closed** (see bottom). Awaiting Fable review.*
*Date: 2026-07-14 · owner: PROCITY-A (Opus 4.8)*
## 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](../web/js/citygen/plan.js) | `generatePlan(citySeed)`, `chunkIndex(plan)`, `CHUNK`, `chunkKey` |
| 2 | [web/js/citygen/names.js](../web/js/citygen/names.js) + [wordlists.js](../web/js/citygen/wordlists.js) | seeded shop+town names, 50+ patterns, short signboard forms |
| 3 | [web/js/core/registry.js](../web/js/core/registry.js) | `SHOP_TYPES` (all 9), district weights, enums, pure helpers |
| 4 | [web/map.html](../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](../web/js/citygen/selfcheck.js) | `node web/js/citygen/selfcheck.js` full acceptance harness |
| + | [web/package.json](../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 SN 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 edgereal nodes · every blockreal
district · every lotreal block + **valid frontEdge** + positive size · every shopreal 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 1434m) + 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 §83.57`, `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 facadetype 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.