v1-tag round. Two confirmations + one hardening fix; qa.sh --strict GREEN (4/4).
A2 — openLate: F's night gate `hours[1] >= 22` matched 4–15 shops/seed (regular videos
jitter base-21 → 22), not the single landmark the shell assumes. Fixed in plan.js: regular
shops close by LATE_HOUR-1 (21:00); only the deterministically chosen openLate landmark
reaches ≥22, pick is now video-first. So `hours[1] >= 22` and the `openLate` field are the
same singleton, always the video rental (verified 15 seeds). Makes generation match
web/index.html:159-161 + soak.py:68 ("at night only the openLate shop is open"). selfcheck
asserts exactly-one / field⟺threshold / is-video. ALL GREEN 1301/1301.
A1 — Lane F's F2 permittedMax storeys scoping: confirmed 0 warnings across 7 seeds + a
false-negative test (injected video@2 in a scratch checker → caught every seed).
GOLDEN.hash 0x098eec2b → 0x3fa36874: intended A2 output change (a few videos close 21 not
22 for seed 20261990), NOT drift; F2 is tooling-only. Same lots/shops/types/names/skins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
4.1 KiB
Markdown
64 lines
4.1 KiB
Markdown
# 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 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 **4–15 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.
|