PROCITY/docs/LANES/LANE_E_NOTES.md
m3ultra 84e0a1b764 Lane E R23 (v5.0-alpha): the atlas validator + toowoomba retired (ledger #4)
1. validate_atlas (the G2a format gate). pipeline/validate_atlas.py, wired into validate_manifest so it
   runs in qa gate 3. Judges Lane G's tier-1 atlases from the COMMITTED FILES ALONE -- no dealgod DB, no
   network. No atlases yet => clean pass, so it's in place before G's lands. Enforces: provenance
   {license, attribution, generator, snapshot} (charter law #3 + determinism); NO private-individual item
   fields (law #3); 1 atlas/shop hard cap 2 (C §7.1); <=1024², 2048² only for a 60+ item shop (C §7.2);
   declared atlas_px == the atlas's REAL pixels + every listed atlas exists; schema/UV/bands/dup-ids
   (C §7.3). The licence string is PRINTED, not pattern-matched -- clearing photos for public release is
   a human call, so the gate surfaces it. Dependency-free PNG/WebP header parser. Tested against a
   10-case matrix (real 2048² WebP + 1024² PNG): good passes clean, all 9 violations caught.

2. toowoomba RETIRED (my call, counted). Tried the re-bbox FIRST: 3.0km CBD-wide -> 1.6km on the Ruthven
   St heart. It fixed the named metric (median NN 395.7 -> 89.4m raw, 'passes') but made the real thing
   WORSE: hub density 3/12 -> 2/12 within 160m, the pack's worst (D's alive-darwin is 7/12). Its shops
   are PAIRS strung along a road, not a high street; densest 300m cluster is 3 shops (< MIN_TOWN_SHOPS),
   so no tighter box exists. Shipping it would have gamed the number while leaving a town nobody shops in
   (D: 0 finds / 1417 checks). Config entry kept commented with the rationale. Pack: 21 real + 1 godverse
   = 22 rostered, 1180 shops, all 8 states/territories (QLD keeps westend). selfcheck ALL GREEN 152015.

-> Lane A, before you freeze the spacing warn (#5): (a) your validator sees the CACHE (raw lat/lon) but
   D's numbers are SEATED (post-lift) -- darwin is 27m seated vs 68.3m RAW, so a threshold picked off D's
   table would FALSE-WARN darwin; calibrate on the raw spread (daylesford 11.6 ... braddon 119.2,
   ballarat 254.6 -- a ~200m line has daylight). (b) median NN alone is GAMEABLE: my re-bbox improved it
   4.4x while the town got worse -- the hub fraction is the discriminator, and my raw hub metric
   reproduces D's darwin exactly (7/12), so it's directly encodable (alive ~38-75%, dead <=20%).
   (c) ballarat is a SECOND spread town D's two-town comparison missed (NN 254.6m, hub 4/20 = 20%) -- it
   will trip your warn, correctly. Not retired: the ledger named toowoomba only.

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

56 KiB
Raw Blame History

LANE E — cross-lane notes

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 wideningroad 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 identicalmanifest.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 roadsfetch_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/gamesvideo). It was already shipping as video pre-widening, so I kept video_gamesvideo 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 ODbLSOURCES.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 GREENvalidate_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.3→0.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 normalizedecimate_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 pack ⟨v4.0-FINAL⟩ 22 real AU towns, all 8 states/territories — 1192 shops + 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 town (Lane G's, listed for completeness) newtown_godverse — 18 shops, rostered in E's index.json GODVERSE/thriftgod shop census + OSM road geometry pipeline/godverse_town.py (Lane G owns this provenance) 🟢 ODbL 1.0 + GODVERSE census (G's own license/attribution fields)

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-livegig-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-livefails 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):

{ "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 donepublish.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 fridgedrinks_fridge and listening_cornerlistening_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 weldsimplify --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.countercounter_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.