Compare commits

...

2 Commits

Author SHA1 Message Date
m3ultra
b2d29ba112 Lane F R27 (v5.0): the covenant re-pinned, the frame that took five tries, QA GREEN
qa.sh --strict --matrix: 7 passed / 0 failed / 0 warn. selfcheck ALL GREEN 156,352/156,352 ·
MATRIX GREEN 10 towns x 7 gates · scaffold_check GREEN (F's re-pins took) · flags harness green,
carrying the kill-the-server gate and the beta crate gate.

F RE-PINNED ITS OWN FILES per A's handoff (A doesn't edit other lanes' files, correctly):
GOLDEN_HASH 0x3fa36874 -> 0x5f76e76 · GIG_GOLDEN 0xb1d48ea1 -> 0xec7a2d39 · melbourne · katoomba
· the captions. AND A CAUGHT A LIVE HAZARD IN F'S GATE: hash_ok carried `or ('3fa36874' in
r.stdout)` — a bare-string OR-clause, dead the moment the covenant moved and worse than dead,
because it would go green off a SENTENCE ABOUT the old covenant appearing in stdout. Deleted.
The check derives from GOLDEN_HASH or it doesn't exist.

JOHN'S RULING IS THE EPOCH'S BEST LINE: the covenant is anti-drift machinery, not a defect
preservative. 0x3fa36874 did its job for five epochs — it never drifted. It just was never right,
and no hash can catch what was wrong in the first commit. A golden proves NOTHING CHANGED; it
cannot prove anything is CORRECT. Every gate we own was green over a world of back walls, and the
only thing that ever saw it was a human standing in the street looking at a wall.

THE FRONTAGE FRAME TOOK FIVE ATTEMPTS, FOUR OF THEM F'S FAULT:
  1-3. cluster pose (the town's heart isn't this shop) / lot-anchored facing along the edge /
       lot-normal facing — three photographs of empty road. Also never winnable: the shops faced
       backwards the whole time. F was posing a camera at a wall and tuning the geometry.
  4.   B's helper — and F THREW THE RESULT AWAY. poseForShopFront RETURNS a pose; it does not
       apply one. F called it, error-checked it, and never touched the camera, so the frame was
       the spawn paddock — and F's error-check PASSED, because the call succeeded perfectly. The
       vacuous check one last time, in the last frame of the epoch.
  5.   Applied it -> MONSTER ROBOT PARTY. Sign, door, awning, a ped walking in off Musgrave Road.

The measurement that would have found the facade bug in five minutes — compare the two
conventions against each other — is the one nobody ran for nine rounds, F included, right up to
the vacuous 72/72 F nearly filed as "B is wrong".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:48:22 +10:00
m3ultra
c1e862cf7c Lane A R27 wave 5: THE FACADE FIX — it was every town, not just the real ones (the covenant amended)
selfcheck ALL GREEN 156,352/156,352; consistency green. 51 goldens re-pinned in this one commit per the
amendment law. scaffold_check is RED and CANNOT be green from here — the covenant hashes are duplicated in
LANE F's files; exact values handed to F in LANE_A_NOTES §R27w5 (I don't edit other lanes' files).

THE RULING'S PREMISE WAS WRONG, AND THE WRONG HALF WAS LOAD-BEARING.
Wave 5 says "synthetic follows B's canon so every golden has been green over a wrong product since R18".
Half right. Synthetic does NOT follow B's canon -- it is broken identically, and so is the marched path,
and so are all three checked-in fixtures. It is not "every real town since R18". It is EVERY TOWN, EVERY
PATH, SINCE v1. The ruling told me to measure the marched path rather than assume it was clean "because
that's how this one survived nine rounds" -- so I measured all three, and the same discipline caught the
premise itself.

MEASURED FIRST, IN B'S CANON (+Z = the facade), BEFORE TOUCHING ANYTHING:
  synthetic seed 20261990   0 toward / 681 away   (dot = -1.0000, exactly anti-parallel)
  marched katoomba/fremantle/godverse, fixtures melbourne/katoomba/silverton   0 toward, all away
  real-roads katoomba   0/72   <- B's number, reproduced
637 of 681 synthetic lots had their facade pointing at NO STREET AT ALL (44 hit a different street by
corner coincidence). Then I confirmed it with my own eyes rather than trust the arithmetic: booted
?classic=1, stood on the synthetic main street at lot 0 -> three blank grey boxes. Walked BEHIND them into
the block -> NUMBAT PLAYTHINGS, sign/door/windows/awning, facing the dirt. The classic covenant town has
been a row of back walls since v1.
After the fix, same camera, same seed, same ?classic=1: CROWN TOYS & GAMES · MARBLES & KITES · NUMBAT
PLAYTHINGS -- a strip of shopfronts facing the road.

FOUR SITES, ONE SIGN, AND EVERY ONE OF THEM SAID SO IN ITS OWN COMMENT:
  plan.js:122    synthetic strip   "facade faces back down the outward normal, to the street"  <- it didn't
  plan.js:244    market row        "the facade then faces -(outward) = +x"                     <- it didn't
  plan_osm:330   marched avenue    "facade faces +z (north) to the avenue"                     <- due south
  plan_osm:443   real roads        B's proven one-liner (B measured it, then reverted it -- A's file)
All four were atan2(outward) where the intent was atan2(-outward). The code never did what its comment
said, and CITY_SPEC:71 blessed the result by writing the SIM convention into the LOTS contract. Nobody was
wrong; the spec was. After: synthetic 681/681, marched + fixtures 100%, real-roads katoomba 72/72.

THE COVENANT MOVED -- JOHN RULED IT, I DIDN'T.
Fixing synthetic necessarily moves 0x3fa36874, frozen "forever" since R16. That is not Lane A's call, so I
stopped and asked with the evidence. John's ruling: AMEND THE COVENANT, FIX ALL PATHS -- the covenant is
anti-drift machinery, not a defect preservative. 51 pins in this commit:
  classic/synthetic  0x3fa36874 -> 0x5f76e76
  gig                0xb1d48ea1 -> 0xec7a2d39
  fixtures           melbourne 0x9c7e76b3 · katoomba 0xf3aafec8 · silverton 0x6d74c4
  23 real towns      base + gig (46)
Verified byte-identical across two fresh processes; pack-xor 0xa5058322. CITY_SPEC records the amendment as
a ONE-TIME, ruling-gated precedent -- from this hash forward the law reads as it always did.

A SECOND BUG THE FIX EXPOSED (fixed here, counted). buildRealRoads seated blocks at KERB = 4 m from the
CENTRELINE, but a `main` carriageway is 10 m wide -- its kerb is at 5 m. EVERY MAIN-STREET BUILDING HAS
STOOD 1 m INSIDE THE ROAD SINCE R18. The footprint never moved; only which face is the front -- so it was
invisible until the shopfront turned around and its poster landed in the traffic lane (-1.07 m clearance,
6 real failures). Blocks now seat behind the kerb per A's own published corridor law (roadWidth/2 +
FOOTPATH). Pack drops 6.4% -> 6.6%, counted, not hidden.

THE CROSS-CONVENTION GATE -- built to F's lesson, not to a spec line. F proved that measuring lots against
CITY_SPEC:71's own convention is SELF-AGREEMENT: 72/72 "facing" over a town of blank walls. So this gate
restates nothing. It replicates LANE B'S OWN maths from buildings.js (toWorld(lot, 0, y, d/2 + 0.02)) and
asks a physical question neither spec line can fudge: does the quad the RENDERER actually draws land nearer
the street than the lot centre? It fires if A's ry sign drifts OR if B's zf sign drifts. It would have
caught this on day one of v1.

CITY_SPEC amendments: the lots contract now states facade/door/frontage = local +Z; the SIM/rig convention
is documented separately as the different thing it is, with the explicit rule -- never assert a lot's
facing against a spec line, assert it against the geometry the renderer draws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:28:20 +10:00
17 changed files with 336 additions and 116 deletions

View File

@ -1,6 +1,41 @@
# LANE A — CITYGEN · progress (PROCITY-A)
*Status: **all deliverables landed; self-check ALL GREEN (156,212/156,212); rounds 227 closed** (see below).
*Status: **all deliverables landed; self-check ALL GREEN (156,352/156,352); rounds 227 (incl. wave 5) closed**.
R27 w5 THE FACADE FIX: every town — synthetic included — had its shopfronts facing away from the street
since **v1**. Fixed in all four ry sites; **51 goldens re-pinned**; the classic covenant amended on John's
ruling (`0x3fa36874 → 0x5f76e76`); a cross-convention gate added so it can never recur.*
## Round 27 wave 5 (2026-07-17) — THE FACADE FIX (the epoch's biggest catch)
The ruling asked me to land B's one-liner in the real-roads seating and to **measure the marched path
rather than assume it was clean**. I measured all three paths, and the ruling's premise was wrong in the
half that mattered: **synthetic is broken too**. Not "every real town since R18" — **every town, every
path, since v1**. Confirmed numerically (0 toward / 681 away, dot 1.0000) and then with my own eyes:
`?classic=1`, synthetic main street = three blank boxes; walk into the block and NUMBAT PLAYTHINGS is
there, facing the dirt. 637 of 681 lots pointed their facade at **no street at all**.
- **Four sites, one sign.** `plan.js:122` (synthetic strip), `plan.js:244` (market row), `plan_osm:330`
(marched avenue), `plan_osm:443` (real roads — B's proven one-liner). All `atan2(outward)` where the
intent was `atan2(outward)`, and **every one says so in its own comment**. The code never did what it
said; CITY_SPEC:71 blessed the result by writing the SIM convention into the LOTS contract.
After: synthetic 681/681, marched + all 3 fixtures 100%, real-roads katoomba **0/72 → 72/72** (B's number).
- **The covenant moved, on John's ruling.** Fixing synthetic necessarily moves `0x3fa36874`, frozen
"forever" since R16 — not Lane A's call, so I stopped and asked. Ruling: **amend and fix all paths** (the
covenant is anti-drift machinery, not a defect preservative). **51 goldens re-pinned in one commit**;
amendment recorded in CITY_SPEC as a one-time, ruling-gated precedent.
- **A second bug the fix exposed, fixed with it:** `buildRealRoads` seated blocks at `KERB = 4 m` from the
**centreline**, but a `main` carriageway is 10 m wide (kerb at 5 m) — **every main-street building has
stood 1 m inside the road since R18**. Invisible until the shopfront turned around and its poster landed
in the traffic lane. Blocks now seat behind the kerb per A's own corridor law. Drops 6.4% → **6.6%**, counted.
- **The cross-convention gate (the lesson made law).** It does not restate a spec line — that is the trap F
proved (72/72 "facing" over blank walls). It replicates **Lane B's own** `buildings.js` maths and asks
whether the quad the *renderer* draws lands nearer the street than the lot centre. Fires if A's `ry` **or**
B's `zf` drifts. It would have caught this on day one.
- **→ F (blocking their re-gate):** `scaffold_check` is RED until F re-pins the covenant hashes duplicated
in **F's own files** (`flags_check.py` `GOLDEN_HASH`/`GIG_GOLDEN`, `scaffold_check.mjs` `TOWN_GOLDENS`,
two tour captions). Exact values handed over in LANE_A_NOTES §R27w5; I can't touch F's files.
*Previous status: 156,212/156,212; rounds 227 closed (see below).
R27 (v5.0 close): **CITY_SPEC v5 section** (tier ladder · `sourcing` vs `tier` · the three id-space fences ·
the manifest · Ruling 2) + the **charter risk list judged line by line** — risk #1 deliberately CARRIED, not
pre-retired, because F's kill-the-server gate is what retires it. classic `0x3fa36874` / gig `0xb1d48ea1`

View File

@ -28,7 +28,21 @@ considered and parked: nothing in v1 needs it, and the asset pipeline (GLB, web-
- **Metres. +Y up. Ground plane is XZ.** City origin (0,0) = centre of the main square.
- **Right-handed three.js defaults.** Buildings face their street; a facade's outward normal
points at its street edge. GLB convention: origin at base, facing **Z**, real-world scale.
points at its street edge.
- **THE TWO CONVENTIONS — they are different things, and conflating them cost nine rounds
(ROUND27). Read both:**
- **The FACADE convention (the lots contract, `lot.ry`): the visible shopfront is local `+Z`**
world `(sin ry, cos ry)`. This is B's canon and the de facto truth since v1 — `buildings.js`
draws the facade quad and door at local `z = +d/2`, `venue.js` seats the door-glow/queue/
posters there. **`lot.ry` aims +Z at `frontEdge`.**
- **The SIM/RIG convention (props, not lots): a GLB is modelled facing `Z`** (origin at base,
real-world scale) ⇒ rig-forward is world `(sin ry, cos ry)`. This describes *how a model is
authored*, **not which way a lot faces.**
- **They are opposites, so a check written in one silently passes over a world built in the
other.** CITY_SPEC:71 wrote the SIM convention into the LOTS contract by ambiguity; each
generator implemented one half; every golden stayed green over a town whose every shopfront
faced away. **Never assert a lot's facing against a spec line — assert it against the
geometry the renderer actually draws** (`selfcheck.js` now does exactly that).
- **Seeded everything, `Math.random()` is banned in generation code.**
`web/js/core/prng.js` is the only randomness source:
- `seedFor(citySeed, kind, id)` → stable uint32 (xmur3 hash of `"seed:kind:id"`).
@ -68,8 +82,11 @@ Schema v1 (Lane A owns it; extend, don't break):
// onto the block so consumers can theme it without a lookup (Lane A extension, non-breaking).
lots: [ { id, block, x, z, w, d, ry, frontEdge, use } ],
// use: 'shop' | 'anchor' | 'house' | 'yard' | 'stall' | 'infill'
// ry: Y-rotation (radians) so a GLB modelled facing Z ends up with its facade's OUTWARD normal
// pointing at frontEdge. World facing = (sin ry, cos ry). Lane B rotates the shell by ry.
// ry: Y-rotation (radians) so the lot's VISIBLE FACADE — local +Z — points at frontEdge.
// Facade/door/frontage normal = local +Z ⇒ world (sin ry, cos ry). Lane B rotates the shell by ry
// and draws the shopfront at local z = +d/2 (buildings.js), so +Z IS the street face. ROUND27.
// (The SIM/rig convention — a GLB modelled facing Z, world (sin ry, cos ry) — is a DIFFERENT
// thing and is documented as such below. It is not the lots contract. See the amendment.)
// frontEdge: id of the street edge this lot fronts (guaranteed to exist and be geometrically adjacent).
shops: [ { id, lot, type, name, sign, seed, facadeSkin, storeys, hours: [open, close], openLate? } ]
// type: see SHOP TYPES below. hours: [open, close], 24h integers, 0 ≤ open < close 23.
@ -256,9 +273,30 @@ night in v1; interior-mapping shader is a stretch goal).
> did not disappear. `gigs`, `weather`, `winmap`, `tram` now default **ON**; the default boot is the full
> experience, deterministic to the **gig golden `0xb1d48ea1`**. Each flag keeps a `=0` opt-out, and
> **`?classic=1`** forces all four off at once:
> - **Classic law (the new frozen regression target):** the `?classic=1` boot is byte-identical to the v2
> baseline **forever** — synthetic fingerprint `0x3fa36874`, zero gig layer, **zero fetch delta** (no
> - **Classic law (the frozen regression target):** the `?classic=1` boot is byte-identical to the v2
> baseline — synthetic fingerprint **`0x5f76e76`**, zero gig layer, **zero fetch delta** (no
> `custom_bands.json`, no `sit.glb`). The old "flags-off regression" gate is now the **classic-regression** gate.
>
> > **THE COVENANT AMENDMENT — ROUND27, THE FACADE FIX (John's ruling; the only amendment in five epochs).**
> > The covenant froze `0x3fa36874` "forever" from R16 to R27. That hash anchored **a wrong world**: every
> > shopfront in every town — synthetic and real — faced *away* from its street, because CITY_SPEC:71 wrote
> > the SIM convention into the LOTS contract (see *Units, axes*). The covenant did exactly its job for nine
> > rounds: it held the base town byte-identical. It just held it identical to something broken, and no gate
> > could see it — a self-consistent check over a self-contradicting spec passes under either convention.
> >
> > **The covenant's purpose is anti-drift, not the preservation of a defect.** So on John's ruling it is
> > **amended once, deliberately, in the open**: the anchor moves `0x3fa36874 → 0x5f76e76`, and every other
> > golden moves with it (gig `0xb1d48ea1 → 0xec7a2d39`, the three marched fixtures, all 23 real towns base
> > + gig — **51 pins in one commit**, per the amendment law). The classic town is now what it always claimed
> > to be: a street you can stand on and read the shopfronts. **From this hash forward the law reads as it
> > always did — byte-identical forever.** An amendment requires a John-level ruling and a demonstrated
> > world-level defect, not a convenience; this is the precedent, not an opening.
> >
> > *Exposed by the same fix (and fixed with it): `buildRealRoads` seated blocks at `KERB = 4 m` from the
> > centreline while a `main` carriageway is 10 m wide — its kerb is at 5 m. Every main-street building has
> > stood 1 m inside the road since R18. Invisible while nothing was drawn on that face; the moment the
> > shopfront turned around, its poster landed in the traffic lane. Blocks now seat behind the kerb per the
> > corridor law (`roadWidth/2 + FOOTPATH`). Pack drops 6.4% → 6.6%, counted.*
> - **Default law:** the no-flag boot pins to the gig golden, and every gig / district / continuity / cover
> smoke **and the budget law** (interior ≤350 draws · street ≤300 / 200k tris, at the busiest venue block)
> now run against the **default boot with all four flags live**. `?noassets=1` composes with it and stays

View File

@ -2,6 +2,68 @@
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
## Round 27 wave 5 (2026-07-17) — THE FACADE FIX: it was every town, not just the real ones
### → F: **your files hold the old covenant hashes — they are now wrong, and only you can fix them**
This is the one thing blocking your re-gate. `scaffold_check` is **RED** until you re-pin, and I can't
touch your files. Exact values (verified byte-identical across two fresh processes):
| your file | constant | old | **new** |
|---|---|---|---|
| `tools/flags_check.py:30` | `GOLDEN_HASH` | `0x3fa36874` | **`0x5f76e76`** |
| `tools/flags_check.py:31` | `GIG_GOLDEN` | `0xb1d48ea1` | **`0xec7a2d39`** |
| `tools/qa/scaffold_check.mjs:231` | `TOWN_GOLDENS.melbourne` | `0x34cfdec0` | **`0x9c7e76b3`** |
| `tools/qa/scaffold_check.mjs:231` | `TOWN_GOLDENS.katoomba` | `0x0f652510` | **`0xf3aafec8`** |
| `tools/qa/scaffold_check.mjs:225` | the comment quoting all three | — | same three |
| `tools/qa/tour_v5.py:42`, `tour_shots.py:10` | captions quoting `0x3fa36874` | — | **`0x5f76e76`** |
Also: `flags_check.py:148` greps stdout for the literal `'3fa36874'` — that will silently stop matching.
### The finding: the ruling's premise was wrong — **synthetic was broken too**
The wave-5 ruling says *"synthetic follows B's canon so every golden has been green over a wrong product
since R18"*. Half right, and the wrong half is load-bearing. **Synthetic does not follow B's canon** — it
is broken identically, and so is the marched path, and so are the three checked-in fixtures. It is not
"every real town since R18": it is **every town, every path, since v1**. Measured in B's canon before
touching anything, then confirmed with my own eyes in the browser:
| path | before | after |
|---|---|---|
| synthetic (`plan.js`) — **the classic covenant** | **0 toward / 681 away** | 681 / 0 |
| marched-osm + the 3 fixtures | 0 / 80, 0 / 95, 0 / 19, 0 / 12 | all toward |
| real-roads (B's finding) | 0 / 72 katoomba | **72 / 72** — matches B exactly |
**The screenshot is the proof:** standing on the synthetic main street at `?classic=1`, lot 0 — three blank
grey boxes. Walk *behind* them, into the block, and there is NUMBAT PLAYTHINGS: sign, door, windows, awning,
facing the dirt. Same camera after the fix: CROWN TOYS & GAMES · MARBLES & KITES · NUMBAT PLAYTHINGS, a
strip of shopfronts facing the road. **637 of 681 synthetic lots had their facade pointing at no street at
all.**
**Why it survived**: all four sites are the same one-sign error — `atan2(outward)` where the intent was
`atan2(outward)` — and *every one of them says so in its own comment* (`plan.js:122` "facade faces back
down the outward normal, to the street"). The code never did what its comment said, and CITY_SPEC:71 blessed
the result. Nobody was wrong; the spec was.
### The covenant moved — John ruled it
Fixing synthetic necessarily moves `0x3fa36874`, frozen "forever" since R16. That was not mine to decide.
**John's ruling: amend the covenant, fix all paths** — the covenant is anti-drift machinery, not a defect
preservative. **51 goldens re-pinned in one commit** (classic `0x3fa36874 → 0x5f76e76`, gig
`0xb1d48ea1 → 0xec7a2d39`, 3 fixtures, 23 towns × base+gig). The amendment is recorded in CITY_SPEC as a
one-time, ruling-gated precedent — from this hash forward the law reads as it always did.
### A second bug the fix exposed (fixed here, counted)
`buildRealRoads` seated blocks at `KERB = 4 m` **from the centreline** — but a `main` carriageway is 10 m
wide, so its kerb is at 5 m. **Every main-street building has stood 1 m inside the road since R18.** The
footprint never moved; only which face is the front — so it was invisible until the shopfront turned around
and its poster landed in the traffic lane (1.07 m). Blocks now seat behind the kerb per A's own corridor
law (`roadWidth/2 + FOOTPATH`). Pack drops **6.4% → 6.6%**, counted, not hidden.
### The cross-convention gate — built to F's lesson, not to a spec line
F proved that measuring lots against CITY_SPEC:71's own convention is **self-agreement**: 72/72 "facing" over
a town of blank walls. So the new gate does not restate a spec line. It **replicates Lane B's own placement
maths** from `buildings.js` (`toWorld(lot, 0, y, d/2 + 0.02)`) and asks a physical question neither spec line
can fudge: *does the quad the renderer actually draws land nearer the street than the lot centre?* It fires
if A's `ry` sign drifts **or** if B's `zf` sign drifts. It would have caught this on day one of v1.
## Round 27 (2026-07-17) — v5.0: the docs freeze — CITY_SPEC v5 + the risk list judged (ledger #6)
### → F + Fable: **A's docs half is done and the tree is green; nothing of mine gates your tag**

View File

@ -1338,3 +1338,38 @@ untouched by this — the crate is real either way).
instancing work in R22 — 348 tris is ~29 boxes. It wasn't efficiency, it was the buildings being absent
from frame. B read the evidence as a win for five rounds. A number you like is the easiest vacuous gate
of all.*
### Wave 5 — the covenant amended, and the frame that took five tries
A's fix landed and **A caught the ruling's premise**: it was never "the real towns since R18". Synthetic is
broken identically — **0 toward / 681 away, dot = 1.0000** — and so is the marched path, and all three
fixtures. **EVERY TOWN, EVERY PATH, SINCE v1.** A booted `?classic=1`, stood on the synthetic main street,
found three blank grey boxes, walked *behind* them and found **NUMBAT PLAYTHINGS facing the dirt**. The
covenant town has been a row of back walls since the first commit.
**F re-pinned its own files** per A's handoff (A doesn't edit other lanes' files, correctly):
`GOLDEN_HASH 0x3fa36874 → 0x5f76e76` · `GIG_GOLDEN → 0xec7a2d39` · melbourne · katoomba · the captions.
**And A caught a live hazard in F's gate**: `hash_ok` carried `or ('3fa36874' in r.stdout)` — a bare-string
OR-clause that was dead the moment the covenant moved, and worse than dead, because it would go green off
a *sentence about* the old covenant appearing in stdout. Deleted. The check derives from `GOLDEN_HASH` or
it doesn't exist.
**John's ruling is the epoch's best line: the covenant is anti-drift machinery, not a defect
preservative.** `0x3fa36874` did exactly its job for five epochs — it never drifted. It just was never
right, and **no hash can catch what was wrong in the first commit**. A golden proves *nothing changed*; it
cannot prove *anything is correct*. Every gate we own was green over a world of back walls, and the only
thing that ever saw it was a human standing in the street looking at a wall.
**The frontage frame took five attempts and four of them were F's fault:**
1. cluster pose → the town's heart, which in Red Hill isn't Monster Robot. Empty road.
2. lot-anchored, facing along the edge → the road, shop beside the lens.
3. lot-normal facing → bitumen.
4. **B's helper — and F threw the result away.** `poseForShopFront` *returns* a pose; it does not apply
one. F called it, error-checked it, and never touched the camera, so the frame was the spawn paddock.
*A helper that hands you a value is not a helper that does the thing* — and F's own error-check passed,
because the call succeeded perfectly. The vacuous check, one last time, in the last frame of the epoch.
5. Applied it → **MONSTER ROBOT PARTY**, sign, door, awning, a ped walking in off Musgrave Road.
Attempts 13 were also never winnable: the shops faced backwards the whole time. **F was posing a camera
at a wall and tuning the geometry.** The measurement that would have found it in five minutes — compare
the two conventions against each other — is the one nobody ran for nine rounds, F included, right up to
the vacuous 72/72 F nearly filed as "B is wrong".

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 744 KiB

After

Width:  |  Height:  |  Size: 784 KiB

View File

@ -2,7 +2,7 @@
<style>body{background:#111;color:#ddd;font:15px/1.6 -apple-system,sans-serif;max-width:1100px;margin:40px auto;padding:0 20px}img{width:100%;border-radius:6px;margin:10px 0}h1{font-weight:600}p{color:#aaa}hr{border:0;border-top:1px solid #333;margin:36px 0}</style>
<h1>PROCITY v5.0 — THE LIVE CRATE</h1>
<p>One real record shop, on its real street, holding its real stock — and a server that can die mid-session without the game noticing.</p>
<hr><h3>musgrave_road</h3><img src='musgrave_road.png'><p>the frontage — Monster Robot Party on Musgrave Road, Red Hill, Brisbane. It is on this road in the game because it is on this road · Red Hill · retail heart: 2 shops/60m · 95 draws · 64,109 tris</p>
<hr><h3>musgrave_road</h3><img src='musgrave_road.png'><p>the frontage — Monster Robot Party on Musgrave Road, Red Hill, Brisbane. It is on this road in the game because it is on this road · Red Hill · retail heart: 2 shops/60m · 91 draws · 62,576 tris</p>
<hr><h3>the_real_crate</h3><img src='the_real_crate.png'><p>THE REAL CRATE — Monster Robot Party (147 Musgrave Rd): 120 records off its own till, real covers, real titles, real prices. Sell one in the shop and it leaves this crate · Monster Robot Party · 120 records from assets/stock_godverse/3962749/ · 40 draws · 40,239 tris</p>
<hr><h3>a_mint_crate</h3><img src='a_mint_crate.png'><p>a MINT crate — plausible, NOT real: a seeded sample standing in for a shop whose till we don't hold. Same tier, same offline guarantee, different provenance — and the atlas says so in its own index · Presents of Mind · 16 records from assets/stock_godverse/767/ · 51 draws · 30,897 tris</p>
<hr><h3>classic_covenant</h3><img src='classic_covenant.png'><p>?classic=1 — the exact v2 town, byte-identical forever (0x3fa36874). Five epochs, one covenant · Boolarra Heads · retail heart: 35 shops/60m · 156 draws · 38,855 tris</p>
<hr><h3>classic_covenant</h3><img src='classic_covenant.png'><p>?classic=1 — the exact v2 town, byte-identical forever (0x5f76e76). Five epochs, one covenant · Boolarra Heads · retail heart: 35 shops/60m · 162 draws · 39,259 tris</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 748 KiB

After

Width:  |  Height:  |  Size: 778 KiB

View File

@ -2,7 +2,7 @@
{
"name": "musgrave_road",
"file": "musgrave_road.png",
"caption": "the frontage \u2014 Monster Robot Party on Musgrave Road, Red Hill, Brisbane. It is on this road in the game because it is on this road \u00b7 Red Hill \u00b7 retail heart: 2 shops/60m \u00b7 95 draws \u00b7 64,109 tris"
"caption": "the frontage \u2014 Monster Robot Party on Musgrave Road, Red Hill, Brisbane. It is on this road in the game because it is on this road \u00b7 Red Hill \u00b7 retail heart: 2 shops/60m \u00b7 91 draws \u00b7 62,576 tris"
},
{
"name": "the_real_crate",
@ -17,6 +17,6 @@
{
"name": "classic_covenant",
"file": "classic_covenant.png",
"caption": "?classic=1 \u2014 the exact v2 town, byte-identical forever (0x3fa36874). Five epochs, one covenant \u00b7 Boolarra Heads \u00b7 retail heart: 35 shops/60m \u00b7 156 draws \u00b7 38,855 tris"
"caption": "?classic=1 \u2014 the exact v2 town, byte-identical forever (0x5f76e76). Five epochs, one covenant \u00b7 Boolarra Heads \u00b7 retail heart: 35 shops/60m \u00b7 162 draws \u00b7 39,259 tris"
}
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -3,7 +3,7 @@
The machine version of what the R5 review did by hand. Three things, headless, <2 min:
1. FLAGS-OFF REGRESSION the enforcement arm of the prime law. Boot with no flags the plan
fingerprint must equal the golden 0x3fa36874, the settled street view must stay within the v1
fingerprint must equal the golden 0x5f76e76, the settled street view must stay within the v1
budget (300 draws / 200k tris) AND within tolerance of the v1.1 snapshot, every v2 subsystem
must report inactive, and there must be ZERO console errors. A v2 flag leaking into the default
boot trips this.
@ -27,8 +27,13 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent
PORT = 8130
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
GOLDEN_HASH = 0x3fa36874 # frozen v2 determinism anchor (?classic boot) — UNCHANGED by the flips
GIG_GOLDEN = 0xb1d48ea1 # [R16] the DEFAULT boot's gig-augmented plan fingerprint (A's R16 corner-poster re-pin)
GOLDEN_HASH = 0x5f76e76 # [R27w5] AMENDED. Was 0x3fa36874 "forever" since R16 — and that town
# was a row of BACK WALLS since v1 (A measured 0 toward / 681 away;
# walked behind them and found NUMBAT PLAYTHINGS facing the dirt).
# John's ruling: the covenant is anti-drift machinery, NOT a defect
# preservative. It did its job — it never drifted; it was wrong from
# the first commit, and no hash can catch what was never right.
GIG_GOLDEN = 0xec7a2d39 # [R27w5] re-pinned with the facade fix (was 0xb1d48ea1)
# flags-off DBG.shot('street_noon') snapshot. RE-PINNED at the R7 roster flip (prime-law amendment):
# old (v1.1, roster-off): 163 draws, 19000 tris
# new (R7, roster default-on): 164 draws, ~66000 tris
@ -119,7 +124,7 @@ def plansrc_landed(p):
# ── the three checks ─────────────────────────────────────────────────────────
def classic_regression(p):
# [R16 — THE FLIP] The prime-law covenant MOVED from "flags off" to "?classic=1": the classic boot must
# stay byte-identical to the frozen v2 baseline FOREVER — synthetic fingerprint 0x3fa36874, zero gig
# stay byte-identical to the frozen v2 baseline FOREVER — synthetic fingerprint 0x5f76e76, zero gig
# layer, no opt-in flags, the R7 streamed roster (which IS the v2.0 baseline). Same assertions as the old
# flags-off gate, now booted with ?classic=1 (the four flipped flags are off, everything else v2-default).
head('CLASSIC REGRESSION (?classic=1 → the frozen v2 covenant)')
@ -144,13 +149,16 @@ def classic_regression(p):
# determinism anchor — run Lane A's selfcheck in-process (golden fingerprint)
r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT),
capture_output=True, text=True)
hash_ok = (f'{GOLDEN_HASH:#010x}'.replace('0x', '0x') in r.stdout) or (hex(GOLDEN_HASH) in r.stdout) \
or ('3fa36874' in r.stdout)
# [R27w5] The hardcoded literal that used to ride here (`or '3fa36874' in r.stdout`) is GONE —
# A caught it. It was dead the moment the covenant moved, and worse than dead: an OR-clause
# matching a bare string anywhere in stdout would go green off a sentence ABOUT the old covenant.
# The check derives from GOLDEN_HASH or it doesn't exist.
hash_ok = (f'{GOLDEN_HASH:#010x}' in r.stdout) or (hex(GOLDEN_HASH) in r.stdout)
# [R24] These are TWO assertions and they get two verdicts. This gate used to require
# `hash_ok and r.returncode == 0` and report BOTH failures with the fingerprint's message — so
# any unrelated red anywhere in Lane A's selfcheck (R24: 8 godverse-identity checks in a
# different town) screamed "determinism/prime-law broken", the loudest alarm we own, about a
# covenant that was provably intact (0x3fa36874, node-side, unmoved). A gate must name what it
# covenant that was provably intact (then 0x3fa36874, node-side, unmoved). A gate must name what it
# actually measured; F wrote this one, so F fixed it. The covenant is asserted on its own.
if hash_ok: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} — the classic covenant holds')
else: FAIL(f'plan fingerprint != golden {hex(GOLDEN_HASH)} — determinism/prime-law broken')
@ -235,7 +243,7 @@ def default_boot_gate(p):
else:
FAIL(f"default boot not fully live: gig={present}, tram={state['tram']}")
# The default boot's plan IS the gig-augmented plan; selfcheck pins its fingerprint to the gig golden
# (0xb1d48ea1) INTERNALLY and is ALL GREEN only if it matches. (selfcheck prints the base fingerprint
# (GIG_GOLDEN) INTERNALLY and is ALL GREEN only if it matches. (selfcheck prints the base fingerprint
# in its summary but asserts the gig golden silently — so we gate on the green run, not a substring.)
r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT), capture_output=True, text=True)
if r.returncode == 0 and 'ALL GREEN' in r.stdout:

View File

@ -222,13 +222,13 @@ if (existsSync(R('web/js/citygen/plan.js'))) {
`${p1?.streets?.edges?.length ?? '?'} edges, name "${p1?.name ?? '?'}"`);
// [F2/R8] plan-source selector — pin goldens keyed by (seed, plansrc, TOWN).
// Lane A contract: synthetic 0x3fa36874 (selfcheck) · osm/melbourne 0x34cfdec0 · osm/katoomba 0x0f652510.
// Lane A contract: synthetic 0x5f76e76 (selfcheck) · osm/melbourne 0x9c7e76b3 · osm/katoomba 0xf3aafec8.
// Guarded: no-op until A ships generatePlanFor + generatePlanOSM.
try {
const bar = await import(R('web/js/citygen/index.js'));
if (typeof bar.generatePlanFor === 'function' && typeof bar.generatePlanOSM === 'function') {
const { xmur3 } = await import(R('web/js/core/prng.js'));
const TOWN_GOLDENS = { melbourne: '0x34cfdec0', katoomba: '0x0f652510' };
const TOWN_GOLDENS = { melbourne: '0x9c7e76b3', katoomba: '0xf3aafec8' };
for (const [town, golden] of Object.entries(TOWN_GOLDENS)) {
const a = bar.generatePlanFor(20261990, 'osm', { town }), b2 = bar.generatePlanFor(20261990, 'osm', { town });
assert(JSON.stringify(a) === JSON.stringify(b2),

View File

@ -7,7 +7,7 @@ The v4 thesis in five frames, into docs/shots/release_v4/ (+ contact.html). The
fitzroy_mecca the mecca (160 shops, 2× anything absorbed before this epoch)
toowoomba_thintail the thin tail (12 shops): proof the floor still reads as a town
synthetic_control the procedural town, UNCHANGED: the control the whole epoch was measured against
classic_covenant `?classic=1`: the v2 covenant made visible, byte-identical forever (0x3fa36874)
classic_covenant `?classic=1`: the v2 covenant made visible, byte-identical forever (0x5f76e76)
Posing: every real-town frame resolves through Lane B's `DBG` retail-heart anchor / `clusterPose` — the
primitive that came out of F's R21 finding (per-EDGE shop counts are ~1 on a real graph, so "densest edge"

View File

@ -39,7 +39,7 @@ V5_TOUR = [
"a MINT crate — plausible, NOT real: a seeded sample standing in for a shop whose till we don't hold. "
"Same tier, same offline guarantee, different provenance — and the atlas says so in its own index"),
('classic_covenant', f'classic=1&seed={SEED}', 2, 'street',
"?classic=1 — the exact v2 town, byte-identical forever (0x3fa36874). Five epochs, one covenant"),
"?classic=1 — the exact v2 town, byte-identical forever (0x5f76e76). Five epochs, one covenant"),
]
SHOT_JS = r"""
@ -82,47 +82,41 @@ async ({ mode, seg, shop }) => {
}
// street
// R21's cluster primitive answers "where is this town's heart". For `frontage:<id>` the question is
// "where is THIS shop", and they are not the same question: Red Hill's densest cluster is 3 shops/60m
// and does not contain Monster Robot, so the cluster pose produced a beautiful photograph of an empty
// footpath the Fitzroy trap a third time, wearing the fix for the first two. Anchor on the subject
// when the frame is ABOUT the subject; use the cluster only when the frame is about the town.
// [R27w5] The frontage frame now goes through B's `poseForShopFront` — B's helper, B's geometry, B's
// shop. F re-derived this three times and produced three photographs of empty road; the fourth attempt
// would have been the same mistake with more arithmetic. The lane that owns the frontage owns the pose.
// (And the empty road was never the pose at all the shops faced backwards. Fixed by A, wave 5.)
const nById = new Map(plan.streets.nodes.map(n => [n.id, n]));
const lots = (plan.lots || []).filter(l => l.use === 'shop' || l.use === 'anchor');
let hub = -1, best = null;
if (mode.startsWith('frontage:')) {
const gid = +mode.split(':')[1];
const sh = shops.find(x => x.godverseShopId === gid);
best = sh ? (plan.lots || []).find(l => l.id === sh.lot) : null; // shop.lot lot.id
if (best) { hub = lots.filter(o => Math.hypot(o.x - best.x, o.z - best.z) < 60).length; }
if (!best) return { err: 'no lot for godverse ' + gid };
const r = D.poseForShopFront('g:' + mode.split(':')[1], 8);
if (!r || r.error) return { err: 'poseForShopFront: ' + JSON.stringify(r) };
// poseForShopFront RETURNS a pose; it does not apply one. F called it, error-checked it, and threw
// the result away so the camera never moved and the frame was the spawn paddock. A helper that
// hands you a value is not a helper that does the thing.
P.camera.position.set(r.pos[0], r.pos[1], r.pos[2]);
P.camera.lookAt(r.look[0], r.look[1], r.look[2]);
P.camera.updateMatrixWorld();
const sh = shops.find(x => x.godverseShopId === +mode.split(':')[1]);
const lot = sh ? (plan.lots || []).find(l => l.id === sh.lot) : null;
hub = lot ? lots.filter(o => Math.hypot(o.x - lot.x, o.z - lot.z) < 60).length : -1;
} else {
for (const l of lots) { let c = 0; for (const o of lots) if (Math.hypot(o.x - l.x, o.z - l.z) < 60) c++;
if (c > hub) { hub = c; best = l; } }
}
if (best) {
const e = (plan.streets.edges || []).find(x => x.id === best.frontEdge) || plan.streets.edges[0];
const a = nById.get(e.a), b2 = nById.get(e.b);
const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L;
let t = (best.x - a.x) * ux + (best.z - a.z) * uz;
t = Math.max(6, Math.min(L - 6, t));
if (mode.startsWith('frontage:')) {
// A frontage frame FACES the shop. The cluster pose faces ALONG the edge correct for "here is a
// street", useless for "here is this shop": it photographed Musgrave Road with Monster Robot out of
// frame beside the lens. Stand on the road at the lot's projection, step back along the lot→road
// normal, and look AT it.
const px = a.x + ux * t, pz = a.z + uz * t; // the lot's own point on the road
let nx = px - best.x, nz = pz - best.z; // lot road normal
const NL = Math.hypot(nx, nz) || 1; nx /= NL; nz /= NL;
const cx = best.x + nx * (NL + 7), cz = best.z + nz * (NL + 7);
D.teleport(cx, cz, Math.atan2(best.x - cx, best.z - cz));
} else {
if (best) {
const e = (plan.streets.edges || []).find(x => x.id === best.frontEdge) || plan.streets.edges[0];
const a = nById.get(e.a), b2 = nById.get(e.b);
const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L;
let t = (best.x - a.x) * ux + (best.z - a.z) * uz;
t = Math.max(6, Math.min(L - 6, t));
const back = Math.min(26, t);
D.teleport(a.x + ux * (t - back), a.z + uz * (t - back), Math.atan2(ux, uz));
}
} else { D.shot('street_noon'); }
} else { D.shot('street_noon'); }
}
await new Promise(r => setTimeout(r, 1800));
const hud = document.getElementById('pc-hud'); if (hud) hud.style.display = 'none';
for (const el of document.body.children) if (el.tagName !== 'CANVAS') el.style.display = 'none';
for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r));
const info = D.info();
return { name: plan.name, shops: shops.length, edges: (plan.streets.edges || []).length,

View File

@ -119,7 +119,11 @@ export function generatePlan(citySeed) {
const c3 = [A.x + nx * (half + stripDepth), A.z + nz * (half + stripDepth)];
const blockId = addBlock(districtId, districtKind, [c0, c1, c2, c3]);
out.blockId = blockId;
const ry = Math.atan2(nx, nz); // facade faces back down the outward normal, to the street
// ROUND27 THE FACADE FIX: aim the VISIBLE facade (local +Z — B's canon, what buildings.js draws) back
// down the outward normal, at the street. The old `atan2(nx, nz)` put +Z along the OUTWARD normal, i.e.
// facing away — this line's own comment has described the intended result since v1; only the sign was
// wrong. It read correct because CITY_SPEC:71 wrote the SIM convention (Z) into the lots contract.
const ry = Math.atan2(-nx, -nz); // facade (+Z) faces back down the outward normal, to the street
const r = rng(citySeed, streamKind, streamId);
const iStart = band.insetStart ?? 0, iEnd = band.insetEnd ?? 0;
let t = iStart; const end = len - iEnd;
@ -237,11 +241,14 @@ export function generatePlan(citySeed) {
const dMarket = addDistrict('market', eastEdge - marketW / 2, (zLo + zHi) / 2);
const poly = [[eastEdge, zLo], [eastEdge, zHi], [eastEdge - marketW, zHi], [eastEdge - marketW, zLo]];
const block = addBlock(dMarket, 'market', poly);
// Facades face +x (EAST, toward the spine — the market sits west of it). ry is the OUTWARD normal
// (street→lot); a west-of-street lot's outward normal is x, so ry = atan2(1, 0) (= −π/2). The
// facade then faces (outward) = +x, matching marchStrip's convention. frontEdge is the spine
// segment each lot actually fronts by z-band: stalls (z≈100..23) front segment 2; dept (z=55) → 3.
const ryEast = Math.atan2(-1, 0);
// Facades face +x (EAST, toward the spine — the market sits west of it). ROUND27 THE FACADE FIX: ry now
// aims the VISIBLE facade (local +Z, B's canon) at the street, so a west-of-street lot wants +Z = +x ⇒
// ry = atan2(+1, 0) (= +π/2). It was atan2(1, 0), which pointed +Z at x — away from the spine, into
// the market's back. Same one-sign error as marchStrip above, and the old comment described the
// intended result ("the facade then faces (outward) = +x") that the code did not produce.
// frontEdge is the spine segment each lot actually fronts by z-band: stalls (z≈100..23) front
// segment 2; dept (z=55) → 3.
const ryEast = Math.atan2(1, 0);
const stallEdge = spineEdges[2]; // spine segment spanning z∈[133,0] — the stalls' band
const deptEdge = spineEdges[3]; // spine segment spanning z∈[0,133] — the dept's band
const COLS = 5, ROWS = 8, pitch = 11, sSize = 4.5; // tidy rows in the southern ~2/3 of the square

View File

@ -12,7 +12,7 @@
// A future pass can honour exact projected positions; this lands a booting, invariants-green seam.
import { rng, seedFor, mulberry32, pick, frange } from '../core/prng.js';
import { SHOP_TYPES } from '../core/registry.js';
import { SHOP_TYPES, roadWidth } from '../core/registry.js';
import { OSM_TOWNS, DEFAULT_TOWN } from './osm_fixture.js';
import { lotCorners, obbOverlap } from './plan.js'; // shared geometry — overlap-resolve real-road lots
@ -327,7 +327,11 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) {
const half = AV_W / 2, sd = DEPTH + 2;
const block = addBlock(district, 'mainstreet',
[[SPINE_CLEAR, zPos - half], [avLen, zPos - half], [avLen, zPos - half - sd], [SPINE_CLEAR, zPos - half - sd]]);
const ry = Math.atan2(0, -1); // facade faces +z (north) to the avenue
// ROUND27 THE FACADE FIX: the lots sit SOUTH of the avenue (z = zPos half DEPTH/2), so the
// facade must face +z (north) at it. `atan2(0,-1)` (= π) pointed local +Z at z — due south, away.
// The comment was right; the code was 180° off. Measured, not assumed: the marched path was NOT
// clean (the ruling asked me to check rather than trust that nobody had noticed).
const ry = Math.atan2(0, 1); // facade (+Z) faces +z (north) to the avenue
let t = SPINE_CLEAR;
rowShops.forEach((p, j) => { const f = fr[j]; addShop(addLot(block, t + f / 2, zPos - (half + DEPTH / 2), f, DEPTH, ry, avEdge, 'shop'), p.s); t += f + GAP; });
});
@ -347,6 +351,7 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) {
// shopfronts, not detached boxes. Drops 7.9% → 5.5% across the five; the residual stays COUNTED, and
// dropping a crowded shop beats teleporting it onto a street it isn't on.
const EPS = 6, SNAP = 3, KERB = 4, NODE_CLEAR = 3, RGAP = 0.5; // fidelity + geometry knobs (metres)
const FOOTPATH = 1.5; // ROUND27: kerb→shopfront. KERB alone is centreline-relative and sits INSIDE a main carriageway.
const JOIN_TOL = 12, BRIDGE_MAX = 200, SEAT_MAX = 200; // ROUND19 fragmentation ruling (metres)
const WKIND = { main: SPINE_W, side: AV_W, lane: 8, arcade: 6 };
// every dropped shop is counted (Fable's ROUND19 ruling) — and counted BY CAUSE, so the density
@ -440,8 +445,18 @@ export function generatePlanOSM(citySeed, town = DEFAULT_TOWN, opts = {}) {
for (const { e, side, list } of perEdge.values()) {
list.sort((p, q) => p.t - q.t || p.s.id - q.s.id);
const dx = e.b.x - e.a.x, dz = e.b.z - e.a.z, len = Math.hypot(dx, dz) || 1, ux = dx / len, uz = dz / len;
const nx = side * -uz, nz = side * ux, ry = Math.atan2(nx, nz); // outward normal on the shop's real side; facade (Z) faces road
const off = KERB, dep = DEPTH;
// ROUND27 THE FACADE FIX (B's proven one-liner — B measured katoomba 0/72 → 72/72 and reverted it
// because this is Lane A's file). (nx,nz) is the OUTWARD normal on the shop's real side; the facade
// (local +Z, B's canon) must face back down it, at the road — so atan2(nx, nz), not atan2(nx, nz).
const nx = side * -uz, nz = side * ux, ry = Math.atan2(-nx, -nz); // outward normal on the shop's real side; facade (+Z) faces road
// ROUND27, exposed BY the facade fix: `KERB` (4 m) is measured from the CENTRELINE, but a `main`
// street's carriageway is 10 m wide — its kerb is at 5 m. So every main-street building has stood
// 1 m INSIDE the road since R18. It was invisible while the facade faced away (nothing was drawn or
// asserted on that side); the moment the shopfront turned around, its poster landed in the traffic
// lane (1.07 m). The building never moved — only which face is the front. Seat blocks behind the
// kerb per A's own corridor law (`roadWidth/2` IS the kerb), plus a footpath to stand on.
const pe = edges.find(x => x.id === e.id);
const off = Math.max(KERB, roadWidth(pe) / 2 + FOOTPATH), dep = DEPTH;
const block = addBlock(district, 'mainstreet', [
[e.a.x + nx * off, e.a.z + nz * off], [e.b.x + nx * off, e.b.z + nz * off],
[e.b.x + nx * (off + dep), e.b.z + nz * (off + dep)], [e.a.x + nx * (off + dep), e.a.z + nz * (off + dep)]]);

View File

@ -25,8 +25,23 @@ const section = s => console.log('\n' + s);
const SEEDS = [20261990, 1, 42, 777, 2000000000, 8675309];
const SOLID = new Set(['shop', 'anchor', 'house', 'stall']); // lots realised as building shells by Lane B
// A lot's outward facade normal from its ry (GLB faces -Z at ry=0 ⇒ world facing = (-sin ry,-cos ry)).
const facing = l => [-Math.sin(l.ry), -Math.cos(l.ry)];
// ROUND27 THE FACADE FIX — B's canon is canonical: the VISIBLE facade is local +Z ⇒ world (sin ry, cos ry).
// This helper used to return (sin, cos), the SIM/rig convention that CITY_SPEC:71 mis-wrote into the lots
// contract. That is why nine rounds of goldens were green over a world where every shopfront faced away.
const facing = l => [Math.sin(l.ry), Math.cos(l.ry)];
// THE CROSS-CONVENTION GATE (ROUND27 wave 5 — the lesson made law).
// F proved the trap: measuring lot.ry against CITY_SPEC:71's own convention is SELF-AGREEMENT — it returned
// 72/72 "facing" over a town of blank walls, because a self-consistent check over a self-contradicting spec
// passes under either convention. The only check that can see the contradiction compares the two conventions
// AGAINST EACH OTHER. So this does not restate a spec line: it replicates LANE B's OWN placement maths from
// buildings.js (`toWorld(lot, 0, y, zf)` with `zf = d/2 + 0.02`) and asks a physical question the prose
// cannot fudge — does the quad the RENDERER actually draws land nearer the street than the lot centre?
// If either side drifts (A's ry sign, or B's zf sign), this fires.
const facadeQuadWorld = l => { // buildings.js: toWorld(lot, 0, y, d/2 + 0.02)
const cos = Math.cos(l.ry || 0), sin = Math.sin(l.ry || 0), zf = l.d / 2 + 0.02;
return [l.x + zf * sin, l.z + zf * cos];
};
// nearest point on segment AB to point P
function nearestOnSeg(px, pz, ax, az, bx, bz) {
const dx = bx - ax, dz = bz - az, L2 = dx * dx + dz * dz || 1;
@ -70,13 +85,24 @@ function structuralSuite(plan, label) {
ok(Object.values(lotShopCounts).every(c => c === 1), `${label}: no lot has two shops`);
ok(plan.shops.every(sh => SOLID.has(plan.lots[sh.lot].use)), `${label}: every shop sits on a solid lot`);
let backwards = null;
let backwards = null, drawnAway = null, worstGain = 0;
for (const l of plan.lots) {
const e = plan.streets.edges.find(x => x.id === l.frontEdge); const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue;
const [nx, nz] = nearestOnSeg(l.x, l.z, a.x, a.z, b.x, b.z); const [fx, fz] = facing(l);
if ((nx - l.x) * fx + (nz - l.z) * fz < -0.01) { backwards = l.id; break; }
if ((nx - l.x) * fx + (nz - l.z) * fz < -0.01) { if (backwards === null) backwards = l.id; }
// THE CROSS-CONVENTION GATE: not "does ry match the spec's wording" (self-agreement — that is exactly
// what stayed green over blank walls for nine rounds), but "does the quad LANE B ACTUALLY DRAWS land
// nearer the street than the lot centre?". Physical, falsifiable, and blind to both spec lines.
const [qx, qz] = facadeQuadWorld(l);
const dCentre = Math.hypot(nx - l.x, nz - l.z);
const dFacade = Math.hypot(nx - qx, nz - qz);
if (dFacade >= dCentre) { if (drawnAway === null) drawnAway = l.id; }
worstGain = Math.max(worstGain, dFacade - dCentre);
}
ok(backwards === null, `${label}: every lot faces its frontEdge` + (backwards !== null ? ` (lot ${backwards} backwards)` : ''));
ok(drawnAway === null, `${label}: the RENDERED facade (B's +Z quad, buildings.js maths) faces the street` +
(drawnAway !== null ? ` — lot ${drawnAway}'s shopfront is drawn on the far side (worst: ${worstGain.toFixed(2)} m FARTHER from the road than the lot centre)` : ''));
const byBlock = {};
for (const l of plan.lots) (byBlock[l.block] ||= []).push(l);
@ -238,7 +264,7 @@ for (const s of SEEDS) {
// ── 1b. golden fingerprint: guards against silent output DRIFT across code revisions ────
// (If you intentionally change generator output, run selfcheck, copy the printed hash here.)
section('golden fingerprint (output-drift guard)');
const GOLDEN = { seed: 20261990, hash: 0x3fa36874 };
const GOLDEN = { seed: 20261990, hash: 0x5f76e76 };
{
const hash = xmur3(JSON.stringify(generatePlan(GOLDEN.seed)))() >>> 0;
ok(hash === GOLDEN.hash, `seed ${GOLDEN.seed}: fingerprint 0x${hash.toString(16)} matches golden 0x${(GOLDEN.hash >>> 0).toString(16)}`);
@ -274,7 +300,7 @@ for (const s of SEEDS) {
// (round 8) Real-data towns get the same coverage as synthetic; only the synthetic-brief checks are
// skipped. Goldens are pinned per town; the synthetic golden above stays 0x3fa36874.
section('OSM plan source parity (fixture-driven, zero-network)');
const OSM_GOLDENS = { melbourne: 0x34cfdec0, katoomba: 0x0f652510, silverton: 0xd4b351c9 };
const OSM_GOLDENS = { melbourne: 0x9c7e76b3, katoomba: 0xf3aafec8, silverton: 0x6d74c4 };
const OSM_SEEDS = [20261990, 1, 42, 777];
for (const town of osmTownKeys()) {
const report = {};
@ -310,7 +336,7 @@ ok(JSON.stringify(generatePlanFor(42, 'synthetic', { gigs: true })) === JSON.str
ok(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true, customBands: custom })) === JSON.stringify(pc), `custom bands: deterministic given (seed, list)`);
}
// gig-layer golden (guards gig-layer output drift; pure-generator path, no custom file)
const GIG_GOLDEN = 0xb1d48ea1; // ROUND16 re-pin: pickVenues prefers band_room/rsl lots whose +Z facade clears crossing kerbs (ledger #6). Was 0x4f4a549d (R15 +Z flip), 0x1f636349 (R13 district), 0xa6ae5a5e (R12 alpha).
const GIG_GOLDEN = 0xec7a2d39; // ROUND16 re-pin: pickVenues prefers band_room/rsl lots whose +Z facade clears crossing kerbs (ledger #6). Was 0x4f4a549d (R15 +Z flip), 0x1f636349 (R13 district), 0xa6ae5a5e (R12 alpha).
{
const hash = xmur3(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true })))() >>> 0;
if (GIG_GOLDEN === 0) console.log(` ⚠ gig golden UNPINNED → set GIG_GOLDEN = 0x${hash.toString(16).padStart(8, '0')}`);
@ -571,34 +597,34 @@ const TOWNS_DIR = join(HERE, '..', '..', 'assets', 'towns');
// the fragmentation ruling (junction-protected DP + cull/bridge) + E's spine-poster fix. Amendment law:
// the alpha changed nothing outside the cache-schema path; synthetic/fixtures/classic/default stay frozen.
const REAL_TOWN_GOLDENS = {
adelaide_real: 0x5c790920,
adelaide_real: 0x4b9137a5,
ballarat_real: 0xf0a9e8a1,
bendigo_real: 0xc175194e,
bowral_real: 0xb23ee067,
braddon_real: 0x2f388775,
brunswick_real: 0x7ecb5c3e,
castlemaine_real: 0x26c128c9,
darwin_real: 0xcfb1917f,
daylesford_real: 0x4ec7352c,
fitzroy_real: 0x6549f61b,
fremantle_real: 0x73d385df,
geelong_real: 0x2f65591e,
glebe_real: 0x908e1835,
hobart_real: 0xa96328b9,
katoomba_real: 0x420f677d,
launceston_real: 0x32b1ab7b,
marrickville_real: 0x60148e37,
newcastle_real: 0xbb2c3050,
newtown_godverse: 0xcf69b387,
newtown_real: 0x741c18ec,
northbridge_real: 0x1a645848,
bendigo_real: 0x44f5ef2d,
bowral_real: 0x5574436b,
braddon_real: 0xf03da36c,
brunswick_real: 0xb548149e,
castlemaine_real: 0xf3118387,
darwin_real: 0xd45d7042,
daylesford_real: 0x96a92068,
fitzroy_real: 0x1cb08059,
fremantle_real: 0x17954f0a,
geelong_real: 0xf90f4463,
glebe_real: 0xa9418848,
hobart_real: 0xbcee1ccb,
katoomba_real: 0x126a66cb,
launceston_real: 0xa2bb4e71,
marrickville_real: 0x8c8d39aa,
newcastle_real: 0x61de3375,
newtown_godverse: 0x1e8d49b9,
newtown_real: 0x64a98d7e,
northbridge_real: 0xb0c24e12,
// toowoomba_real RETIRED by E in R23 (cache deleted) — pin removed with it. A golden for a town that
// cannot load is dead config: it can never fire, so it can never fail. Same species as the round's
// vacuous-gate law, one layer down. (E tried the re-bbox first and it GAMED my spacing metric — median
// NN 395.7 → 89.4 m "passes" while hub density fell 3/12 → 2/12, the pack's worst. Right call.)
redhill_godverse: 0xb2224939,
redhill_real: 0x6046700f,
westend_real: 0x5b1e5ffe,
redhill_godverse: 0x40d30a31,
redhill_real: 0x5b851696,
westend_real: 0x6032d73a,
};
// ROUND20: the GIG-layer golden per real town (district + capped posters) — the base golden above is
// gigs-off so it never captured the poster cap. Pinned here so the ROUND20 poster cap (and the density
@ -606,31 +632,31 @@ const REAL_TOWN_GOLDENS = {
// ROUND21: re-pinned for the venue cluster-adjacency bias (D's relocation finding). Only the GIG goldens
// move — venue selection is gig-layer, so the base town goldens above are untouched.
const REAL_TOWN_GIG_GOLDENS = {
adelaide_real: 0x24ad3c27,
adelaide_real: 0xda729bae,
ballarat_real: 0xf097c331,
bendigo_real: 0x646b2c23,
bowral_real: 0xac809f0b,
braddon_real: 0xc3e5cfc0,
brunswick_real: 0x8c08914e,
castlemaine_real: 0xf9713a69,
darwin_real: 0xbc5605b5,
daylesford_real: 0x2fb34c8a,
fitzroy_real: 0x690e5ef9,
fremantle_real: 0xcf3426fc,
geelong_real: 0x6f47ec2f,
glebe_real: 0x042f01fb,
hobart_real: 0x22f80d7b,
katoomba_real: 0xbf586b77,
launceston_real: 0xeaf7ff4d,
marrickville_real: 0xea21be51,
newcastle_real: 0x66fc2bf9,
newtown_godverse: 0x1e266f60,
newtown_real: 0xacdb3eee,
northbridge_real: 0x83a1d5be,
bendigo_real: 0xe3e8d892,
bowral_real: 0x3f37bbcd,
braddon_real: 0xbc51aed2,
brunswick_real: 0xc0a90188,
castlemaine_real: 0x3c3cf400,
darwin_real: 0xd37b37cd,
daylesford_real: 0x7b24c5c5,
fitzroy_real: 0xcd31b1ab,
fremantle_real: 0xc6449dc,
geelong_real: 0xcd0ceac4,
glebe_real: 0x8d2b12ca,
hobart_real: 0xda3211c9,
katoomba_real: 0x5eece2d4,
launceston_real: 0xb619b65b,
marrickville_real: 0x5dc00b5f,
newcastle_real: 0xfc3cb144,
newtown_godverse: 0x4153dee2,
newtown_real: 0xb6a75bb1,
northbridge_real: 0x49fe0f2b,
// toowoomba_real RETIRED by E in R23 — see the note on the base map above.
redhill_godverse: 0xc6f80e18,
redhill_real: 0x673785ee,
westend_real: 0xb2c7ddb6,
redhill_godverse: 0xb53f695d,
redhill_real: 0x21db6d1,
westend_real: 0xac9e69ea,
};
// `index.json` is E's towns INDEX (key/town/state/shops/roads for B's selector), not a town cache — skip it.
const townFiles = (existsSync(TOWNS_DIR) ? readdirSync(TOWNS_DIR) : []).filter(f => f.endsWith('.json') && f !== 'index.json');