PROCITY/docs/LANES/LANE_E_NOTES.md
m3ultra 640bdbb55d Lane E R28 (v5.1): Spike 1 — look.glb shipped; lean does not exist (ledger #1)
look.glb — DONE, published, verified. Looking_Around.fbx (675 KB) from the CACHED tailnet stash (not
re-downloaded — the asset-pipeline skill's law) -> fbx_to_clip.py -> web/models/peds/look.glb: mesh-free,
65 mixamorig joints, 195 tracks, inverseBindMatrices present, 136 KB. Structurally identical to the
sit.glb precedent on every measurable field (meshes/nodes/skins/joints/bind), so D's _canon binds it
exactly like walk/idle/sit. Depot: published to digalot.fyi/3god under John's R22 standing authorization
for Lane E assets (_published.json 43 -> 44), verified by fetching the bytes BACK from the public depot:
https://digalot.fyi/3god/a/look.glb -> HTTP 200, 139,640 bytes, sha1 identical to local.
-> D: it's ready. Gate it behind an opt the way rigs.js gates sit (loadPedFleet(base, { sit })) — a raw
fetch would breach the ?classic=1 zero-fetch-delta covenant, same as sit.glb.

`lean` DOES NOT EXIST — the spike's premise is half wrong. The brief and kickoff both say "two of the 34
already-cached clips (lean, look-around)" and "Spike 1 needs nothing from anyone". look-around is cached;
lean is not, anywhere. Measured, not assumed: the stash's 34 clips contain no lean; `find ~/Documents
-iname "*lean*"` on ultra returns nothing (not mixamo-fetch, not character_kit, not 3D=models, no FBX by
that name at any depth); and the fetch tool cannot help unattended — `node fetch.cjs search "lean"` ->
"FATAL nothing on debug port 9222 — quit Brave and relaunch with --remote-debugging-port=9222". It needs
John's logged-in Brave session (his browser, his Mixamo account); I won't relaunch it.
I did NOT substitute a lookalike. `Bored` is the nearest stash clip and it is a standing weight-shift idle,
not a wall-lean — D's half explicitly needs shopfront adjacency ("lean against a wall wants the same care"),
so a standing idle would quietly change what Spike 1 proves. Half a spike, honestly, beats a whole one that
lies. -> John: one small unblocker — relaunch Brave with --remote-debugging-port=9222 and open a Mixamo
tab; the fetch+convert is minutes once the token exists.

Instrument LOD — D killed it and D's measurement is better than mine. I had built the five _lo variants in
scratch (14k -> 3500/3500/4000/6000/3000) when D's commit landed saying "E: STOP — do not build the _lo
variants". Nothing shipped; scratch discarded. D measured the rooms an LOD must fire in: max diagonal 15.2m
in the largest venue, so the player can never be far enough — practical win 0 tris. My own reading reached
the same verdict by another route, recorded as corroboration: C's §4 already says "the interior gate is
DRAWS; tris are the STREET budget (200k)", and an LOD swap does not change draws at all (same object, same
material, fewer tris) — the item's entire win landed on a metric no interior gate reads. Two independent
measurements, one verdict. Salvage if it ever returns: at ~3k the guitar_amp SHREDS (white gaps punched
through its silhouette — confirmed against the shipped 14k thumb as a decimation artifact, not a source
defect, per the R22 food_cart discipline); it needs 6k to read clean. The other four were clean at 3-4k.

-> A: your own file's comment now contradicts its code. Chasing why the fingerprint read 0x5f76e76 rather
than the 0x3fa36874 I'd seen every round, I confirmed it is LEGITIMATE — your R27 w5 facade fix re-pinned
51 goldens under the amended covenant. But selfcheck.js:267 pins hash: 0x5f76e76 while selfcheck.js:301
still says "the synthetic golden above stays 0x3fa36874". Code and comment disagree by 34 lines — the exact
species as R24's validator bug (my docstring said nested, my code read top-level); the next reader trusts
whichever they read first. Your file, your call; flagging per the round's "flag loudly if anything
disagrees".

Gates: selfcheck ALL GREEN 156,352/156,352 · qa.sh --strict 6 passed / 0 failed. look.glb moves NO golden —
proved by moving it aside and re-running: the fingerprint is identical either way, because an unreferenced
clip is never fetched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:35:17 +10:00

1174 lines
91 KiB
Markdown
Raw Permalink 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 E — cross-lane notes
## Round 28 — Spike 1: half of it shipped, half of it doesn't exist ⟨v5.1⟩
**1. `look.glb` — DONE, published, verified.** `Looking_Around.fbx` (675 KB, from the cached tailnet stash,
not re-downloaded — the skill's law) → `fbx_to_clip.py`**`web/models/peds/look.glb`**: mesh-free, **65
`mixamorig` joints, 195 tracks, inverseBindMatrices present, 136 KB** — structurally identical to the
`sit.glb` precedent on every field I can measure (`meshes/nodes/skins/joints/bind`), so D's `_canon` binds
it exactly like walk/idle/sit. Depot: published to `digalot.fyi/3god` under John's **R22 standing
authorization for Lane E assets** (`_published.json` 43 → 44). **→ D: it's ready.**
**2. 🚩 `lean` DOES NOT EXIST — the spike's premise is half wrong, and only John can unblock it.** The
brief and the kickoff both say "two of the 34 already-cached clips (lean, look-around)" and "Spike 1 needs
nothing from anyone". **`look-around` is cached. `lean` is not — anywhere.** Measured, not assumed:
- The stash's 34 clips contain no lean (`Agreeing · Bored · Breathing_Idle · Counting · … · Looking_Around
· … · Yawn`).
- `find ~/Documents -iname "*lean*"` on ultra: **nothing** — not in `mixamo-fetch`, not in `character_kit`,
not in `3D=models`, no FBX by that name at any depth.
- The fetch tool can't help unattended: `node fetch.cjs search "lean"`
`FATAL nothing on debug port 9222 — quit Brave and relaunch with --remote-debugging-port=9222`. It needs
**John's logged-in Brave session** (his browser, his Mixamo account) — **I won't relaunch it.**
**→ John, one small unblocker (adds to your two):** relaunch Brave with `--remote-debugging-port=9222`,
open a Mixamo tab, and either run `node fetch.cjs search "lean"` yourself or tell me and I'll fetch +
convert it in minutes (the path is proven twice now). **I did NOT substitute a lookalike.** `Bored` is the
nearest stash clip and it's a standing weight-shift idle, not a wall-lean — D's half explicitly needs
shopfront adjacency ("`lean` against a wall wants the same care" as the scale-aware traps), so a standing
idle would quietly change what Spike 1 proves. Half a spike, honestly, beats a whole one that lies.
**3. Instrument LOD — D killed it, and D's measurement is better than mine.** I'd built the five `_lo`
variants in scratch (14k → 3500/3500/4000/**6000**/3000) before D's commit landed saying *"E: STOP — do not
build the _lo variants."* **Nothing shipped**; scratch discarded. D measured the rooms an LOD would have to
fire in — **max diagonal 15.2 m in the largest venue, so the player can never be far enough: practical win
0 tris.** That's decisive in a way my argument wasn't. My own reading agreed on the verdict by a different
route, and it's worth recording as corroboration: **C's §4 already says "the interior gate is *draws*; tris
are the *street* budget (200k)"** — and an LOD swap doesn't change draws *at all* (same object, same
material, fewer tris). So the item's entire win landed on a metric no interior gate reads. Two independent
measurements, one verdict. **The oldest tri item is dead, correctly.**
*(One salvage worth keeping if it ever returns: at the brief's ~3k the **guitar_amp shreds** — white gaps
punched through its silhouette, confirmed against the shipped 14k thumb as a decimation artifact and not a
source defect, the R22 `food_cart` discipline. It needs **6k** to read clean. The other four were clean at
34k.)*
**4. 🚩 → A: your own file's comment now contradicts its code.** Investigating why the fingerprint read
`0x5f76e76` instead of the `0x3fa36874` I'd seen every round, I confirmed it's **legitimate** — your R27 w5
facade fix re-pinned 51 goldens under the amended covenant. But `selfcheck.js:267` pins
`hash: 0x5f76e76` while **`selfcheck.js:301` still says "the synthetic golden above stays 0x3fa36874"**.
Code and comment disagree by 34 lines. That's the exact species as R24's validator bug (my docstring said
nested, my code read top-level) — the next reader trusts whichever they read first. Your file, your call;
flagging per the round's "flag loudly if anything disagrees".
**5. Gates.** selfcheck **ALL GREEN 156,352/156,352** · `qa.sh --strict` **6 passed / 0 failed**. `look.glb`
moves **no golden** — proved by moving it aside and re-running: fingerprint identical either way, because an
unreferenced clip is never fetched. The `?classic=1` covenant is untouched: like `sit.glb`, a raw fetch
would breach the zero-fetch-delta rule, so **D must gate `look` behind an opt** the way `rigs.js` gates
`sit` (`loadPedFleet(base, { sit })` → the same shape for `look`). Depot-live verified by fetching the
bytes back: `https://digalot.fyi/3god/a/look.glb`**HTTP 200, 139,640 bytes, sha1 identical to local**.
## Round 27 — provenance v5-final, and the roster gap the docs freeze caught ⟨v5.0⟩
**1. THE BUG THE FREEZE FOUND: `redhill_godverse` was on disk but NOT in the roster — for three rounds.**
The v5 provenance table needed real numbers, so I measured instead of carrying the v4 row forward. The
count didn't match: **23 caches on disk, 22 rostered.** The missing one was **`redhill_godverse` — the
town the entire v5 epoch walks to.**
**Mine, squarely.** `index.json` is Lane E's file (`write_index()`). G's `redhill_godverse` landed in R24
(`86e2985`) *after* my last index run (`5815dfb`), and nobody re-rostered it. **An addition isn't done when
the cache is written — it's done when the roster is regenerated.** That's the exact mirror of my own R24
lesson (*"a retirement isn't done when the cache is deleted; it's done when the consumers are swept"*) —
I wrote the lesson for deletions and then missed its reflection for additions.
**Why four green rounds never caught it — worth reading, because it's a gate-shape lesson.** Nothing was
broken *for the gates*: `selfcheck` enumerates `web/assets/towns/*.json` **directly** (so it saw the town
and A even pinned its goldens), and F's crate smoke boots **by key** (`?town=redhill_godverse`). The
roster's *only* consumer is **B's HUD selector** (`hud.js`, my R20 ledger #2 design). So the single symptom
was that **a player could not choose the headline town from the dropdown** — invisible to every automated
check by construction. A gate that reads around the artifact under test cannot test it: the same species as
the vacuous-gate law, one layer out. **Fixed:** re-ran `write_index()` → **23 rostered, 23 on disk, 0
missing**; only `index.json` moved (every cache byte-identical again); selfcheck **ALL GREEN 156,212**,
golden `0x3fa36874` untouched; `qa.sh --strict --matrix` **7/0/0**.
**→ F: this matters for #5, the tour.** The money shot walks Musgrave Road into Monster Robot Party. If any
tour frame reaches that town **through B's selector** rather than by key, it could not have found it before
this commit — by key it always worked. Worth one glance at your framing path.
**→ B:** the dropdown now carries both godverse towns. Your `${t.state ? ', ' + t.state : ''}` handles their
empty `state` gracefully — no change needed, and the defensive write is why this cost nothing cosmetically.
**→ G:** your godverse caches carry no `state`, so they roster as `state:''`. Harmless today (B degrades
cleanly). If you want "Red Hill, QLD" in the picker, emit `state` in the cache — my `write_index()` already
prefers the cache's own field, so it needs nothing from me.
**2. Provenance v5-final (ledger #6).** The ⟨v5.0-FINAL⟩ rows are measured, not inherited: the town pack
row was still claiming **v4's "22 towns / 1192 shops"** — stale since my R24 ballarat retirement and redhill
build (now **21 real / 1196 / 299 heroes**, plus both godverse towns = 23 rostered). Added the two v5 art
rows: the **real-stock atlas** (1 crate, 120 items, Monster Robot's own POS) and the **mint atlases** (14
crates, 224 items, seeded from real dealgod listings). **These are the table's first 🟡 rows** — everything
before v5 was 🟢 web-ok; real product photography is the first asset class this project ships that a human
must clear before public release.
**3. The licence line's story, written down as a mechanism (ledger #6).** The 🟡 rows are guarded by exactly
one printed line, so the freeze records why it took two rounds to make honest: R23's glob matched 0 files
and returned 0; R25's **permissive `or` fallback let the print go silent while the check still passed**
the flag stopped reaching a human on real product photos while the gate said OK. Now: one spelling, one
lookup with one owner, and the line is **counted** (judged N ⇒ N lines, or FAIL). **Both zones print**
mint carries the same in-house-green flag, because minting changes whose stock a crate *claims* to be,
never whose photograph it *is*. Frozen today at **15 crates, 15 flags, every one in front of a human.**
## Round 26 wave 4 — the mint-id fence: my check was faithful, the rule was stale ⟨v5.0-beta⟩
**The red was mine and the fix is C's ruling.** My R25 slot-id check demanded `sku_` on every real-stock
pack — faithfully implementing C's R25 line, which was **written before mint existed**. It then failed all
14 of G's mint crates, whose ids honour its every intent: non-positional, seeded, stable, zero reuse. C's
ruling names it exactly: *"the rule was the stale thing, not E's check."* Fourth hold of the epoch, and
the fourth to trace to **a rule meeting a case written before the case existed.**
**The fix is a fence, and it is STRONGER than the rule it replaces — that was the thing to prove.** The
easy green was to drop the prefix check. Instead the form is now sourcing-scoped per C §7.2a (R26 w4):
`sourcing:"real"``sku_` (a POS copy on a shelf) · `sourcing:"mint"``mint_` (a dealgod listing
standing in for one). **A crate wearing the other's prefix FAILS.**
**Why this is a money bug and not a naming bug — G's judgment, now law.** G refused to file dealgod listing
ids under `sku_` to go green the easy way. They were right: **tier-2's sold-means-gone looks up POS skus.**
R24's id collision entered the *wrong shop*; the same collision in the money layer **sells a real record
out of a real shop to satisfy a minted stand-in.** The gate's error says that, in those words — the cost,
not the rule.
**Proved 7/7 — that the fence bites, not merely that the red stopped.** G's real crate (`sku_…`) → PASS ·
G's mint crate (`mint_…`) → PASS · **mint filed under `sku_` (the exact shortcut G refused) → FAIL**, naming
the fence and the real cost · real wearing `mint_` (the reverse) → FAIL · **positional ids still FAIL in
BOTH sourcings** (the old rule's whole intent, retained) · duplicate ids → FAIL. Loosening would have
passed the first two and lost the last four; this passes the first two and keeps all four.
**Both older suites brought forward rather than left to rot.** The R26 suite's mint fixture wore `sku_`
ids, so the fence correctly rejected it — a fixture that fails for a *new* reason has stopped testing what
it was written for. Moved to the `mint_` space: **17/17**. R25 licence suite: **8/8**, tally still firing
**via the tally**. This is the third round running where the near-miss was a test going quietly stale
around a changing contract; it is the failure mode I now look for first.
**Gates:** `atlas-QA OK — 15 per-shop atlas(es) valid, 15 licence line(s) shown, 0 warning(s)` (1 real +
14 mint, every one showing its flag to a human) · `validate_manifest` exit 0 · **`qa.sh --strict --matrix`
7 passed / 0 failed / 0 warn — QA GREEN.** F's 6/1/0 is now 7/0/0. **→ F: the beta is yours to tag.**
## Round 26 — the sourcing arm + the atlas manifest ⟨v5.0-beta⟩
**1. The field is `sourcing`, NOT `tier` — and reading C's contract instead of the brief is the only
reason this gate isn't already broken (ledger #3).** The R26 brief says *"the validator grows the tier
arm… `tier` required on every atlas"*. **C ruled it cannot be `tier`** (§7.2b): `tier` is already the
charter's ladder rung (0 parody / 1 static-real / 2 live) and **a mint atlas is also rung 1** — same
mechanism, different data origin. Overloading it would put two meanings in one name: the same species as
the plan-id/godverse-id collision and the positional ids. Had I built the brief's word, my gate would have
demanded `tier` while G emitted `sourcing`**R23 exactly, re-run**. The contract's author is the
authority for the contract's shape. **Built to C §7.2b.**
**2. The honesty separation, asserted both directions.** `sourcing ∈ {real, mint}` required (vacuous-gate
law: no `sourcing` ⇒ FAIL — a crate that can't say where it came from must never pass as one that can).
Then: **mint must carry no POS claim, real must carry the POS provenance it claims.**
**→ C: §7.2b says "a mint index MUST NOT carry POS-snapshot fields" but does not enumerate them, so I
named the set — please correct it if it's wrong.** I assert on `crate`, `provenance.source.pos_db`,
`provenance.source.pos_dump` — the fields that claim *a specific shop's actual inventory*. I deliberately
**excluded `snapshot`**: §7.3 requires it on every atlas as the determinism pin, and a mint bake has its
own honest source snapshot (the dealgod dump it sampled). If a mint bake legitimately needs any field in
my set, say so and the set changes. The error names the exact field found, so a divergence shows up as a
diagnosis rather than a mystery.
**3. The manifest (ledger #6), consistency both ways + one hole C's rule implies but doesn't spell out.**
`stock_godverse/index.json` per §7.2d. An atlas on disk absent from the manifest FAILS (no entry ⇒ no
fetch ⇒ that crate silently never loads — an unreachable atlas is not a passing atlas); a manifest entry
with no atlas FAILS (the wire fetches it and 404s — the exact noise the manifest exists to kill). C's
"absent manifest ⇒ legitimate zero-shop state" is honoured: **no atlases + no manifest ⇒ SKIP, not an
error.** But **a manifest that PROMISES shops while zero atlases exist now FAILS** rather than skipping —
C's rule scopes "absent" to the empty tree, and a tree where something on disk still asserts atlases exist
is the vacuous shape wearing a hat. Flagging it since it's my reading, not C's words.
**4. Proved before handing G the gate — 17/17, including a mint crate that does not exist yet.** The R25
rule (never hand a lane a gate that fails on compliance) applied: I hand-built G's likely R26 emit and a
seeded mint index and ran them. Real → PASS · **mint → PASS with its licence line printed** (ledger #3's
"prints for both" — verified, not assumed) · real+mint side by side → PASS · mint carrying `crate` → FAIL ·
mint carrying `pos_db` → FAIL · **real carrying no POS provenance → FAIL** (the reverse direction) · no
`sourcing` → FAIL · `sourcing:'realish'` → FAIL · atlas absent from manifest → FAIL · manifest entry with
no atlas → FAIL · declared type not on disk → FAIL · **manifest says mint, index says real → FAIL** ·
manifest missing while atlases exist → FAIL · unparseable manifest → FAIL · empty tree → SKIP · empty
manifest → SKIP · manifest promising a shop with no atlases → FAIL.
**5. The R25 suite was going stale and I brought it forward rather than let it rot.** Adding `sourcing` +
the manifest meant every R25 licence case started failing for a NEW reason — which would have left the
licence arm *untested while appearing tested*. Updated to the R26 contract; **8/8 still green, and the
tally tripwire still fires VIA THE TALLY** (`via_tally=True`), not incidentally. Worth noting the near-miss:
the manifest error briefly shadowed the tally in the harness, which is precisely how the tally died the
first time — a test that fails for a new reason has stopped testing what it was written for.
**6. The gate is RED until G lands, by design (2 errors: no `sourcing`, no manifest).** Same as R25: G is
in my wave, parallel. It goes green when G's re-emit + manifest land, with no change from me.
**→ F: one thing before the alpha tag.** There is still **no `v5.0-alpha` tag**, and C's + B's **beta**
commits (`4f0adf1`, `2f4ab59`) are now on `main` ahead of wave 0's close. Tagging `HEAD` would produce a
`v5.0-alpha` whose tree contains beta work your green verdict never covered — the mirror of the reason you
held three times. The alpha's tree is **`e7d58ea`** (wave 0 COMPLETE, my `9b1df5c` beneath it). Your call
and your tag; flagging it because "the tag must certify what was proven" cuts both ways.
## Round 25 — the licence line, made unmissable + the sku form ⟨v5.0-alpha⟩
**1. The bug was not a spelling — it was a DIVERGENCE, and the fallback was the culprit (ledger #2).**
The R24 gate printed `OK — 0 warnings` on a real shop's product photos while saying **nothing** about
their licence. Two paths read one field with different spellings:
```
the CHECK: lic = prov.get("licence") or prov.get("license") ← permissive: passed either spelling
the PRINT: lic = prov.get("licence") ← strict: British only, behind `if lic:`
```
While G's artifact was British they agreed. **G's R24 keyed re-emit moved it to US `license`** — the
check still passed, the print silently resolved to `{}`, and `FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE`
stopped reaching a human. **The `or` fallback WAS the bug**: it made one path permissive so it could not
feel the other path break. This is **my R21 stale-roads lesson one layer up** — *a check applied to one of
two symmetric paths is a trap with a fuse on it* — and I wrote that lesson down and still shipped it
again. Fixed structurally, not by adding the missing spelling:
- **One spelling, no fallback.** British `licence` now ERRORS and names C's §7.3 retirement.
- **One lookup, one owner.** `validate_index` returns the evidence string; `main` prints what it is
handed and never re-looks-up the field. There is no second path left to drift.
- **The evidence line is COUNTED.** Judged N atlases ⇒ N licence lines shown, or FAIL.
**2. My own first fix had the same disease, and my own test passed for the WRONG REASON.** I added the
tally tripwire — then `main()` *also* appended its own missing-licence error, so `all_errs` was never
empty when the tally would fire: **the tally was unreachable dead code.** The test went green through the
other path and reported PASS. That is exactly the trap I recorded in R24 (*"a gate that fails for the
wrong reason is a gate that will pass for the wrong reason later"*) — one round later, on me, in the very
file where I wrote it. The fix: the field check owns the precise error; the tally sits *after* the
`all_errs` return and exists **only** for the day that check goes permissive again. Verified by stubbing
`validate_index` permissive — it now fails **via the tally** (`via_tally=True`), not incidentally.
**Proved, 8/8:** G's re-emit → PASS with the flag shown · `license` gone / empty → FAIL · British at top
level → FAIL · British nested → FAIL · `attribution` / `snapshot` gone → FAIL · **the R24 silent shape →
FAIL** · permissive-check simulation → FAIL via the tally.
**3. C §7.2a — the sku form asserted, and I verified C's claim myself before building to it (ledger #3).**
Independently measured: **120/120 godverse ids also exist in the generic pack** — F and C are exactly
right, and an id-equality gate cannot discriminate while that holds. G's snapshot carries **120/120 unique
skus**, so the re-emit was feasible, and I proved the compliant shape passes **before** handing G the gate
(4/4: `sku_<POS sku>` → PASS · positional → FAIL · sku-but-duplicated → FAIL · one id sneaking back into
`rec_` → FAIL at 1/120, so it can catch a *real* collision, not just an all-or-nothing one).
**→ C: two things I did beyond your literal ask — say if you disagree.**
- Your ask was "id present, unique within the pack, `sku_`-prefixed". I also assert the **actual property
your rule exists to guarantee**: the pack's ids are **disjoint from the shipped generic packs**, read
from `web/assets/models/stock_*_index.json`. The prefix says ids *should* be disjoint; this measures
whether they *are*, and it catches the reverse drift (a generic pack wandering into `sku_`) that a
prefix check never could. If the generic packs are absent it **warns** rather than passing silently.
- I flag British `licence` **anywhere**, including inside the nested `provenance{}` "colour" block. Your
§7.3 retires the spelling and R25 is what a spelling fork costs, so I read the retirement as global. If
you meant the block to stay G's business, say so and I'll scope it to the four gate fields.
**4. The gate was RED by design for one wave, and that was the point.** Between my commit-readiness and
G's re-emit the collision was real and unfixed, so `validate_atlas` failed — deliberately. A gate that
stayed green on a defect F had just found would be the vacuous species wearing a different hat. It went
green the moment G landed `4314622`, with no change from me.
## Round 24 — the alpha completion: redhill, the honest validator, ballarat ⟨v5.0-alpha⟩
**1. `redhill_real` — the crate's real street (ledger #3). BUILT.** One bounded Overpass fetch under the
standing authorization. **Red Hill, Brisbane** · centre `-27.4553, 153.0064` · span 2.4 km · **36 shops
(9 heroes + 27 texture, 29 subsampled) · 884 roads** · median NN 73 m, hub 5/36. **Musgrave Road is in
`roads[]` — 41 ways, kinds `[primary, residential]`.** That was the round's point: Monster Robot Party's
real shop now has its real street. Raw snapshots in `_raw/`, ODbL, C's mapping, same discipline as the
pack. **→ Lane G: this is your donor** for `godverse_town.py redhill_real redhill_godverse`.
**→ G AND A: TWO HAZARDS ON THIS DONOR. Please read before you seat the shop.**
> **(a) The shop is NOT in OSM.** There is no `shop=music` node and no "Monster Robot"/"monsterrobot"
> anywhere in the Red Hill bbox — I checked the raw snapshot, not just the emitted cache. It **cannot**
> arrive via my fetch; **G must inject it from the census** (id `3962749`) into `shops[]`. If G assumes
> the donor already carries it, the town ships without the one shop the release is about.
>
> **(b) THE SEATING HAZARD — from G's geocode, the shop seats on the WRONG STREET.** A's lift seats each
> shop on its **nearest edge**. Measured against the real `roads[]` geometry, from G's geocode
> `-27.4553, 153.0064`:
>
> | distance | street | kind |
> |---|---|---|
> | **38.6 m** | **Zig Zag Street** | residential ← **A would seat it HERE** |
> | 46.1 m | Craig Street | residential |
> | 51.6 m | **Musgrave Road** | primary ← the real address (147 Musgrave Rd) |
>
> Three streets are nearer than the addressed one, so Monster Robot Party lands on Zig Zag Street and the
> round's headline quietly becomes false. A street-address geocode sitting 52 m off its own centreline is
> itself the tell — the pin is roughly the block, not the shopfront.
>
> **The measured fix (E's recommendation, G's call):** the point on the real Musgrave Road centreline
> nearest G's geocode is **`-27.455169, 153.006902`**. Seating there: **0 m to Musgrave Road, next
> candidate 43 m away** — unambiguous, with margin. It moves the shop 51.6 m; the residual error *along*
> the street is G's geocode error and is below the game's resolution. **Caveat, stated plainly:** this
> snaps to the right street, it does not verify the house number — I have no address data to confirm
> where #147 sits along Musgrave Rd.
>
> **The robust fix (A's call, likely v5.x):** a godverse shop knows its addressed street; A's lift could
> prefer a name-matching edge over the merely-nearest one. That fixes the whole class, not this one shop.
> The snap above is enough to ship the alpha honestly.
**2. `validate_atlas` — made honest (ledger #4). F's catch was right; there were FOUR bugs, not one.**
The R23 gate could pass without ever touching its subject — the vacuous-gate law's exact species. Fixed:
| # | bug | why it mattered |
|---|---|---|
| 1 | globbed the **published-contract** name (`stock_shop_<id>_index.json`) — a name the runtime can *never* load | matched **0 files** and returned 0. It passed identically whether atlases were absent, misnamed, or perfect |
| 2 | the **code read provenance top-level** while **my own docstring said nested** | G nested it (correctly). The gate would have failed G's real, correct atlas |
| 3 | `license` vs **`licence`** | same — a spelling fork silently failing a valid artifact |
| 4 | ceiling stale by one round | drifted from C's §7 |
Per the R24 **"the runtime is the authority"** ruling the gate now globs the path the loader fetches:
`stock_godverse/<godverseShopId>/stock_<type>_index.json`, `type ∈ {record, book, toy}`. **The vacuous-gate
law is implemented literally:** 0 matches **+ stray files** ⇒ **FAIL**, listing the unmatched paths; 0
matches **+ a truly empty dir** ⇒ `atlas-QA SKIP: no atlases present (0 files matched glob …)`, exit 0 —
it skips **loudly, with the glob printed**, so a silent pass can't hide. It also asserts identity
(`shop.godverse_id` == the directory name) — a falsifiable id equality, per the law's "prove it touched
its subject".
**Proved, not asserted** — four cases: empty ⇒ SKIP (loud, exit 0) · **misnamed (F's exact bug) ⇒ FAIL**
· unparseable ⇒ FAIL · **G's real atlas ⇒ PASS**: `3962749/stock_record_index.json — Monster Robot Party
— items=120 atlases=2 px=2048 OK`, licence printed, 0 warnings. **My own test found a fifth hole:** the
glob still matched the old `stock_shop_*` name, so the misnamed case failed only *incidentally* — I added
a `STOCK_TYPES` name check, and it now fails **for the right reason**. A gate that fails for the wrong
reason is a gate that will pass for the wrong reason later.
**→ Lane C:** built to the runtime + G's real artifact per the ruling. `LANE_C_PUB §7` **line 271** still
names the contradictory `stock_shop_<godverseShopId>_index.json` — when your corrected contract lands I'll
reconcile; if it names anything other than the above, the gate and the loader disagree again.
**3. Ballarat — RETIRED (my call, counted). A different illness from Toowoomba.** Full rationale in
`SOURCES.md` and the commented config entry. The short version, because the distinction matters:
**Toowoomba was dead everywhere. Ballarat is alive exactly where the game isn't.** It has 103 POIs and a
genuinely dense core (densest 400 m disc = **47** POIs; Toowoomba's was **3**) — but that disc is
cafés/clothes/bakeries, while the op-shops sit 854 m / 1042 m / 1709 m out. Heroes and density don't
coincide, so no bbox holds both:
| span | shops | heroes | median NN | hub ≤160 m | op-shops |
|---|---|---|---|---|---|
| 1.0 km (the live core) | 8 | 2 | 184 m | 4/8 (50%) | **0** ← a thrift town with no thrift shop |
| 2.0 km | 12 | 3 | 186 m | 3/12 (25%) | 1 |
| 3.0 km (as shipped) | 20 | 5 | 255 m | 4/20 (20%) | 2 |
Every row is worse than any shipped town (darwin 68 m, glebe 60 m, redhill 73 m, katoomba 22 m).
**→ A, D: the binding constraint was MINE, not OSM's — and it's a real finding.** With 2 heroes the 3×
texture cap keeps 6 of 47 texture POIs, and **the subsample is spatially blind** (deterministic hash rank
over osmId). So in any hero-poor/texture-rich town the survivors scatter *no matter how tight the box*
**part of the 255 m A's warn caught was my own cap's fingerprint, not Ballarat's.** Seeding texture near
heroes would fix the class, but it moves **every** town's golden → I'm filing it as a **v5.x proposal**,
not doing it in a release round.
**→ A: I OVERSTATED MY R23 ADVICE — please don't encode it as a threshold.** I told you the hub fraction
is the discriminator. It isn't, on its own: **glebe — shipped and healthy — reads 7/52 (13%)**, and
redhill reads 5/36 (14%). A hub threshold would false-flag both. What actually held up:
- **median NN is the robust primary** (dead: toowoomba 396 m, ballarat 255 m; alive: 2277 m).
- **hub is only comparable at equal shop counts** — toowoomba 2/12 vs darwin 7/12 was a fair comparison
*because both had 12 shops*; hub% otherwise scales with town size and morphology.
- The R23 calibration point still stands and still matters: **your `validateTownCache` sees raw cache
lat/lon; D's numbers are seated (post-lift)** — darwin is 27 m seated but **68.3 m raw**, so a threshold
read off D's table will false-warn darwin.
**4. A methodology note I'm recording because it nearly bit me.** I simulated the re-bbox offline to pick
Ballarat's span; the sim predicted **68 m / hub 5-8** and I had already written those numbers into a
config comment as fact. The real build returned **184 m / 4-8***same* counts (2 heroes + 6 texture),
*different sample*: my re-implementation of the hash subsample picked a dense six where the pipeline picks
a spread six. **Never trust a re-implementation of the pipeline's own selection — run the pipeline.** The
sim's numbers would have shipped as a claim about a town nobody would have re-measured.
**5. Determinism receipt.** The ballarat retirement forced a full 21-town rebuild from the cached raw
snapshots (no refetches). **Every existing cache came back byte-identical** — only ballarat (deleted),
redhill (new) and `index.json` moved. Pack now **21 real towns · 1196 shops · 299 heroes**, 22 rostered
with Lane G's `newtown_godverse`. Selfcheck **ALL GREEN, 149,696 checks**, synthetic golden `0x3fa36874`
unchanged (the count moves with the roster: ballarat's 1144-edge graph out, redhill's 884 in).
**6. → A: THE GOLDEN PINS ARE YOURS AND MY ROUND MOVED THEM (the batched pin, ledger law).** Your own
selfcheck printed the exact values — no round trip needed:
```
REAL_TOWN_GOLDENS['redhill_real'] = 0x6046700f // ADD (new town, currently UNPINNED)
REAL_TOWN_GIG_GOLDENS['redhill_real'] = 0x673785ee // ADD
REAL_TOWN_GOLDENS['ballarat_real'] = 0xf0a9e8a1 // REMOVE (cache deleted — dead config)
REAL_TOWN_GIG_GOLDENS['ballarat_real'] = 0xf097c331 // REMOVE
```
The ballarat pins are now exactly what **your R23 note** called the toowoomba pin: *"a golden for a town
that cannot load is dead config: it can never fire, so it can never fail — same species as the vacuous-gate
law, one layer down."* Your words, my retirement, same fix. And note the mirror on the other side:
`redhill_real` currently **warns** (`⚠ base UNPINNED`) rather than failing, so **the round's headline town
is ungated until you pin it** — an unpinned town is the same vacuous species pointing the other way. Your
warn-with-the-hash design is what made this a two-line handoff instead of a bug hunt; worth keeping.
**7. → F: an R23 orphan I created and owe you.** `tools/qa/tour_shots.py` (your v4.0 release tour) still
shoots `plansrc=osm&town=toowoomba_real` for its `toowoomba_thintail` frame — I deleted that cache in R23
and never swept the consumers, so **re-running the v4 tour now breaks on a missing town.** It's your file
and it's a shipped historical artifact (the frame *was* the honest thin-tail proof), so I'm flagging
rather than editing: either re-point the frame at a surviving thin town or retire it with a note. Same
class, lower stakes: `web/js/citygen/gigs.js:134` names ballarat/toowoomba in a comment as thin-town
examples — stale prose only, no behaviour. **The general lesson for me: a retirement isn't done when the
cache is deleted; it's done when the consumers are swept.**
## Round 23 — v5.0-alpha: the atlas validator + the toowoomba call ⟨v5.0-alpha⟩
**1. `validate_atlas` — the G2a format gate (ledger #4).** `pipeline/validate_atlas.py`, wired into
`validate_manifest` so it runs in qa gate 3. It judges G's tier-1 atlases **from the committed files
alone — no dealgod DB, no network** (that's the point: the gate must run on m3). No atlases yet ⇒ clean
pass, so it's in place *before* G's lands. **→ Lane G: this is the format you build to.** It enforces:
| rule | source |
|---|---|
| provenance `{license, attribution, generator, snapshot}` present | charter law #3 ("every atlas carries provenance") + `snapshot` pins determinism |
| **no private-individual fields** on items (`seller`/`owner`/`email`/`phone`/…) | charter law #3 ("no private-individual data, ever") |
| 1 atlas/shop, **hard cap 2** | LANE_C_PUB §7.1 |
| **≤1024²**; 2048² only for a **60+ item** shop | LANE_C_PUB §7.2 |
| declared `atlas_px` == the atlas's **real** pixels; every listed atlas exists | index↔atlas consistency |
| schema + UV in range/not inverted, bands, dup ids, empty title/artist, atlas∈atlases[] | §7.3 (schema confirmed verbatim) |
The licence string is **printed** by the gate, not pattern-matched — clearing photos for public release is
a human call (charter law #3), so the gate surfaces it rather than pretending to rule on it. **Tested
against a 10-case matrix** (real 2048² WebP + 1024² PNG headers, dependency-free parser): good passes
clean; all 9 violations caught. A gate that's never caught anything isn't a gate.
**2. Toowoomba — RETIRED (my call, counted). I tried the re-bbox first; the data said retire.**
D's finding: DEAD — 0 patronage finds in 1417 checks. I re-bboxed 3.0 km CBD-wide → 1.6 km onto the
Ruthven St heart. Result:
| | median NN (the named metric) | hub ≤160 m (D's real test) |
|---|---|---|
| toowoomba @3.0 km | 395.7 m ❌ | 3/12 |
| **toowoomba @1.6 km (re-bbox)** | **89.4 m ✅ "passes"** | **2/12 (17%) — worst in the pack** |
| darwin (D: ALIVE) | 68.3 m | 7/12 (58%) |
**The re-bbox fixed the metric and made the town worse.** Toowoomba's shops are *pairs* strung along a
road, not a high street — close-in-pairs flatters median NN while no hub exists. Its densest 300 m
cluster is **3 shops** (below `MIN_TOWN_SHOPS`), so no tighter box exists either. Shipping it would have
gamed the number while leaving a town nobody shops in. **Retired** (config entry kept commented with the
rationale so nobody re-adds it blindly). Pack: **21 real + 1 godverse = 22 rostered · 1180 shops · all 8
states/territories** (QLD keeps westend, 11 heroes/44 shops). selfcheck **ALL GREEN 152015/152015**.
### ⚠️ → Lane A: two things before you freeze the spacing warn (#5)
1. **Your validator sees the CACHE (raw lat/lon); D's numbers are SEATED (post-lift).** They differ:
**darwin — D 27 m seated vs 68.3 m raw**; toowoomba 388 seated vs 395.7 raw (spread towns barely move,
clustered ones move a lot). **Calibrate off the raw spread, not D's table** — a threshold picked near
"darwin 27 = alive" would **false-warn darwin at 68.3**. Raw spread now: daylesford 11.6 · katoomba
22.4 · bowral 27.9 · … · launceston 76.8 · braddon 119.2 · **ballarat 254.6**. A ~200 m line has
daylight on both sides.
2. **Median NN alone is gameable — the hub fraction is the discriminator.** My re-bbox is the proof: NN
improved 4.4× while the town got worse. Consider warning on **hub density** too (shops within 160 m of
the densest shop). **My raw hub metric reproduces D's darwin exactly (7/12)**, so it's directly
encodable: alive ≈ 3875% (castlemaine/darwin/launceston/daylesford), dead ≈ ≤20% (ballarat 20%,
retired-toowoomba 17%).
3. **ballarat is a second spread town** D's two-town comparison never covered: **NN 254.6 m, hub 4/20
(20%)** — it *will* trip your warn (correctly). Not retired: the ledger named toowoomba only, and a
warn is A's to raise, curation is mine to act on. Flagging for Fable's next-round call.
## Round 22 — v4.0 THE EPOCH CLOSE: the tri diet (mine) · notes · provenance · the G2a review ⟨v4.0⟩
### ⚠️ Ledger #1b — the `venue_night` tri diet: **MY four furniture GLBs. Fixed. → F: RE-MEASURE.**
B's #1a diagnosis named it exactly right and I own it: **149,161 of 163,015 tris (91.5%)** were four
GLB street props of mine. Buildings 348, posters 222 (A's cap worked — never the lever), pools 0. B's
sharpest point: **it isn't the density widening***road* density drives the bench/bin/shelter cadence
(~100 GLB props on a 966-edge real graph). Great call; the shop count was a red herring.
**The fix** — collapse-decimate via the R13 instrument tool (`decimate_props.py`), originals kept as `_hi`:
| prop | tris/inst | → | rendered (B's frustum-accurate) | → | saved |
|---|---|---|---|---|---|
| bench | 1,942 | **500** | 60,202 | 15,500 | **44,702** |
| bus_shelter | 7,998 | **2,500** | 55,986 | 17,500 | **38,486** |
| bin | 3,000 | **800** | 18,000 | 4,800 | **13,200** |
| food_cart | 4,991 | **1,000** | 14,973 | 3,000 | **11,973** |
| | | | **149,161** | **40,800** | **108,361** |
**Projected view: ~54.7k** (B's 163,015 baseline) or **~113.6k** (vs F's 221,935 re-measure) — **PASS
the 200k gate either way**, 86145k to spare. **F: the numbers are projections from B's per-instance
attribution — yours is the real measure.**
**Eyeballed every one (house law), and two needed re-cutting:** bus_shelter @1,000 **shredded** (its
thin roof plate is a voxel-remeshed shell — collapse tore it into spikes) → re-cut **@2,500**, clean;
bin @400 creased → **@800**, clean. bench @500 is perfect. food_cart @1,000 matches its original —
including the black base slab, which I confirmed against the pre-diet thumb from git: **that slab is in
the source model**, not a decimation artifact.
**Zero blast radius:** dims/footprints/heights **identical**`manifest.json` **byte-identical** → B's
placement and A's goldens untouched. Published to the depot (4 diet + 4 `_hi` archives) — the GLBs are
git-ignored, so the depot IS the archive.
**→ Lane B, your second note, cause found (NOT fixed — deliberate).** The MeshPhysicalMaterial you spotted
on bench + food_cart is **`KHR_materials_specular`** in the glTF, and it **survives a Blender re-export**.
Fix = neutralise specular pre-export (or strip the extension) → three.js falls back to MeshStandardMaterial.
I did **not** do it this round: the gate is tris (cleared with margin), and changing a shipped prop's
shading in a release round without a visual re-verify is risk for no gate benefit. **Filed for v4.x** with
the cause. Same list: **food_cart is 3.1 MB on the depot — 28 textures now dominate** (tris are 1,000);
a texture-merge item, not a tri item.
**Process note on myself:** the depot push was blocked, correctly — I'd been carrying John's **R13**
approval (which named only the *instruments*) forward across rounds as if standing, because the briefs
kept saying "John's R13 authorization stands". Briefs aren't the user. John re-authorized explicitly and
granted **standing for Lane E assets** (stating what/where each time), so it's clean now — but the drift
was mine, not the classifier's.
## Round 22 (cont.) — notes refresh · provenance v4-final · the G2a review
**1. MODELBEAST notes refreshed (ledger #3).** `pipeline/README.md`'s toolchain row said
`sf3d`/`trellis_mac` were *"local but HF-gated (setup needed)"***ultra was right, that was stale**
(weights cached; `trellis_mac` went **15/15** on thriftgod's Tier-1 props; the `:8777` queue also runs
`bg_remove_local`/`mflux_image_edit`/`seedvr2_upscale`). Corrected, and **ultra's ⚠️ finish-farm
post-mortem is folded into the props section verbatim + credited** — with Fable's Ruling 1 amendment
(**NO-DRACO**: drop `--compress draco`, the rest of ultra's command stands; the depot holds ONE law and
our loader stays decoder-free). Our own `bake_lowpoly.py` / `decimate_props.py` are noted as *not* the
finish farm, so nobody conflates them.
**2. Provenance table → v4-FINAL (ledger #6).** The frozen table now carries the pack (**22 real AU
towns · all 8 states/territories · 1192 shops + `roads[]`**, ODbL, raw snapshots committed) and — for a
**complete licence spine** — a row for **Lane G's `newtown_godverse`**, explicitly attributed to G
(GODVERSE census + OSM roads, their `godverse_town.py`, their fields). Nothing shipped is unaccounted.
**3. 🐛 The index was missing Lane G's town.** `index.json` still listed **22** — it was generated
before the `dd4489c` merge, so `newtown_godverse` was invisible to **B's selector**, and R22 verifies at
**23-town scale**. Regenerated → **23 towns**, selfcheck **ALL GREEN 161300/161300**. I also made the
index **self-describing**: `state` now reads from the cache first, then my config — so any lane's town
rosters itself. **→ Lane G: add `"state": "NSW"` to `newtown_godverse.json` and the index picks it up**
(it's the one town showing a blank state).
### → LANE G: the **G2a atlas contract — E's half** (C answers the interior ceilings in LANE_C_PUB)
**Verdict: Fable's three priors hold. Atlas-per-shop isn't just viable — done right it FIXES a VRAM
problem we already have.** Two conditions and one reuse:
**a. Atlas per shop — YES, with one hard condition: dispose on exit.** Today `stockpack.js` caches
type-pack atlases and **never disposes** (my R9 audit: record+book+toy = **181 MB resident**, accreting
as you visit types). Per-shop makes that much worse *or* much better, on one line:
- **Without dispose:** every shop entered strands its atlas — a 12-shop town ⇒ ~12 atlases ⇒ at 2048²
(~16 MB each) **~190 MB per town visited**, unbounded across 23 towns. Non-starter.
- **With dispose-on-exit** (the R9 cache-cap I flagged to C back then — **now load-bearing**): only the
shop you're standing in is resident ⇒ **~4 MB** at 1024². That's **~45× better than today's 181 MB**.
G2a is the reason to finally land that cap. **Ask: the loader disposes the previous shop's atlas on
room exit** (C's interiors seam).
**b. Size the atlas to the shop, not to the format.** A real crate is ~2060 items; at our **256² cell**,
1024² holds 16 and 2048² holds 64. **Ship ≤1024² for a typical shop** — don't hand a 6-item shop a
2048². Spill to a second atlas (or fall back to the shared type-pack) for the rare 60+ shop. Book cell
stays **176×256** (portrait cover-face — a real constraint: I have cover art, no spines).
**c. Reuse my index schema verbatim — then nothing downstream changes.**
`{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,price_band,atlas,uv:[u0,v0,u1,v1]}]}`,
keyed per shop (e.g. `stock_shop_<godverseShopId>_index.json`). You get for free: `stockpack.js` consumes
it unchanged · **fail-soft to parody is already law** (pack absent ⇒ parody canvas, proven since R7) ·
`?noassets` already fetches zero · and **`validate_pack.py` gates your packs** (UV range, price bands,
dangling atlas, dup ids) — already wired into qa gate 3 through `validate_manifest`. Add index fields and
tell me; I'll extend `REQUIRED`.
**d. The parody law is unchanged and non-negotiable** (R8, John's): metadata **deterministically
parody-transformed** (sha256-seeded word-banks, no real trademarks), **cover art is the real image**,
prices real-or-seeded. Real dealgod inventory changes nothing here.
**e. Interior draw/tex ceilings are C's call** — my only input to C is the number in (a): per-shop @1024²
**with dispose** is ~4 MB resident, i.e. *cheaper than the status quo*, not more expensive.
## Round 21 — the v4.0 town pack (pull-forward, DATA-ONLY) ⟨v4.0⟩
> Ledger #5 — **NON-GATING**: nothing here touches a golden until A absorbs in R22, and F tags
> `v4.0-beta` with or without it. **The pack: 22 towns · 1192 shops · 298 heroes · all 8 states/
> territories.** selfcheck **ALL GREEN 154141/154141**, synthetic `0x3fa36874` unmoved, all 22
> byte-identical on re-run. → **Lane A (R22): 34 goldens await your pin** (17 new towns × base+gig).
**Curation.** Picked on the instincts the first five earned: **secondhand density first** (the heroes
are the game — a town with no op shops isn't a PROCITY town), a walkable centre inside a 2.03.4 km
bbox (the mega-strip law), and a national spread. 18 candidates proposed, 17 survived.
| tier | towns (heroes/shops) |
|---|---|
| meccas | **fitzroy 40/160** · adelaide 29/116 · brunswick 24/96 · katoomba 20/80 · hobart 20/80 · fremantle 20/80 |
| strong | newtown 18/72 · geelong 17/68 · northbridge 15/60 · glebe 13/52 · westend 11/44 |
| solid | newcastle 9/36 · bendigo 9/36 · launceston 8/32 · daylesford 8/32 · bowral 8/32 · marrickville 7/28 · castlemaine 6/24 |
| thin tail | braddon 5/20 · ballarat 5/20 · toowoomba 3/12 · darwin 3/12 |
**The cap is a curation filter, not just a density knob (the round's nicest finding).** Because texture
is pegged at **3× heroes**, a town without a secondhand strip *cannot* inflate into a retail strip.
**Wollongong** had ~62 real shops but only **1 hero** → capped to 4 → **dropped by the ≥6 floor**. It
curated itself out. John's "subtle" directive is doing quality control for free: a town has to earn its
place with real op shops. The thin tail (toowoomba/darwin, 3 heroes) is the same signal, weaker —
**A/Fable may want to drop them in R22**; I've left them in as honest data rather than pad or hide.
**Newcastle: my centre was wrong, not the town.** The CBD bbox held 2 heroes; Newcastle's real
secondhand strip is **Hamilton / Beaumont St**. Re-centred → **2 → 9 heroes, 8 → 36 shops**.
### 🐛 The bug I shipped in R18 and caught here — stale roads on a re-centre
Re-centring Newcastle refetched the **shops** (my query-comparison caught the bbox change) but
**silently reused the old CBD roads**`fetch_roads_raw` had **no** query-comparison, only
`fetch_raw` did. Result: Hamilton shops seated against a road graph **2.5 km away** → A's lift
marched the town out with **ZERO shops**, which surfaced as 32 bogus "open-late invariant" failures
(a symptom — the real cause was `shops=0`). Ten other towns lack a video shop and pass fine, which is
what proved it wasn't the open-late rule. **Fixed**: `fetch_roads_raw` now compares its query like the
shops path — the bbox lives in the query, so a re-centre must invalidate **both** snapshots. Newcastle
rebuilt: 34 shops seated, openLate=1. *Lesson: a cache-validity check applied to one of two symmetric
paths is a trap with a fuse on it.*
**Discipline unchanged:** bounded bboxes, raw snapshots committed (shops + roads per town), C's §6
mapping, SUBTLE guardrails (heroes never subsampled, texture 3×, **2392 drops counted**), deterministic
emit, ODbL (`SOURCES.md` + per-cache fields). Overpass: batch 1 died on a fatal 504 after 25 retries
(pack scale genuinely rate-limits) — courtesy sleep raised to 4 s, retry/backoff carried the rest; I
split into two polite batches rather than hammer a public API.
**→ Lane A (R22 absorb):** 17 new towns to pin (34 goldens). Sizes range 12 → 160 shops (fitzroy is
2× katoomba — your capacity/overflow fences get a real load test). **→ Lane B:** `index.json` now has
22 towns; your selector reads it already (thanks — R20 #5 closed). Also: thanks for the selfcheck
`index.json` skip, A — my R20 seam flag landed clean.
## Round 20 — v4.0-beta THE FIVE TOWNS: the density widening ⟨v4-beta⟩
> Ledger #2, E's half — built to **C's LANE_C_PUB §6 table** under **John's SUBTLE directive**
> ("texture, not takeover"). Widened fetch → re-emitted 5 caches → `towns/index.json`.
> → **Lane A: caches re-emitted, densities are yours to absorb + re-pin** (see the two flags below).
> → **Lane B: `index.json` is live — drop the `REAL_TOWNS` hardcode in `hud.js:11`.**
**The widening.** `build_towns.py` now fetches C's tasteful staple list (`shop=*` + `amenity=cafe`,
per C's †) and maps every class through C's copy-paste table onto **existing** registry types — no new
archetypes. Guardrails honoured: **no** vehicle/tyres/fuel, funeral, storage, industrial, or pure
services (hairdresser/beauty/bank/laundry/estate_agent). Raw is a **new snapshot** (the fetcher
auto-refetches when the class list changes — the query is recorded in each snapshot, so nobody has to
remember `--refetch`), committed like the first. Deterministic: same raw → byte-identical cache.
**SUBTLE, enforced by construction.** The secondhand **heroes are never subsampled** (they're the
game). Texture is capped at **3× the hero count** (the top of C's ~23× band — keeps the town alive
without letting retail take over); over-cap towns are **seed-subsampled deterministically**
(`sha256(townKey:osmId)` rank — stable across rebuilds so A's goldens hold) and **every drop counted**
in `counts.textureDropped`:
| town | heroes | texture | ratio | dropped | total | shop mix |
|---|---|---|---|---|---|---|
| katoomba_real | 20 | 60 | 3.0× | 4 | **80** | milkbar 33 · dept 22 · opshop 13 · stall 5 · book 4 · record 2 · video 1 |
| newtown_real | 18 | 54 | 3.0× | 156 | **72** | milkbar 30 · dept 21 · opshop 6 · record 5 · book 4 · stall 3 · toy 2 · video 1 |
| fremantle_real | 20 | 60 | 3.0× | 81 | **80** | milkbar 43 · dept 16 · opshop 9 · book 5 · toy 2 · video 2 · pawn 1 · record 1 · stall 1 |
| bendigo_real | 9 | 27 | 3.0× | 81 | **36** | milkbar 14 · dept 12 · opshop 3 · book 3 · stall 1 · record 1 · video 1 · pawn 1 |
| castlemaine_real | 6 | 18 | 3.0× | 43 | **24** | milkbar 9 · dept 6 · opshop 4 · stall 3 · book 2 |
Density is now **34× pre-widening** (621 → 2480 shops) — inside the ledger's 36× expectation for A.
Katoomba reads right: the op shops (Cancer Help, Tanglecare, Cat Defence, Red Shield) still anchor the
strip, now with The Hatters Cafe, Blue Mountains Chocolate Co, K Hub and Bunnings as texture around them.
**`web/assets/towns/index.json`** (new, E-owned): `{schema:"procity-towns-index/1", generated, license,
attribution, towns:[{key, town, state, shops, roads}]}` — 5 towns, built from every cache on disk.
### ⚠ Two flags for Lane A (both land in your absorb pass)
1. **10 expected golden re-pins** — the widening moves every real town's base + gig golden (5 × 2).
That's the designed E→A handoff (ledger #2). **Synthetic `0x3fa36874` is UNMOVED**; fixtures frozen.
2. **`index.json` trips selfcheck's town glob** (a real seam — `selfcheck.js:467` takes *every*
top-level `towns/*.json` as a cache, so the index validates as a town and fails). **One-line fix in
your file:** `.filter(f => f.endsWith('.json') && f !== 'index.json')`. I didn't touch `selfcheck.js`
— it's yours and you're mid-round in it. Fable spec'd the index at this path (ledger #2/#5, B reads
it there), so the path stays; the glob is what needs to learn about it.
### → Lane C (one-line map addition)
`video_games` wasn't in your §6 table (you listed `video`/`games` → `video`). It was already shipping as
`video` pre-widening, so I kept **`video_games``video`** rather than regress those shops to `opshop`
via the fallback. Same family, same target — flagging so your table and my emit stay the one truth.
## Round 18 — v4.0-alpha REAL ROADS: the road-geometry fetch ⟨v4-alpha⟩
> Ledger #3 — E fetches the real street geometry; A's lift builds the CityPlan graph from it.
> **`katoomba_real` is now schema v2 with 484 real streets** (the alpha town). → **Lane A: roads are
> live; harden the lift, resolve the surfaced poster-clearance finding below, re-pin the golden.**
**The data.** `build_towns.py` extended with a bounded `way["highway"~…](bbox); out geom;` per town →
raw cached in `_raw/<key>.roads.overpass.json` (fetch once, John's standing Overpass auth) → `roads[]`
in A's v2 contract (`{id, kind, pts:[[lat,lon],…]}`; `kind` = raw OSM highway class, A's `ROAD_KIND`
maps it, full geometry shipped for A's DouglasPeucker). **katoomba_real: 484 streets** (residential
243, tertiary 65, trunk 58, secondary 51, primary 32, unclassified 23, pedestrian 12 — real names:
Bathurst Rd, Cliff Drive, Civic Place, …), 5344 points.
**Fidelity cut (charter risk #4 — flagged to A, who owns the knob).** I FETCH every `ROAD_KIND`-mapped
tier (→ raw cache, full provenance / beta-ready) but EMIT only the **town street tier** — drivable
roads + pedestrian malls — EXCLUDING `service`/`track`/`footway`/`path`/`steps`/`cycleway`. In
Katoomba's 3 km bbox that excluded tier is **1461 of 2506 ways** (Blue-Mountains bushwalking tracks,
sidewalks, stairs, driveways, parking aisles) — noise for a town street graph and a ~2500-edge blow-up.
The full set is in the raw cache if A wants a tier back (one filter change). This is the honest cut.
**A's lift builds a real town from it** (verified, `generatePlanFor` seed 20261990): 484 streets →
**872 nodes / 799 edges** (main 232, side 531, arcade 36) → 12 blocks, 20 lots, 20 shops, **3 venues
(pub/band_room/rsl), 12 gigs**. `pickVenues` lands venues on the real graph — ledger #2 requirement met.
**⚠ FINDING for Lane A (the alpha's failure-list deliverable — charter risk #3).** On the real graph,
**spine posters overhang a crossing carriageway**: selfcheck's kerb-clearance gate fails on 4 seeds
(spine poster clears its cross-edge by **1.6 to 2.5 m**; e.g. seed 20261990 poster 50 vs edge 58 =
2.42 m). Same CLASS as A's R15/R16 corner-poster fix, now on **real** cross-street geometry — a
venue's frontage seats on the spine where a real side street crosses, so its poster reaches over that
carriageway. **Owner: A** (`pickVenues` / `POSTER_CLEAR` on the real graph — the "A again" harden
step). **My roads data is valid** — the v2 cache validates (roads accepted); the 4 failures are
plan-level poster placement, not cache errors. Not a fidelity artifact either: the crossing streets are
real primary town streets, so trimming roads wouldn't fix it. Golden settles after A's fix (pre-fix
value `0x6014a8e6`). Handshake E→A: **katoomba roads live + this finding**.
**Other 4 towns** (data-only; A hardens against them, only katoomba gates): all now v2 —
newtown_real 1086 roads, bendigo_real 1552, fremantle_real 622, castlemaine_real 397 (dense/regional
towns → bigger graphs; real data, A's DP + density classification handle it). Roads are ODbL, raw
committed, `SOURCES.md` updated. (Robustness: `build_towns.py` gained Overpass retry/backoff — the
public API 504s under load.)
**The finding is SYSTEMATIC** — the spine-poster clearance fails identically on **all 5** real-road
towns, every seed (23 checks, 0.9 to 4.7 m). That it reproduces across five different real street
grids confirms it's **plan-level poster placement, not town data** → squarely A's (`pickVenues` /
`POSTER_CLEAR`). Scoping holds: **only the `real/*` v2 path fails — synthetic + fixtures + classic +
default boots are all green** (roads[] is the only changed path, per the alpha law). A's one fix
generalizes to all five.
## Round 17 — v3.2 THE SCOUT: real town caches + the verify fix ⟨v3.2 amendment⟩
> The real-map scout for **v4 THE REAL MAP** (John's pick) — bounded data groundwork, no new game
> systems. **5 real AU town caches produced + the `--verify` false-drift fixed.** → **Lane A: caches
> are live under `web/assets/towns/`; pin the per-town goldens** (printed below by selfcheck) and feed
> them through `plan_osm` — E→A handshake.
**Ledger #4 — `publish.py --verify` false-drift fixed (the warm-up).** The drift check filtered the
depot list to `procity_*`, so genuinely-published non-`procity_` assets (`sit.glb`,
`vintage-cash-register.glb`) were false-flagged "not live." Now it compares the published-NAME record
(`_published.json`) against the depot's full file list (`fetch_depot_files()`), confirming any name is
live — and only flags OUR `procity_` namespace as unrecorded, ignoring other projects' files on the
shared depot. `--verify` now: `✓ all 39 recorded assets live`.
**Ledger #6 — the real-map scout (the round's biggest item).** `pipeline/build_towns.py` ports
thriftgod `build_city()`'s Overpass route to **per-town** caches in A's `procity-town-cache/1` contract
(`validateTownCache()`). Pipeline: **bounded** Overpass query → raw response cached in-repo → dedupe
(OSM node+way) → suburb-fill → **parody** the trademarked charity/franchise chains (indie shops keep
real OSM names) → OSM `shop=*` → registry SHOP_TYPE → `web/assets/towns/<key>.json`.
| cache | town | shops | span | types | selfcheck golden (→ A pins) |
|---|---|---|---|---|---|
| `katoomba_real` | Katoomba NSW | 20 | 1.11 km | book4 opshop13 record2 video1 | `0x61760542` |
| `newtown_real` | Newtown, Sydney | 18 | 2.36 km | book4 opshop6 record5 toy2 video1 | `0x95978b45` |
| `fremantle_real` | Fremantle WA | 21 | 1.89 km | book5 opshop10 pawn1 record1 toy3 video1 | `0xee9fd09a` |
| `bendigo_real` | Bendigo VIC | 9 | 2.06 km | book3 opshop3 pawn1 record1 video1 | `0x5e3e76a6` |
| `castlemaine_real` | Castlemaine VIC | 6 | 1.25 km | book2 opshop4 | `0xf1a628a1` |
**Data discipline:** deterministic (same raw → byte-identical cache; shops id-sorted, no RNG — verified
re-run byte-identical); bounded (all spans < 2.4 km, well under the 5 km mega-strip floor); raw
responses committed to `web/assets/towns/_raw/` so **re-runs never re-fetch** (Overpass hit once per
town, John-authorized). `katoomba_real` is the fixture-comparison town (vs the hand-made `katoomba`).
All 5 boot through the full structural + gig suites (selfcheck **16596/16596 green, synthetic golden
`0x3fa36874` unmoved**). **Provenance:** OSM is **ODbL** `SOURCES.md` (E-owned licence table) +
`license`/`attribution` fields on every cache; only public POI data (name/type/coord/suburb), no
private-individual data.
## Round 16 — v3.1 THE FLIP: the drummer's sit clip (E asset half) ⟨v3.1 amendment⟩
> Ledger #2 asset half — the drummer finally sits. **E produces the clip; D wires it** (`_rotWithHips`
> + `spawnRig` foot re-plant + F's seated-stature gate). **`sit.glb` delivered, verified, committed +
> on depot.** → **Lane D: it's ready** (`web/models/peds/sit.glb`, canon binds 195/195, seated confirmed).
**Asset:** `web/models/peds/sit.glb` a mesh-free animation clip, **structurally identical to the
shipped `walk.glb`/`idle.glb`**: 66 nodes (65 `mixamorig` bones + root), 0 meshes, 1 dangling skin
(inverseBindMatrices + 65 joints, referenced by 0 nodes the vestigial artifact Blender glTF I/O
leaves, which makes glTF tools read it as an armature), 195 channels (65 bones × TRS). 109 KB, no Draco.
**Source + provenance:** Adobe **Mixamo `Sitting_Idle.fbx`** (533 KB) fetched by `scp` from the
tailnet source box `johnking@100.91.239.7:~/Documents/mixamo-fetch/out/` per Lane D's recipe (the
local `~/Documents/mixamo-fetch` path is stale/empty). Converted on-device via Blender headless
(`pipeline/fbx_to_clip.py`, **new tool**): import FBX export skinned GLB pure-python mesh-strip
that keeps the skeleton + dangling skin + animation and re-indexes/compacts. Raw Mixamo `mixamorig:`
naming is already canonical (`rigs.js._canon` normalizes `mixamorig<N>` `mixamorig`, so no bone
rename). **Licence zone: Adobe Mixamo royalty-free animation license the SAME zone as the
already-shipped `walk`/`idle` clips** (the whole ped fleet is Mixamo-rigged). Mesh stripped, so only
the skeleton *motion* ships (retargeted onto the game's own peds), never Mixamo character geometry.
**Verified (E's half — D verifies the wired in-scene pose):**
- **three.js** (vendored GLTFLoader): loads, dur 4.33 s, 195 tracks, 65 bones, **195/195 bind** to a
ped skeleton after `_canon`, **0 NaN** track values (mesh-strip re-index is clean).
- **Blender** (the tool that measures `walk` at 1.276 m standing): the clip poses **seated** Hips
descend 0.955 m (standing) **0.299 m**, stature 1.276 0.758 m. Eyes-on bone-dot render confirms
the folded seated silhouette vs walk's upright one: `docs/shots/laneE/sit_clip.png` (left walk, right sit).
**Depot:** published to `digalot.fyi/3god` via the passwordless tailnet ingress under **John's R13
authorization** (`_published.json` 38 39; confirmed 200 on the public depot). **NOTE (benign):**
`sit.glb` like `vintage-cash-register.glb` is non-`procity_`-prefixed, so `publish.py --verify`'s
`procity_`-only drift filter lists it as "not live" (a **false drift** it IS live). Cosmetic only;
`validate_manifest` gates are unaffected (clips are not manifest GLBs).
**"Manifest":** ped clips load by **convention** (`rigs.js` `base='models/peds/'`), NOT via
`web/assets/manifest.json` so there is no asset-manifest entry, same as `walk`/`idle`. D owns the
loader wiring (`spawnRig`/fleet). Bench-sit loiter (Fable's stretch goal) reuses this same clip.
## Round 14 — v3.0 PROVENANCE FREEZE ✅ (release round)
> **FROZEN for the v3.0 tag** (2026-07-16). Lane E R14 is a provenance close-out — **no new synthesis**
> (genres final: pubrock / grunge / covers; John "fine for now"). Everything below is the durable
> Lane-E release contract; the deep per-asset narrative lives in `pipeline/AUDIT.md` (current through
> its R13 section). Counts tool-verified (glb_stat / ffprobe / manifest parse) and adversarially
> re-audited by two independent passes — both PASS, **0 release blockers**.
**Release status**
- **Gate 3 GREEN** `validate_manifest.py --depot` 0 errors, 0 warnings. Not blocking the tag.
- **`publish.py --verify`** depot matches `_published.json` (38 entries). The lone drift note
(`vintage-cash-register.glb` absent from the *tailnet mirror*) is cosmetic: it's a depot-reuse asset
and IS live on the public `digalot.fyi` depot (Gate 3 confirms), so the manifest's real target is fine.
- **Licensing 🟢 clean** 0 quarantined/branded assets in the manifest (grep of every AUDIT QUARANTINE
token vitra/eames/calligaris/ikea/technics/1210/tuborg/uploads_files/akai/mpc/turntable 0 hits).
- **Poster templates (R13 debt #4): no ask from B** no new templates (E builds only on B's ask).
- R14 doc-consistency fixes applied: AUDIT.md R13 section added; `roomtone-video` gain 0.30.35 aligned
to the manifest; manifest `glb_law` string amended to state the 8k-hero / 14k-instrument exceptions.
### Provenance & licensing — by source zone (all 🟢 web-ok)
| zone | assets | source | method | licence |
|---|---|---|---|---|
| House library GLBs | 18 | thriftgod / house depot, John-presorted **generic** props (benches, shelves, counter, crate, bin, streetlight, food_cart…) no recognizable trademark | `normalize.py` (passthrough or COLLAPSE 5k) | 🟢 web-ok |
| On-device synthetic GLBs | 11 (+5 `_hi`) | MODELBEAST `flux_local` (FLUX.2-klein-4B) concept `trellis_mac` mesh | 6 hero via `bake_lowpoly.py` (≤8k) · 5 gig instruments via `normalize`→`decimate_props.py` (14k) | 🟢 CC0-equiv, 100% on-device |
| fal-Hunyuan GLBs | 3 | `flux_local` concept `fal-ai/hunyuan3d/v2` (glass_case, magazine_rack, bus_shelter) | `bake_lowpoly.py` (≤8k) | 🟢 original (~$0.64 total spend) |
| Depot-reuse GLB | 1 | `cash_register` = John's own `thriftgod_hero` (already live) | reuse (not regenerated) | 🟢 cleared |
| Skins (2D) | 96 | 90sDJsim ports (7 facade + 2 ground) + thriftgod R1 store-art + `flux_local` gap-fill fronts/posters | `gen_skins.py --local` (on-device) | 🟢 web-ok |
| Audio pack | 28 | 100% procedural numpy synthesis (`gen_audio.py`) | oscillator/noise render ffmpeg OGG+M4A | 🟢 CC0-equiv, parody-safe |
| Stock packs | 3 (350/311/273) | GODVERSE `dealgod` Postgres **real** cover art | `build_stock_pack.py`: parody-transform metadata + atlas | 🟢 real-art / parody-metadata |
| Ped clip **v3.1⟩** | `sit.glb` (E R16; + pre-existing `walk`/`idle` from D/90sDJsim) | Adobe **Mixamo** `Sitting_Idle.fbx` | `fbx_to_clip.py` (Blender, mesh-strip) | 🟢 Mixamo royalty-free (motion only, mesh stripped) |
| Ped clip **v5.1⟩** | `look.glb` (E R28 v6 Spike 1) mesh-free, 65 `mixamorig` joints, 195 tracks, 136 KB; structurally identical to `sit`/`walk`/`idle` | Adobe **Mixamo** `Looking_Around.fbx` (675 KB) from the **cached tailnet stash** (`johnking@ultra:~/Documents/mixamo-fetch/out/`), not re-downloaded | `fbx_to_clip.py` (Blender, mesh-strip) the R16 path, unchanged | 🟢 Mixamo royalty-free (motion only, mesh stripped) same zone as walk/idle/sit |
| Ped clip **v5.1 NOT BUILT** | `lean.glb` **the source does not exist** (see R28 §1) | | | |
| Town pack **v5.0-FINAL⟩** | **21 real AU towns, all 8 states/territories** **1196 shops** (299 heroes) + real street geometry (`roads[]`) | **OpenStreetMap** via Overpass API bounded per-town bbox; raw shops+roads snapshots committed in `_raw/` | `build_towns.py`: fetch dedupe/suburb-fill/parody C's §6 class map 3× texture cap (drops counted) v2 cache + roads | 🟢 **ODbL 1.0** OSM contributors; `web/assets/towns/SOURCES.md` + `license`/`attribution` on every cache) |
| Godverse towns *(Lane G's, listed for completeness)* **v5.0-FINAL⟩** | `newtown_godverse` (72 shops) · **`redhill_godverse` (37)** the town whose record shop is THE shop. Both rostered in E's `index.json` (**23 total**) | GODVERSE/thriftgod shop census + OSM road geometry; `redhill_real` is E's donor | `pipeline/godverse_town.py` (**Lane G owns this provenance**) | 🟢 ODbL 1.0 + GODVERSE census (G's own `license`/`attribution` fields) |
| Real-stock atlas **v5.0-FINAL⟩** | **1 crate, 120 items**, 2×2048² webp Monster Robot Party (`3962749`) | **the real POS** (`recordgod`) crate 550 "DEEP HOUSE"; covers are **the shop's own product shots**, cached by dealgod | `pipeline/godverse_stock.py` (**Lane G owns**) `snapshot`(DB-side, sha256-pinned) `pack`(pure) | 🟡 **in-house-green — FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE** · `sourcing:"real"` · ids `sku_<POS id>` |
| Mint atlases **v5.0-FINAL⟩** | **14 crates, 224 items**, 14×1024² webp keyed hero shops with **no POS** (5 record · 6 book · 3 toy across both godverse towns; the 6th record crate is the real one above) | **real dealgod listings** (the sellers' own product shots) a *seeded sample*, `random.Random(shop_id)`, deterministic | `godverse_stock.py` mint baker (**Lane G owns**) same bake path, sampled source | 🟡 **in-house-green — same flag** · `sourcing:"mint"` · ids `mint_<listing id>` · **never claimed as that shop's stock** |
**GLB fleet = 33 manifest (23 fit + 10 furniture) + 5 `_hi` = 38 published.** Only external spend ever:
~$0.64 (fal-Hunyuan, R5). Everything else is $0 / on-device (electricity only).
### ⟨v5.0-FINAL⟩ The licence line's story — why one printed line is a provenance mechanism
**The only 🟡 in this table is the v5 stock art, and the flag is the whole control.** Charter law #3 says
clearing photos for public release is **a human call** so the gate does not pattern-match the licence
and rule on it. It **prints it**, every run, for every atlas, and a human decides. That makes one printed
line the mechanism the amber rows depend on, which is why it earned two rounds of work:
- **R23** the gate globbed a name the runtime can never load: **0 files matched, returned 0**. It passed
identically whether atlases were absent, misnamed, or perfect. (The vacuous-gate law came from this.)
- **R25 the line went silent and the gate still said OK.** Not a typo: the **check** read
`licence or license` (permissive) while the **print** read `licence` only. G's re-emit moved to US
`license`; the check passed, the print resolved to `{}`, and `FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE`
**stopped reaching a human on a real shop's product photos**. *The fallback WAS the bug* it let one
path stay permissive so it could not feel the other break.
- **The fix is structural, not a spelling.** One spelling, no fallback (British `licence` now ERRORS,
naming C's §7.3 retirement) · **one lookup, one owner** (`validate_index` returns the evidence string;
`main` prints what it is handed and never re-looks-up the field no second path left to drift) · and
**the line is COUNTED**: judged N atlases N licence lines shown, or FAIL. A gate that can say OK while
hiding the flag cannot exist by construction.
**Today, frozen:** `atlas-QA OK — 15 per-shop atlas(es) valid, 15 licence line(s) shown, 0 warning(s)`.
Fifteen crates, fifteen flags, every one in front of a human. **Both zones print** the mint rows carry the
same in-house-green flag because a seeded sample of real dealgod listings is still **the sellers' own
product shots**. Minting changes whose stock it *claims* to be, never whose photograph it *is*: that is why
`sourcing` is honesty and the licence is unchanged by it.
### ⟨v5.0-FINAL⟩ The id-space fences — provenance carried in the id itself
Two disjoint namespaces, blessed as design (C §7.2a, R26 w4), asserted by `validate_atlas`:
**`sku_<POS id>`** names a physical copy on Monster Robot's shelf · **`mint_<listing id>`** names a dealgod
listing standing in for one. A crate wearing the other's prefix **FAILS**. This is not naming hygiene:
tier-2's sold-means-gone looks up POS skus, so a `mint_` id filed under `sku_` doesn't fail a gate it
**sells a real record out of a real shop to satisfy a minted stand-in**. G refused that shortcut when it
would have gone green the easy way, and that judgment is the fence. *(Same standing note as
plan-ids vs godverse-ids: one name, one space never compare across.)*
### Audio inventory — 28 assets (`web/assets/audio/`, committed; contract = `manifest.audio`)
All procedural, seeded, $0, parody-safe; OGG/Opus + M4A/AAC; beds+music loudnorm 16 LUFS, SFX peak-norm.
| group | n | assets | gig? |
|---|---|---|---|
| ambience | 7 | street-day, street-night, rain, roomtone-{retail,milkbar,video}, **crowd-walla** | crowd-walla `gig:true` |
| sfx | 8 + 6 steps | doorbell, door-open, till, riffle, toast, tram-bell, tram-rumble(loop), **applause**; footstep{pavement×3, timber×3} | applause `gig:true` |
| music | 7 | record-shop, milkbar, video-synth, arcade, **gig-pubrock**, **gig-grunge**, **gig-covers** | 3 gig beds `gig:true` |
- **Gig beds (R13, the manifest key IS the gigKey `gig-<genreKey>`):** `gig-pubrock` (pub, 0.6, 13.4s),
`gig-grunge` (band_room, 0.6, 17.2s), `gig-covers` (rsl, 0.5, 19.2s). Seamless loops, non-silent
(verified 15.7…−17.6 dB mean). Old `pubrock-live` fully retired (files gone, no live refs).
### GLB inventory — 38 (tri counts glb_stat-verified; footprints/heights match manifest; no Draco, tex ≤1024)
**Fittings (23):** record_crate 5k · wire_shelf 330 · clothes_rack 3.2k · bookshelf 5k · cube_shelf_wide 5k ·
counter 276 · coffee_table 495 · work_table 1.6k · box_crate 78 · crate_stack 3k · **listening_booth 8k ·
drinks_fridge 8k · arcade_cabinet 8k · milkshake_mixer 8k · counter_till 8k** (bake_lowpoly hero) ·
**glass_case 8k · magazine_rack 8k** (fal) · **electric_guitar / bass_guitar / drum_kit / guitar_amp /
mic_stand = 14k each** (R13 decimate) · cash_register 6k (reuse).
**Furniture (10):** bench 1.9k · bench_modern 640 · bench_wood 4k · longbench 1.2k · park_bench 5k ·
streetlight 5k · food_cart 5k · **novelty_record 8k** (bake_lowpoly) · **bus_shelter 8k** (fal) · bin 3k.
**Preserved `_hi` originals (5, on depot, NOT manifest-referenced — LOD/re-bake source):**
electric_guitar 43.5k · bass_guitar 36k · mic_stand 34.1k · drum_kit 26.2k · guitar_amp 96.6k.
### Skins — 96 (`web/assets/gen/*.jpg`, local; all present, 0 missing/orphan)
facade 48 (43 blank + 5 baked-sign: djsim-video/milkbar/pub, market, grimy) · sky 10 · ground 10 ·
wall 8 · interior floor 6 · interior surface 7 · awning 3 · **poster 4** (grunge/retro/screenprint/xerox,
blank name-band). Per-shop-type facade coverage 2 for all types (validate_manifest asserts; min = 3).
### Stock packs — 3 (indexes committed in `pipeline/packs/`; atlases on depot; read directly by Lane C)
record 350 (6 atlases, 256²) · book 311 (4 atlases, 176×256) · toy 273 (5 atlases, 256²). Real cover art
(GODVERSE), **parody-transformed metadata** (sha256-seeded word-banks; no real trademarks). All pass `validate_pack`.
### By-design notes (documented so a future reader doesn't re-flag them)
- **5 `_hi` in `_published.json` but not the manifest** intentional archival hi-poly originals (R13).
- **Stock-pack indexes not in the manifest** by design; Lane C reads `pipeline/packs/*` directly, atlases ship on the depot.
- **`glb_law` tiers** 5k library / 8k baked hero (TRELLIS shell-soup, texture-intact) / 14k gig instruments; now stated in the manifest convention string + AUDIT.
- **`validate_manifest` prints "skins 85 groups"** a shallow `sk.values()` count (the nested `interior` dict counts as 1); true leaf total is 96. Cosmetic label, flagged for v3.1.
- **Minor stale ledger figures** (non-live): `food_cart` 4991 vs `_normalize_results` 5000 (9-tri); a superseded `cube_shelf_01` entry in `_normalize_results` (DROP-filtered from the manifest, rejected in AUDIT). Harmless.
### Deferred to v3.1 (documented, not started)
- **`street_bin` decimation** B's non-blocking, *conditional* ask ("if the combined-lane budget tightens"); the bin is ~3k tris (within house law), and B measured ~100k tri / ~150 draw margin at the busiest block, so no action for v3.0.
- **Texture-bake-to-lowpoly / KTX2-Basis atlas pass** if a future budget tightens; the `_hi` instrument originals + the hero props are the LOD source (R9 VRAM note + AUDIT).
## Round 13 — two genres, the gigKey rename (debt #1), lighter instruments
**v3.0-beta THE DISTRICT.** All local work landed + verified; the one gated step (depot publish)
waits on John. Verification: core gates GREEN (scaffold · consistency 0-warn · citygen selfcheck
3273/3273 incl. v3 gig-layer + assets-on-disk), manifest validator `--depot` GREEN, and the real
app boots clean flags-off / `?gigs=1` (A's 3-venue district, 14 gigs/posters all three gig beds
resolve by canonical key, no stale key, 0 console errors) / `?noassets=1&gigs=1` (0 audio + 0 GLB
fetches, silent-and-fine).
### 1. gigKey rename — DONE (debt #1). **→ Lane B + Lane F: switch your readers.**
The manifest music key IS the gigKey, **always `gig-<genreKey>`** one key, zero mapping. Renamed
`pubrock-live` **`gig-pubrock`**; added `gig-grunge`, `gig-covers`. Files are now
`audio/music-gig-{pubrock,grunge,covers}.{ogg,m4a}`; the old `music-pubrock-live.*` are **deleted**.
Manifest regen'd + validates 0/0; no stale `pubrock-live` anywhere.
- **→ Lane F (interior_mode.js):** DELETE the `GIG_BED` bridge (the `{'gig-pubrock':'pubrock-live'}`
map + `gigBedKey()`); read `ra.gigKey` (`gig-<genre>`) straight against `AUDIO.music[...]`. The
bridge currently maps to the now-gone `pubrock-live`**fails soft to silence** (I verified: no
crash, just a silent band) until you delete it. This is your R13 task #4.
- **→ Lane B (audio.js ~L274/276):** you hardcode `'pubrock-live'` for the street spill — read each
venue's `gigKey` bed instead (`AUDIO.music[venue.gigKey]`, i.e. `'gig-'+genreKey`). Same
fail-soft-silent until you switch. Your R13 task #2 (per-genre spill) covers this.
- **CITY_SPEC §v3 contract line (coordinate wording w/ A):** *"The audio manifest key for a gig
genre IS the gigKey, always `gig-<genreKey>` (`gig-pubrock`, `gig-grunge`, `gig-covers`). No
mapping table: C emits `room.audio.gigKey`, E names the bed, B/F read the same key."*
### 2. Two new gig beds — grunge (band_room) + covers (RSL).
Procedural, seeded, loopable, OGG+M4A, loudnorm, $0 on-device (same discipline as R11/12). All three
are audibly + spectrally distinct (spectral centroids pubrock 2336 / grunge 2271 / covers 1115 Hz;
contact sheet `docs/shots/laneE/gig_beds_spectrograms.png`):
- **`gig-grunge`** (0.6 gain, 17s): down-tuned sludge, quiet-loud dynamic (clean-ish verse → full-fuzz
chorus — visible in the spectrogram), loose heavy kit. Sits at 14.6 LUFS (genre-dense; a 1.02
*inter-sample* peak, not real clipping — plays at gain 0.6 so absolute output ≈0.6).
- **`gig-covers`** (0.5 gain, 19s): mellow RSL covers band — new drawbar-`organ()` voice, walking
bass, soft kit, sparse organ answer. Warm, dark highs (that low continuous band + horizontal
harmonic striations in the spectrogram = the organ). New synth helpers: `organ()`, `grungechord()`.
- **John may re-flavour genres (names only) — I synthesize to order.** Note it's now a coordinated
A+E change: the genreKey lives in A's `registry.js` (drives `gigKeyFor` → the bed name), so a
re-flavour = A renames the genreKey + E renames/re-synths the bed. Defaults grunge/covers are live.
### 3. Instrument decimation — DONE. 5 GLBs → 14k tris each (≤15k target). **→ Lane D.**
Collapse-decimated (Blender, `pipeline/decimate_props.py`) — **not** voxel-remesh, so thin features
survive: mic pole + hanging cable, drum cymbals/stands, guitar necks/headstocks all read (thumbs
re-rendered + eyeballed). Dims/orientation/base-origin/WebP preserved; GLB-law clean (no Draco, tex
≤1024). Fleet **236k → 70k tris (70%)** across the district's multiple stages.
| instrument | tris (norm→14k) | footprint (m) | height |
|---|---|---|---|
| electric_guitar | 43.5k → 14k | 0.38×0.08 | 1.00 |
| bass_guitar | 36.0k → 14k | 0.34×0.13 | 1.15 |
| mic_stand | 34.1k → 14k | 0.57×0.57 | 1.70 |
| drum_kit | 26.2k → 14k | 1.50×1.24 | 1.30 |
| guitar_amp | 96.6k → 14k | 0.52×0.29 | 0.61 |
- **→ Lane D:** wire against the SAME filenames (`procity_fit_{electric_guitar,bass_guitar,mic_stand,
drum_kit,guitar_amp}_01.glb`) — footprints/heights in the manifest are **unchanged**, so your
`instrumentFor(role)` fittings are unaffected; they're just lighter now. (debt #2)
- High-poly originals preserved as `_hi` variants in `_normalized/` (git-ignored) for the depot.
### 4. Depot publish — DONE (John OK'd direct publish). Gate 3 flag CLEARED.
Published via the passwordless tailnet ingress (`GOD3_DEPOT=http://100.94.195.115:8788` — this box is
allowlisted, so no secret handling). The 5 base names now serve the **14k bytes** and the 5 `_hi`
hi-poly originals are live — verified on the public `digalot.fyi` depot (amp base 5.9MB→1.3MB, `_hi`
5.9MB live, byte-identical to local). `_published.json` 33→38 (the 5 `_hi` recorded).
`validate_manifest --depot` GREEN. **The 70% decimation win is now live in production**, not just
?localdepot. (The `vintage-cash-register.glb` drift note from publish.py is pre-existing + unrelated —
a depot-reuse asset, not a Lane E file.)
## Round 9 — VRAM audit verdict + one rec for C
**VERDICT: fine to ship v2.0.** Texture VRAM ~240 MB default boot / ~420 MB worst-case (all flags +
all shop types visited) — comfortable on desktop, within a low-end ~512 MB1 GB budget. Full numbers
in `AUDIT.md`. **Cheap win taken:** book + toy stock atlases half-res'd (they're quick pull-cards, not
the deep record dig — covers verified legible), stock VRAM **323 → 181 MB**, no index/UV change, atlases
re-published. Depot/manifest hygiene all green (`validate --depot` 0 err, pack-QA 3/3, 0 orphans).
**→ Lane C (optional, NOT blocking v2.0 — v3 follow-up):** `stockpack.js` caches atlas textures with
no dispose, so stock VRAM accretes across shop types (record+book+toy → 181 MB resident). A small
**cache-cap** (dispose non-current shop-type atlases on room-type change) would bound it to the
current type (≤134 MB). Not needed for the tag; flagging for when you next touch stockpack.
**→ v3 (bigger, flagged): KTX2/Basis GPU-compressed atlases** — 48× VRAM cut, no quality loss.
**→ Lane B (tram, on-call):** if you want a tram asset, a **box primitive with the right silhouette**
(long low body + roof + windows strip) beats a heavy model and is budget-free — do that first; ping me
only if it truly reads wrong and you want a flux_local/gen pass.
## Round 8 — REAL packs live (update)
**→ Lane C: all 3 stock packs are now REAL** (GODVERSE data, John unblocked DB access). Same schema
you wired — just richer data, no code change needed:
- **record** — 350 real vinyl (John's store stock, real Discogs covers), 6 atlases, cell 256×256.
- **book** — 311 real book covers (`dealgod.products` books), 4 atlases, cell **176×256 (portrait
face-out cover**, not thin spine — I only have cover art; render books face-out).
- **toy** — 273 real toy/collectible photos, 5 atlases, cell 256×256.
Metadata is parody-transformed (no real titles/artists/brands); covers are real art; book prices are
seeded (source is info-only). Indexes committed to `pipeline/packs/` + staged (`assets/models/`, your
`?localdepot` path); atlases published to the depot. All pass the pack-QA gate. If your book shelf
wants thin spines, tell me — I'd need spine art (don't have it; covers only).
## Round 8 (synthetic samples — superseded by REAL above)
**→ Lane C (E1 — book + toy packs staged for you):** `build_stock_pack.py` is generalized over kinds;
book/toy **sample packs are staged** (`web/assets/models/stock_{book,toy}_{index.json,atlas_00.webp}`,
`?localdepot`) + indexes committed to `pipeline/packs/`. Same schema you proved for records — wire
book/toy shelves through the stockAdapter, fail-soft per kind. **Cell geometry:** record 256×256
(square), **book 96×256 (tall-thin spine)**, toy 256×256 (box). You read `uv` (I confirmed from your
notes), so render each kind on its own shelf geometry; the index also carries `cell_w`/`cell_h` if
useful. `artist` carries author (book) / brand (toy). These are **synthetic placeholders** (real index
format) until the DSN lands — your fail-soft + wiring are unaffected when real covers swap in.
**E2 — pack-QA gate is live** (`validate_pack.py` in qa gate 3 via validate_manifest): a bad bake
(bad UV/price-band/empty title/dangling atlas) fails qa. If you add index fields, tell me and I'll
extend `REQUIRED`.
**→ Fable (blocker, flagged early per the slip rule):** the **real** book/toy/record packs need
**`GODVERSE_DSN`** (dealgod Postgres — up on :5432; builder reads it from env, I don't hunt creds).
One command per kind once set (`--kind book --from-db --count 200`). Pipeline + parody + atlas + QA
gate all proven on samples; not blocking C (samples staged). No fal spend.
## Round 7
**→ Lane D (D1 — the flip gate): merged peds ready to validate.** `web/models/peds/*.glb` (19 rigs)
are merged to **ONE draw each** (1 primitive + 1 atlased material) — fleet **92→19 draws**, at the
24-rig near cap **116→24**. Method: Blender join + UV-pack + EMIT-bake all materials into one 1024
atlas (`pipeline/merge_ped.py`). **Skinning + skeleton preserved** — 65 joints intact on all 19,
`walk`/`idle` untouched, so clips retarget unchanged. I verified in the vendored GLTFLoader: all 19
load as SkinnedMesh + bind walk; `man_suit` poses to byte-identical dims vs the original (skin
deforms unchanged); `man_athletic` (3-mat) atlas eyeballed correct. Please do the full pass
(silhouettes/materials/impostor bake/identity variety) + the **full-density draws/tris numbers** for
F's flip. Pre-merge peds backed up (gitignored) at `pipeline/.peds_r6/` if you need a diff; per-ped
in `pipeline/_peds_merge.json`. **If any ped's atlas/silhouette reads wrong, name it — I re-run that
one** (per-ped `atlas` size is tunable; a couple with dense detail could go 2048).
**→ Lane C (E2 — stock pack): pipeline built + PROVEN, sample staged for you now.**
`pipeline/build_stock_pack.py` produces the pack (parody-transform → atlas WebPs + JSON index). A
**24-sleeve sample pack is staged at `web/assets/models/stock_record_{index.json,atlas_00.webp}`**
wire `?stock=real` against it NOW (real index format, synthetic covers), and the real covers drop in
unchanged. **Proposed index schema** (I built to this — confirm/adjust in LANE_C_NOTES and I'll match
`INDEX_FIELDS`):
```json
{ "version":1, "atlas_px":2048, "cell":256, "atlases":["stock_record_atlas_00.webp"],
"items":[ {"id":"rec_0000","title":"Neon Suburbs","artist":"Trev Wollemi","price":39,
"price_band":"collector","atlas":"stock_record_atlas_00.webp","uv":[0,0,0.125,0.125]} ] }
```
`uv` = [u0,v0,u1,v1] into the named atlas (origin top-left); price_band ∈ bargain/standard/collector/grail.
Seeded pick per bin is yours (same shop.seed → same crate). Fail-soft: pack missing → parody canvas.
**→ Fable (E2 blockers — flagging mid-round per your schedule-harness note):** the real 200-400 pack
needs two things I can't self-serve: (1) **`GODVERSE_DSN`** — the dealgod Postgres dsn/creds (Postgres
is up on :5432; I read it from env, don't hunt creds) → `--from-db --count 300`; (2) **C's schema**
confirmation. Both external. The pipeline + parody law + atlas are done and proven on `--sample`; the
real pack builds in one command once the DSN lands. Not blocking C's consumer wiring (sample staged).
## Round 6
**E1 done**`publish.py` now merges into `_published.json` (no more R5 clobber); `--verify` +
post-publish drift-check vs the live depot; `validate_manifest.py` fails if a manifest GLB isn't in
`_published.json`. Record verified 27==depot. All 27 procity GLBs stay live + recorded.
**→ Lane B / Lane C — I'm on-call for the props I shipped:**
- **B:** if `bus_shelter` / `bin` need a scale, origin, or facing tweak once placed on the street
graph, tell me the exact issue (current: bus_shelter 2.33×1.41 m / 2.4 m, bin 0.57×0.68 m / 1.0 m,
both base-origin minY=0). I can re-normalize + re-publish same-name (idempotent, merge-safe now).
- **C:** if `glass_case` / `magazine_rack` / `crate_stack` read wrong in-room (scale, the fal
glass_case's interior clutter, the magazine_rack density), flag it. A *broken*-prop fal re-gen is
pre-authorized ≤2 attempts (decision #5) — I'll log any spend in AUDIT. Footprints in the manifest;
all base-origin. Mapping guidance for the 3 orphans is in the Round-5 → Lane C section below.
## Round 5
### → Lane C: hero props BAKED to ≤8k tris — re-measure the record interior
The 6 heavy TRELLIS props are re-baked and **live on the depot** (same filenames, footprints
unchanged, so **no re-map needed** — they upgrade in place):
| prop | tris before → after |
|---|---|
| drinks_fridge | 77k → 8.0k |
| milkshake_mixer | 73k → 8.0k |
| listening_booth | 54k → 8.0k |
| counter_till | 32k → 8.0k |
| novelty_record | 26k → 8.0k |
| arcade_cabinet | 25k → 8.0k |
**Fleet 288k → 48k tris.** Method: voxel-remesh → decimate → Cycles base-colour bake (see
`pipeline/bake_lowpoly.py`, AUDIT R5). Verified loading in the vendored GLTFLoader; base colour
baked (no separate metallic/normal maps → also dodges the record_crate dangling-texture class).
**Please re-measure the record interior** (was ~273k tris in your R4 note) and record the new
number in LANE_C_GLB_VALIDATION.md — it should drop by ~most of the prop weight.
### → Lane C: 3 new fittings LIVE (fal Hunyuan) — glass_case + magazine_rack
The two remaining primitive-only fittings are now real GLBs on the depot (≤8k, base-colour baked):
- **`glass_case`** (`procity_fit_glass_case_01.glb`, 1.08×1.42 m, 1.0 m tall) — timber base + glass
panels + items inside. Registry ids `glass_case` (toy/dept) + `glass_cabinets` (pawn) both alias
to it. → add `glass_case`/`glassCase` (+ your pawn cabinet kind) → `'glass_case'` in `KIND_TO_GLB`.
- **`magazine_rack`** (`procity_fit_magazine_rack_01.glb`, 0.54×0.44 m, 1.2 m tall) — tiered timber
rack w/ magazines (milkbar/pawn). Registry `magazine_rack` aliases to it. → map your kind → `'magazine_rack'`.
- Both base-origin, verified loading in the vendored GLTFLoader. `manifest.fittingAliases` also maps
`fridge`→`drinks_fridge` and `listening_corner`→`listening_booth` if you iterate registry ids.
### → Lane B: new street props — `bus_shelter` + `bin`
- `procity_street_bus_shelter_01.glb` (`furniture.bus_shelter`, 2.33×1.41 m, 2.4 m, ≤8k, base-origin).
Solid painted-steel shelter (back wall + roof + bench) — place at bus-stop spots along the footpath.
- `procity_street_bin_01.glb` (`furniture.bin`, 0.57×0.68 m, 1.0 m, 3k) — red council wheelie bin, an
obvious town-furniture gap. Map `bin` in your furniture GLB-upgrade path if you want it on footpaths.
- **B3 resolved:** `novelty_record` is now **8k** (was 26.5k) via the E1 bake — re-measure the busy view.
All furniture GLBs base-origin + footprint-accurate; fail-soft as usual.
### → Lane C also: `crate_stack` for floor piles / dig
`procity_fit_crate_stack_01.glb` (`fittings.crate_stack`, 0.71×0.72 m, 0.9 m, 3k) — a stack of 3
plastic milk crates, good for record-store floor dressing or the dig scene. Optional; map a kind to
`'crate_stack'` if useful.
### → Fable: E1 (bake) + E3 (fal) status
- **E1 bake DONE + committed** — 6 props ≤8k, published. Feeds C's re-measure + your v1.1 tag.
- **E3 fal DONE** — glass_case + bus_shelter + magazine_rack generated on fal Hunyuan v2, **~$0.64
total** (log in AUDIT R5), ≤2 attempts each, all baked ≤8k + published. Guardrail held (nothing
else on fal). `validate --depot` 0 err. Last two primitive-only fittings retired.
- **E2 depot growth** next (v2-serving library props).
---
## Round 4
## → Lane D (task D2): decimated peds ready to validate
The 19 ped rigs in `web/models/peds/` are decimated and staged (committed `518678d`, ref
`lane-e/round4-peds`). Please validate rig binding + animation:
- **≤3k tris each** (range 15642805; per-ped before/after in `pipeline/_peds_decim.json`).
Fleet total **924k → 46.5k tris** (48.6k → 2.45k avg). At the 24-rig near cap ≈ **≤67k**.
- **Skinning preserved**: `JOINTS_0`/`WEIGHTS_0` intact; **skeleton unchanged** (nodes + 65
joints identical to source on every ped I checked), so the shared `walk.glb`/`idle.glb`
clips retarget unchanged. **`walk.glb`/`idle.glb` were NOT touched.**
- Method: `gltf-transform weld``simplify --ratio 0.05 --error 0.12` (meshoptimizer — works
because these are manifold skinned meshes, not R3 TRELLIS shell-soup). Contact sheet:
`docs/shots/laneE/peds_decimated.png` (silhouettes/textures clean at street distance; fingers
merge + faces flatten as expected at 3k, no melting).
- Originals backed up (gitignored) at `pipeline/.peds_orig/` if you need a before/after diff.
If any rig fails to bind/animate or a silhouette reads badly, tell me the file(s) and I'll
re-run that one with a gentler error (0.08 keeps ~50% more tris but still <5k). Then F re-measures
gate 3 after your sign-off.
## → Lane C: E2 asset re-exports
1. **`procity_fit_record_crate_01.glb` FIXED.** Root cause of your `undefined 'uri'` crash: the
material's `metallicRoughnessTexture` + `occlusionTexture` both pointed at a **sourceless
texture** (the packed AO/roughness map failed to encode to WebP during normalize; base-colour
+ normal were fine). Fix: `pipeline/fix_glb_textures.py` strips material slots that reference a
sourceless texture (orphaning it three.js loads textures lazily, so no crash). **Verified it
loads in the real vendored `GLTFLoader`** (meshes=1, tris=5000, textures ok) alongside a
bookshelf control. Re-published to the depot re-map `record_crate`/`recordBin` in `glb.js`
(they should already be mapped) and it'll upgrade instead of falling back.
2. **counter-with-till — LIVE.** New fitting `counter_till` (`procity_fit_counter_till_01.glb`):
**1.61 × 0.64 m footprint, 1.15 m tall**, timber counter + cabinet doors + period beige till on
top (flux_local concept trellis_mac, on-device). Fixes the R3 4 m till-less counter that
squashed. **Verified it loads in the vendored `GLTFLoader`** (32k tris TRELLIS shell-soup floor,
within decision-#2's 2577k for focal props) and **live on the depot** (`validate --depot` 0 err).
**In `glb.js` re-add `counter` to `KIND_TO_GLB` mapping to `'counter_till'`** (NOT the old
`counter` = balcao, which is a 4 m long bar counter kept for shops that want it). The manifest
`fittingAliases.counter` `counter_till` too. It's base-origin (minY=0), so your footprint-fit +
floor-plant works unchanged; at a 1.42.0 m counter slot it scales to a believable ~1.01.4 m
tall counter.
## → Fable: status
- **E1 (gate-3 critical path): DONE.** Peds 3k, committed + pinned. Unblocks D2 F3.
- **E2a record_crate: DONE + verified** (loads in vendored GLTFLoader). Re-published.
- **E2b counter: in progress** (local, on-device no fal spend per decision #3).
- Not doing: fal.ai spend (glass-case/bus-shelter stay parked), hero-prop bake-to-lowpoly.