Lane A round 9: freeze CityPlan v2 contract in CITY_SPEC + closing-time occupant ruling
Ship round. Docs + one selfcheck log-line only; no generator change ⇒ all three goldens unchanged (synthetic 0x3fa36874, osm/melbourne 0x34cfdec0, osm/katoomba 0x0f652510). 1. CITY_SPEC v2 contract FROZEN — new "CityPlan v2 — frozen producer contract" section: producer API, goldens table (keyed seed/plansrc/town) + how to pin a new one, town-key mechanism, osm normalization rules, hours/openLate/storeys laws. Matches code as shipped (F diffs claims vs code). Refreshed the stale round-6 Plan-sources blockquote to multi-town reality. Layer-1 changes now require a CITY_SPEC amendment + golden re-pin in the same commit. 2. Recipe sanity — DRY-RAN end-to-end: appended a throwaway Newtown (42 shops), selfcheck auto-ran the full parity suite (1931/1932, only golden unpinned) and printed a paste-ready `newtown: 0x9045e577,`. Reverted; no 3rd town shipped. Added the selfcheck `⚠ UNPINNED — add to OSM_GOLDENS` line so the recipe is mechanical; fixed the recipe wording to match. 3. Closing-time occupant ruling (D's on-call Q) — ratifies + freezes what D's sim.js already does: half-open hours, no new entry when closed, occupants DRAIN (finish bounded dwell) never popped, player not force-ejected, openLate video is the night crowd. In CITY_SPEC + LANE_A_NOTES. No code change asked of D. qa.sh --strict GREEN; selfcheck 1727/1727. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
34145e24a7
commit
e5d4679471
@ -1,9 +1,35 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (1727/1727); rounds 2–8 closed** (see below).
|
||||
*Status: **all deliverables landed; self-check all-green (1727/1727); rounds 2–9 closed** (see below).
|
||||
`qa.sh --strict` GREEN.*
|
||||
*Date: 2026-07-15 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## Round 9 (2026-07-15) — freeze the v2 contract + closing-time ruling (ship round)
|
||||
|
||||
Small round; the most "done" lane makes it official.
|
||||
|
||||
1. **CITY_SPEC v2 contract FROZEN** — added a "CityPlan v2 — frozen producer contract" section: the
|
||||
producer API (`generatePlanFor`/`generatePlan`/`generatePlanOSM`/`osmTownKeys`/`chunkIndex`), the
|
||||
goldens table (synthetic `0x3fa36874`, osm/melbourne `0x34cfdec0`, osm/katoomba `0x0f652510`, keyed
|
||||
(seed,plansrc,town)), the town-key mechanism, osm normalization rules, and the
|
||||
hours/openLate/storeys laws — all matching code as shipped (F diffs claims vs code in review).
|
||||
Refreshed the stale round-6 "Plan sources" blockquote to the multi-town reality. Layer-1 changes now
|
||||
require a CITY_SPEC amendment + golden re-pin in the same commit.
|
||||
2. **Recipe sanity — dry-ran end-to-end** (not just eyeballed): appended a throwaway Newtown (42 shops)
|
||||
to `OSM_TOWNS`, ran selfcheck → the full parity suite auto-covered it (1931/1932, only the golden
|
||||
unpinned) and printed a ready-to-paste `newtown: 0x9045e577,` line. Reverted; no 3rd town shipped
|
||||
(didn't fall out free). Improved selfcheck to print that `⚠ UNPINNED — add to OSM_GOLDENS` line so
|
||||
the recipe is genuinely mechanical; fixed the recipe wording to match.
|
||||
3. **Closing-time occupant ruling** (D's on-call question) — ratifies what D's `sim.js` already does and
|
||||
freezes it: half-open hours, no new entry when closed, **occupants drain (finish their bounded
|
||||
dwell) — never popped**, player not force-ejected, openLate video is the night destination. In
|
||||
CITY_SPEC (contract law) + `LANE_A_NOTES.md` (for D). No code change asked of D.
|
||||
|
||||
Self-check **ALL GREEN 1727/1727**; `qa.sh --strict` GREEN. Only doc + one selfcheck log-line touched;
|
||||
no generator change ⇒ all three goldens unchanged.
|
||||
|
||||
## Round 8 (2026-07-15) — OSM to parity + a second town (carried R7 brief, FULLY LANDED)
|
||||
|
||||
## 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.
|
||||
|
||||
@ -101,22 +101,88 @@ Schema v1 (Lane A owns it; extend, don't break):
|
||||
> furniture never resolves to a chunk that omits its edge; strictly JSON round-trippable (all finite
|
||||
> numbers, no stray keys).
|
||||
|
||||
> **Plan sources (round 6).** A CityPlan can come from two producers behind this one contract, chosen
|
||||
> via `generatePlanFor(seed, source)` (`web/js/citygen/index.js`) — the shell selects with `?plansrc=`:
|
||||
> **Plan sources.** A CityPlan can come from two producers behind this one contract, chosen via
|
||||
> `generatePlanFor(seed, source, { town })` (`web/js/citygen/index.js`) — the shell selects with
|
||||
> `?plansrc=` (+ `&town=` for osm):
|
||||
> - `'synthetic'` (default) — `generatePlan(seed)`, the procedural town. Byte-identical, golden `0x3fa36874`.
|
||||
> - `'osm'` — `generatePlanOSM(seed)`, a real-data import from a **checked-in fixture** (`osm_fixture.js`,
|
||||
> real AU shops from thriftgod's OSM cache; **zero network**). Golden `0x34cfdec0`.
|
||||
> - `'osm'` — `generatePlanOSM(seed, town)`, a real-data import from a **checked-in fixture**
|
||||
> (`osm_fixture.js` → `OSM_TOWNS`; real AU shops from thriftgod's OSM cache; **zero network**).
|
||||
> Towns: `melbourne` (default, golden `0x34cfdec0`), `katoomba` (`0x0f652510`). `osmTownKeys()` lists them.
|
||||
>
|
||||
> An `'osm'` plan sets `source:'osm'`, reports its **real bounding-box `size`** (not 1024²), and contains
|
||||
> **only real retail types** — no synthetic market square, arcade, dept anchor, milk bars, or stalls.
|
||||
> All structural invariants above hold identically. Consumers that assume a synthetic-only feature
|
||||
> (dept/market/milkbar/stall) must guard on `source`. The invariant suite is parameterized by source.
|
||||
> (dept/market/milkbar/stall) must guard on `source`. The invariant suite runs on every (source, town).
|
||||
> **Full frozen v2 producer contract below** (§"CityPlan v2 — frozen producer contract").
|
||||
|
||||
Chunk key: `cx = floor(x/64)`, `cz = floor(z/64)`. **Chunk size 64m.** Lane A ships a
|
||||
`chunkIndex(plan)` helper: chunk key → { lots, shops, edges } touching it. Edges are bucketed across
|
||||
the full road **corridor** (centreline ± `width/2`), so anything placed in the verge (street furniture,
|
||||
footpath NPCs) resolves to a chunk whose `edges` list includes its edge.
|
||||
|
||||
## CityPlan v2 — frozen producer contract
|
||||
|
||||
> **FROZEN at `v2.0` (round 9).** This is the authoritative Layer-1 contract — the v3 epoch
|
||||
> (editor / override layer) reads THIS. It matches the code as shipped; `web/js/citygen/selfcheck.js`
|
||||
> enforces every law here. Changes are a **CITY_SPEC amendment in the same commit + a golden re-pin**,
|
||||
> never a silent drift. Import everything from **`web/js/citygen/index.js`** (the barrel) — internals
|
||||
> (`plan.js`, `plan_osm.js`, `osm_fixture.js`, `names.js`) may move behind it.
|
||||
|
||||
**Producer API** (all from `web/js/citygen/index.js`):
|
||||
- `generatePlanFor(seed, source = 'synthetic', { town } = {}) → CityPlan` — the selector the shell uses.
|
||||
- `generatePlan(seed) → CityPlan` — synthetic producer (unchanged, byte-identical).
|
||||
- `generatePlanOSM(seed, town = 'melbourne', { report } = {}) → CityPlan` — osm producer; pass a
|
||||
`report` object to receive the normalization log (below). `osmTownKeys() → string[]`.
|
||||
- `chunkIndex(plan, chunkSize = 64) → { chunkSize, chunks }`; `CHUNK = 64`; `chunkKey(cx, cz)`.
|
||||
- Geometry helpers (shared with the harness): `lotCorners(lot)`, `obbOverlap(a, b)`.
|
||||
|
||||
**Determinism & goldens.** Same inputs ⇒ byte-identical plan, forever. The determinism gate keys on
|
||||
**(seed, plansrc, town)**. Committed goldens (`xmur3(JSON.stringify(plan))` at seed `20261990`):
|
||||
|
||||
| source / town | golden |
|
||||
|--------------------|--------------|
|
||||
| synthetic | `0x3fa36874` |
|
||||
| osm / melbourne | `0x34cfdec0` |
|
||||
| osm / katoomba | `0x0f652510` |
|
||||
|
||||
*Adding a town* is mechanical (recipe in `docs/LANES/LANE_A_NOTES.md`): append to `OSM_TOWNS`, run
|
||||
selfcheck, paste the printed fingerprint into `OSM_GOLDENS`, tell Lane F the new hash.
|
||||
|
||||
**Trading-hours law.** `shop.hours = [open, close]`, 24h integers, `0 ≤ open < close ≤ 23`. Openness is
|
||||
**half-open**: `isOpen(shop, h) ⟺ open ≤ h < close` (a shop closing at 17 is shut at 17:00 exactly).
|
||||
Both the shell (`index.html`) and the NPC sim (`citizens/sim.js _openAt`) use this identical predicate.
|
||||
|
||||
**openLate law.** **Exactly one** shop per town is the late-night landmark. It is the sole shop that
|
||||
closes `≥ 22:00` (`LATE_HOUR`), so the field and the threshold are equivalent: `{s: s.openLate}` ≡
|
||||
`{s: s.hours[1] ≥ 22}` (a singleton). It is the **video rental** (a town with no video falls back to
|
||||
another non-stall type, logged). Every other shop closes by 21:00 — so after ~21:00 the streets thin
|
||||
to that one lit destination.
|
||||
|
||||
**Storeys law.** `shop.storeys ∈ [registryMin, permittedMax]`, `permittedMax = registryMax ≥ 2 ?
|
||||
min(registryMax + 1, 3) : registryMax`. The occasional 3-storey corner anchor adds ≤1 storey (tall
|
||||
types only); single-storey types (`video`/`milkbar`/`stall`, `[1,1]`) are **never** boosted.
|
||||
|
||||
**OSM normalization rules** (the importer bends the *data* to the contract, never the contract; each
|
||||
change is logged into `report`): unknown OSM `shop=` kind → `opshop` (`report.typesRemapped`); any shop
|
||||
closing `≥ 22:00` → clamped to 21:00 so only the one openLate landmark is late (`report.hoursClamped`);
|
||||
the openLate landmark prefers a video, else a fallback non-stall type (`report.openLate`). Shop `name`s
|
||||
are the real OSM names; `sign` is derived. `osm` plans carry `source:'osm'`, a real bbox `size`, and no
|
||||
synthetic dept/market/milkbar/stall/arcade.
|
||||
|
||||
**Closing-time occupant ruling (Lane A, round 9 — ratifies Lane D's `sim.js`).** When a shop closes:
|
||||
1. **No new entry.** Patronage only ever routes a ped into an *open* shop (`_nearestOpenShop` gates on
|
||||
`_openAt`); the player likewise gets a CLOSED plate/toast and cannot enter (`index.html`).
|
||||
2. **Occupants drain, they don't pop.** A ped already inside when the shop closes finishes its current
|
||||
bounded visit (seeded 5–20s dwell) and re-emerges through the door normally — it is **never**
|
||||
teleported out or culled at the close instant. Occupancy therefore drains to 0 within one dwell
|
||||
window; `sim.occupancyOf(shopId)` falls to 0 on its own. Any browser-rig presence (Lane D/F) tracks
|
||||
`occupancy`, so it clears with the same drain — no orphaned rigs in a shut shop.
|
||||
3. **The player is never force-ejected.** Closing locks the door to *entry* only; a player already inside
|
||||
stays until they choose to leave (leaving restores the street where they stood, as always).
|
||||
4. **The openLate video shop** is the one place still filling after ~21:00 (the night crowd); at its own
|
||||
close (22:00/23:00) it drains identically. Determinism holds: entry/dwell/emerge are seeded + clock
|
||||
driven, so same (seed, time) ⇒ same occupancy.
|
||||
|
||||
### Layer 2 — Streetscape (Lane B)
|
||||
|
||||
Realizes CityPlan chunks around the player. Hard requirements the two parent games skipped:
|
||||
|
||||
@ -2,6 +2,31 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 9 (2026-07-15)
|
||||
|
||||
### → Lane D/F: closing-time occupant ruling (you already built it right — this ratifies + freezes it)
|
||||
|
||||
Question: what happens to a ped browsing inside a shop when it hits closing time? **Ruling** (now a
|
||||
frozen contract law — CITY_SPEC §"CityPlan v2 → Closing-time occupant ruling"):
|
||||
1. **No new entry when closed.** Openness is half-open: `open ≤ h < close` (a shop closing at 17 is shut
|
||||
at 17:00). Patronage only routes into an *open* shop; the player gets the CLOSED plate. ✓ your
|
||||
`sim.js _openAt` + `_nearestOpenShop` already do exactly this.
|
||||
2. **Occupants drain, they don't pop.** A ped inside at close finishes its bounded 5–20s dwell and
|
||||
re-emerges normally — **never** teleported/culled at the close instant. `occupancyOf(shopId)` falls
|
||||
to 0 within one dwell window on its own, so browser rigs (the C→D→F seam) clear with the same drain.
|
||||
✓ your `patronTimer`/`_emerge` already do this — do NOT add a hard close-time eviction.
|
||||
3. **Player is never force-ejected** — closing locks *entry* only; a player inside stays until they leave.
|
||||
4. **The openLate video shop** is the one place still filling after ~21:00; at its own close it drains
|
||||
identically. All seeded + clock-driven ⇒ deterministic occupancy.
|
||||
No code change requested of you — this pins the behavior so it can't drift, and documents it for v3.
|
||||
|
||||
### → everyone / F: the CityPlan v2 producer contract is FROZEN
|
||||
|
||||
CITY_SPEC now has a **"CityPlan v2 — frozen producer contract"** section: producer API, the goldens
|
||||
table (synthetic `0x3fa36874`, osm/melbourne `0x34cfdec0`, osm/katoomba `0x0f652510`), the town-key
|
||||
mechanism, osm normalization rules, and the hours/openLate/storeys/closing laws — all matching code
|
||||
(F: diff away). Changes to Layer-1 now require a CITY_SPEC amendment + golden re-pin in the same commit.
|
||||
|
||||
## Round 8 (2026-07-15)
|
||||
|
||||
### → Lane F: OSM is at parity + there's a SECOND town. Three goldens to pin, one new selector arg.
|
||||
@ -44,10 +69,16 @@ Real OSM data that fights a CityPlan contract is normalized deterministically, a
|
||||
`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.
|
||||
3. **Pin its golden**: run `node web/js/citygen/selfcheck.js`. The unpinned town prints a ready-to-paste
|
||||
line — `⚠ osm/<key>: UNPINNED — add to OSM_GOLDENS → <key>: 0x…,` — paste it into `OSM_GOLDENS`
|
||||
in `selfcheck.js`, re-run → green. (The full parity suite already ran on the town; only the golden
|
||||
needed pinning.)
|
||||
4. **Tell F** the new (seed, plansrc, town) hash for the determinism gate. Done — zero network, the
|
||||
full parity suite covers it automatically.
|
||||
full parity suite covers the town automatically.
|
||||
|
||||
*Dry-run (round 9): verified end-to-end with a throwaway Newtown (42 shops) — the parity suite auto-ran
|
||||
(1931/1932, only the golden unpinned) and selfcheck printed `newtown: 0x9045e577,`. Reverted; no 3rd
|
||||
town shipped (didn't fall out "for free"). The recipe is mechanical as written.*
|
||||
|
||||
## Round 6 (2026-07-14)
|
||||
|
||||
|
||||
@ -197,7 +197,9 @@ for (const town of osmTownKeys()) {
|
||||
}
|
||||
const hash = xmur3(JSON.stringify(generatePlanOSM(20261990, town)))() >>> 0;
|
||||
const want = OSM_GOLDENS[town];
|
||||
ok(want !== undefined && hash === (want >>> 0), `osm/${town}: golden 0x${hash.toString(16)} matches 0x${((want ?? 0) >>> 0).toString(16)}`);
|
||||
// Unpinned town (just added via the "add a town" recipe) → tell the human the exact line to paste.
|
||||
if (want === undefined) console.log(` ⚠ osm/${town}: UNPINNED — add to OSM_GOLDENS → ${town}: 0x${hash.toString(16).padStart(8, '0')},`);
|
||||
ok(want !== undefined && hash === (want >>> 0), `osm/${town}: golden 0x${hash.toString(16)} matches pinned 0x${((want ?? 0) >>> 0).toString(16)}`);
|
||||
console.log(` ${town}: ${report.shops} shops · normalized {typesRemapped:${report.typesRemapped}, hoursClamped:${report.hoursClamped}, openLate:${report.openLate}}`);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user