PROCITY/docs/LANES/LANE_E_NOTES.md
m3ultra 8a383e494e Lane E R21 (v4.0): the town pack — 22 towns, 1192 shops (DATA-ONLY, non-gating)
Ledger #5 pull-forward. Nothing here touches a golden until A absorbs in R22; F tags v4.0-beta with
or without it. selfcheck ALL GREEN 154141/154141, synthetic 0x3fa36874 unmoved, all 22 byte-identical
on re-run.

- Curated 18 candidates on secondhand density first (heroes are the game), a walkable 2-3.4km centre,
  and a national spread; 17 survived -> 22 towns, 1192 shops, 298 heroes, all 8 states/territories.
  fitzroy is the mecca (40 heroes/160 shops, 2x katoomba); thin tail toowoomba/darwin (3 heroes) left
  in as honest data -- A/Fable may drop them in R22.
- FINDING: the 3x-hero cap is a CURATION FILTER, not just a density knob. Wollongong had ~62 real
  shops but 1 hero -> capped to 4 -> dropped by the >=6 floor. A town without a secondhand strip
  cannot inflate into a retail strip; John's SUBTLE directive does quality control for free.
- Newcastle: my centre was wrong, not the town (CBD held 2 heroes; the strip is Hamilton/Beaumont St).
  Re-centred: 2 -> 9 heroes, 8 -> 36 shops.
- BUG (mine, shipped R18, caught here): fetch_roads_raw had no query-comparison while fetch_raw did,
  so re-centring refetched shops but silently reused the OLD CBD roads -> Hamilton shops seated against
  a graph 2.5km away -> the town marched out with ZERO shops (surfacing as 32 bogus 'open-late
  invariant' failures; ten other no-video towns passing is what proved it wasn't the open-late rule).
  Fixed: the roads path now compares its query too -- the bbox lives in the query, so a re-centre must
  invalidate BOTH snapshots. Newcastle rebuilt: 34 shops seated, openLate=1.
- Discipline: bounded bboxes, raw snapshots committed (shops+roads), C's §6 mapping, SUBTLE guardrails
  (heroes never subsampled, texture 3x, 2392 drops counted), ODbL. Overpass rate-limits hard at pack
  scale (batch 1 died on a fatal 504 after 25 retries) -- courtesy sleep 4s, two polite batches.

-> Lane A (R22): 34 goldens to pin (17 new towns x base+gig); sizes 12->160 shops load-test your
   capacity/overflow fences. -> Lane B: index.json now lists 22 towns (your selector already reads it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:06:46 +10:00

579 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LANE E — cross-lane notes
## 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) |
| Town caches **v3.2⟩** | 5 real AU towns (Katoomba/Newtown/Fremantle/Bendigo/Castlemaine) | **OpenStreetMap** via Overpass API | `build_towns.py` (fetch dedupe/suburb/parody/type-map) | 🟢 **ODbL 1.0** OSM contributors; `web/assets/towns/SOURCES.md`) |
**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).
### 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.