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

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

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

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

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

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

91 KiB
Raw Permalink Blame History

LANE E — cross-lane notes

Round 28 — Spike 1: half of it shipped, half of it doesn't exist ⟨v5.1⟩

1. look.glb — DONE, published, verified. Looking_Around.fbx (675 KB, from the cached tailnet stash, not re-downloaded — the skill's law) → fbx_to_clip.pyweb/models/peds/look.glb: mesh-free, 65 mixamorig joints, 195 tracks, inverseBindMatrices present, 136 KB — structurally identical to the sit.glb precedent on every field I can measure (meshes/nodes/skins/joints/bind), so D's _canon binds it exactly like walk/idle/sit. Depot: published to digalot.fyi/3god under John's R22 standing authorization for Lane E assets (_published.json 43 → 44). → D: it's ready.

2. 🚩 lean DOES NOT EXIST — the spike's premise is half wrong, and only John can unblock it. The brief and the kickoff both say "two of the 34 already-cached clips (lean, look-around)" and "Spike 1 needs nothing from anyone". look-around is cached. lean is not — anywhere. Measured, not assumed:

  • The stash's 34 clips contain no lean (Agreeing · Bored · Breathing_Idle · Counting · … · Looking_Around · … · Yawn).
  • find ~/Documents -iname "*lean*" on ultra: nothing — not in mixamo-fetch, not in character_kit, not in 3D=models, no FBX by that name at any depth.
  • The fetch tool can't help unattended: node fetch.cjs search "lean"FATAL nothing on debug port 9222 — quit Brave and relaunch with --remote-debugging-port=9222. It needs John's logged-in Brave session (his browser, his Mixamo account) — I won't relaunch it.

→ John, one small unblocker (adds to your two): relaunch Brave with --remote-debugging-port=9222, open a Mixamo tab, and either run node fetch.cjs search "lean" yourself or tell me and I'll fetch + convert it in minutes (the path is proven twice now). I did NOT substitute a lookalike. Bored is the nearest stash clip and it's a standing weight-shift idle, not a wall-lean — D's half explicitly needs shopfront adjacency ("lean against a wall wants the same care" as the scale-aware traps), so a standing idle would quietly change what Spike 1 proves. Half a spike, honestly, beats a whole one that lies.

3. Instrument LOD — D killed it, and D's measurement is better than mine. I'd built the five _lo variants in scratch (14k → 3500/3500/4000/6000/3000) before D's commit landed saying "E: STOP — do not build the _lo variants." Nothing shipped; scratch discarded. D measured the rooms an LOD would have to fire in — max diagonal 15.2 m in the largest venue, so the player can never be far enough: practical win 0 tris. That's decisive in a way my argument wasn't. My own reading agreed on the verdict by a different route, and it's worth recording as corroboration: C's §4 already says "the interior gate is draws; tris are the street budget (200k)" — and an LOD swap doesn't change draws at all (same object, same material, fewer tris). So the item's entire win landed on a metric no interior gate reads. Two independent measurements, one verdict. The oldest tri item is dead, correctly.

(One salvage worth keeping if it ever returns: at the brief's ~3k the guitar_amp shreds — white gaps punched through its silhouette, confirmed against the shipped 14k thumb as a decimation artifact and not a source defect, the R22 food_cart discipline. It needs 6k to read clean. The other four were clean at 34k.)

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

5. Gates. selfcheck ALL GREEN 156,352/156,352 · qa.sh --strict 6 passed / 0 failed. look.glb moves no golden — proved by moving it aside and re-running: fingerprint identical either way, because an unreferenced clip is never fetched. The ?classic=1 covenant is untouched: like sit.glb, a raw fetch would breach the zero-fetch-delta rule, so D must gate look behind an opt the way rigs.js gates sit (loadPedFleet(base, { sit }) → the same shape for look). Depot-live verified by fetching the bytes back: https://digalot.fyi/3god/a/look.glbHTTP 200, 139,640 bytes, sha1 identical to local.

Round 27 — provenance v5-final, and the roster gap the docs freeze caught ⟨v5.0⟩

1. THE BUG THE FREEZE FOUND: redhill_godverse was on disk but NOT in the roster — for three rounds. The v5 provenance table needed real numbers, so I measured instead of carrying the v4 row forward. The count didn't match: 23 caches on disk, 22 rostered. The missing one was redhill_godverse — the town the entire v5 epoch walks to.

Mine, squarely. index.json is Lane E's file (write_index()). G's redhill_godverse landed in R24 (86e2985) after my last index run (5815dfb), and nobody re-rostered it. An addition isn't done when the cache is written — it's done when the roster is regenerated. That's the exact mirror of my own R24 lesson ("a retirement isn't done when the cache is deleted; it's done when the consumers are swept") — I wrote the lesson for deletions and then missed its reflection for additions.

Why four green rounds never caught it — worth reading, because it's a gate-shape lesson. Nothing was broken for the gates: selfcheck enumerates web/assets/towns/*.json directly (so it saw the town and A even pinned its goldens), and F's crate smoke boots by key (?town=redhill_godverse). The roster's only consumer is B's HUD selector (hud.js, my R20 ledger #2 design). So the single symptom was that a player could not choose the headline town from the dropdown — invisible to every automated check by construction. A gate that reads around the artifact under test cannot test it: the same species as the vacuous-gate law, one layer out. Fixed: re-ran write_index()23 rostered, 23 on disk, 0 missing; only index.json moved (every cache byte-identical again); selfcheck ALL GREEN 156,212, golden 0x3fa36874 untouched; qa.sh --strict --matrix 7/0/0.

→ F: this matters for #5, the tour. The money shot walks Musgrave Road into Monster Robot Party. If any tour frame reaches that town through B's selector rather than by key, it could not have found it before this commit — by key it always worked. Worth one glance at your framing path. → B: the dropdown now carries both godverse towns. Your ${t.state ? ', ' + t.state : ''} handles their empty state gracefully — no change needed, and the defensive write is why this cost nothing cosmetically. → G: your godverse caches carry no state, so they roster as state:''. Harmless today (B degrades cleanly). If you want "Red Hill, QLD" in the picker, emit state in the cache — my write_index() already prefers the cache's own field, so it needs nothing from me.

2. Provenance v5-final (ledger #6). The ⟨v5.0-FINAL⟩ rows are measured, not inherited: the town pack row was still claiming v4's "22 towns / 1192 shops" — stale since my R24 ballarat retirement and redhill build (now 21 real / 1196 / 299 heroes, plus both godverse towns = 23 rostered). Added the two v5 art rows: the real-stock atlas (1 crate, 120 items, Monster Robot's own POS) and the mint atlases (14 crates, 224 items, seeded from real dealgod listings). These are the table's first 🟡 rows — everything before v5 was 🟢 web-ok; real product photography is the first asset class this project ships that a human must clear before public release.

3. The licence line's story, written down as a mechanism (ledger #6). The 🟡 rows are guarded by exactly one printed line, so the freeze records why it took two rounds to make honest: R23's glob matched 0 files and returned 0; R25's permissive or fallback let the print go silent while the check still passed — the flag stopped reaching a human on real product photos while the gate said OK. Now: one spelling, one lookup with one owner, and the line is counted (judged N ⇒ N lines, or FAIL). Both zones print — mint carries the same in-house-green flag, because minting changes whose stock a crate claims to be, never whose photograph it is. Frozen today at 15 crates, 15 flags, every one in front of a human.

Round 26 wave 4 — the mint-id fence: my check was faithful, the rule was stale ⟨v5.0-beta⟩

The red was mine and the fix is C's ruling. My R25 slot-id check demanded sku_ on every real-stock pack — faithfully implementing C's R25 line, which was written before mint existed. It then failed all 14 of G's mint crates, whose ids honour its every intent: non-positional, seeded, stable, zero reuse. C's ruling names it exactly: "the rule was the stale thing, not E's check." Fourth hold of the epoch, and the fourth to trace to a rule meeting a case written before the case existed.

The fix is a fence, and it is STRONGER than the rule it replaces — that was the thing to prove. The easy green was to drop the prefix check. Instead the form is now sourcing-scoped per C §7.2a (R26 w4): sourcing:"real"sku_ (a POS copy on a shelf) · sourcing:"mint"mint_ (a dealgod listing standing in for one). A crate wearing the other's prefix FAILS.

Why this is a money bug and not a naming bug — G's judgment, now law. G refused to file dealgod listing ids under sku_ to go green the easy way. They were right: tier-2's sold-means-gone looks up POS skus. R24's id collision entered the wrong shop; the same collision in the money layer sells a real record out of a real shop to satisfy a minted stand-in. The gate's error says that, in those words — the cost, not the rule.

Proved 7/7 — that the fence bites, not merely that the red stopped. G's real crate (sku_…) → PASS · G's mint crate (mint_…) → PASS · mint filed under sku_ (the exact shortcut G refused) → FAIL, naming the fence and the real cost · real wearing mint_ (the reverse) → FAIL · positional ids still FAIL in BOTH sourcings (the old rule's whole intent, retained) · duplicate ids → FAIL. Loosening would have passed the first two and lost the last four; this passes the first two and keeps all four.

Both older suites brought forward rather than left to rot. The R26 suite's mint fixture wore sku_ ids, so the fence correctly rejected it — a fixture that fails for a new reason has stopped testing what it was written for. Moved to the mint_ space: 17/17. R25 licence suite: 8/8, tally still firing via the tally. This is the third round running where the near-miss was a test going quietly stale around a changing contract; it is the failure mode I now look for first.

Gates: atlas-QA OK — 15 per-shop atlas(es) valid, 15 licence line(s) shown, 0 warning(s) (1 real + 14 mint, every one showing its flag to a human) · validate_manifest exit 0 · qa.sh --strict --matrix 7 passed / 0 failed / 0 warn — QA GREEN. F's 6/1/0 is now 7/0/0. → F: the beta is yours to tag.

Round 26 — the sourcing arm + the atlas manifest ⟨v5.0-beta⟩

1. The field is sourcing, NOT tier — and reading C's contract instead of the brief is the only reason this gate isn't already broken (ledger #3). The R26 brief says "the validator grows the tier arm… tier required on every atlas". C ruled it cannot be tier (§7.2b): tier is already the charter's ladder rung (0 parody / 1 static-real / 2 live) and a mint atlas is also rung 1 — same mechanism, different data origin. Overloading it would put two meanings in one name: the same species as the plan-id/godverse-id collision and the positional ids. Had I built the brief's word, my gate would have demanded tier while G emitted sourcingR23 exactly, re-run. The contract's author is the authority for the contract's shape. Built to C §7.2b.

2. The honesty separation, asserted both directions. sourcing ∈ {real, mint} required (vacuous-gate law: no sourcing ⇒ FAIL — a crate that can't say where it came from must never pass as one that can). Then: mint must carry no POS claim, real must carry the POS provenance it claims.

→ C: §7.2b says "a mint index MUST NOT carry POS-snapshot fields" but does not enumerate them, so I named the set — please correct it if it's wrong. I assert on crate, provenance.source.pos_db, provenance.source.pos_dump — the fields that claim a specific shop's actual inventory. I deliberately excluded snapshot: §7.3 requires it on every atlas as the determinism pin, and a mint bake has its own honest source snapshot (the dealgod dump it sampled). If a mint bake legitimately needs any field in my set, say so and the set changes. The error names the exact field found, so a divergence shows up as a diagnosis rather than a mystery.

3. The manifest (ledger #6), consistency both ways + one hole C's rule implies but doesn't spell out. stock_godverse/index.json per §7.2d. An atlas on disk absent from the manifest FAILS (no entry ⇒ no fetch ⇒ that crate silently never loads — an unreachable atlas is not a passing atlas); a manifest entry with no atlas FAILS (the wire fetches it and 404s — the exact noise the manifest exists to kill). C's "absent manifest ⇒ legitimate zero-shop state" is honoured: no atlases + no manifest ⇒ SKIP, not an error. But a manifest that PROMISES shops while zero atlases exist now FAILS rather than skipping — C's rule scopes "absent" to the empty tree, and a tree where something on disk still asserts atlases exist is the vacuous shape wearing a hat. Flagging it since it's my reading, not C's words.

4. Proved before handing G the gate — 17/17, including a mint crate that does not exist yet. The R25 rule (never hand a lane a gate that fails on compliance) applied: I hand-built G's likely R26 emit and a seeded mint index and ran them. Real → PASS · mint → PASS with its licence line printed (ledger #3's "prints for both" — verified, not assumed) · real+mint side by side → PASS · mint carrying crate → FAIL · mint carrying pos_db → FAIL · real carrying no POS provenance → FAIL (the reverse direction) · no sourcing → FAIL · sourcing:'realish' → FAIL · atlas absent from manifest → FAIL · manifest entry with no atlas → FAIL · declared type not on disk → FAIL · manifest says mint, index says real → FAIL · manifest missing while atlases exist → FAIL · unparseable manifest → FAIL · empty tree → SKIP · empty manifest → SKIP · manifest promising a shop with no atlases → FAIL.

5. The R25 suite was going stale and I brought it forward rather than let it rot. Adding sourcing + the manifest meant every R25 licence case started failing for a NEW reason — which would have left the licence arm untested while appearing tested. Updated to the R26 contract; 8/8 still green, and the tally tripwire still fires VIA THE TALLY (via_tally=True), not incidentally. Worth noting the near-miss: the manifest error briefly shadowed the tally in the harness, which is precisely how the tally died the first time — a test that fails for a new reason has stopped testing what it was written for.

6. The gate is RED until G lands, by design (2 errors: no sourcing, no manifest). Same as R25: G is in my wave, parallel. It goes green when G's re-emit + manifest land, with no change from me.

→ F: one thing before the alpha tag. There is still no v5.0-alpha tag, and C's + B's beta commits (4f0adf1, 2f4ab59) are now on main ahead of wave 0's close. Tagging HEAD would produce a v5.0-alpha whose tree contains beta work your green verdict never covered — the mirror of the reason you held three times. The alpha's tree is e7d58ea (wave 0 COMPLETE, my 9b1df5c beneath it). Your call and your tag; flagging it because "the tag must certify what was proven" cuts both ways.

Round 25 — the licence line, made unmissable + the sku form ⟨v5.0-alpha⟩

1. The bug was not a spelling — it was a DIVERGENCE, and the fallback was the culprit (ledger #2). The R24 gate printed OK — 0 warnings on a real shop's product photos while saying nothing about their licence. Two paths read one field with different spellings:

the CHECK:  lic = prov.get("licence") or prov.get("license")   ← permissive: passed either spelling
the PRINT:  lic = prov.get("licence")                          ← strict: British only, behind `if lic:`

While G's artifact was British they agreed. G's R24 keyed re-emit moved it to US license — the check still passed, the print silently resolved to {}, and FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE stopped reaching a human. The or fallback WAS the bug: it made one path permissive so it could not feel the other path break. This is my R21 stale-roads lesson one layer upa check applied to one of two symmetric paths is a trap with a fuse on it — and I wrote that lesson down and still shipped it again. Fixed structurally, not by adding the missing spelling:

  • One spelling, no fallback. British licence now ERRORS and names C's §7.3 retirement.
  • One lookup, one owner. validate_index returns the evidence string; main prints what it is handed and never re-looks-up the field. There is no second path left to drift.
  • The evidence line is COUNTED. Judged N atlases ⇒ N licence lines shown, or FAIL.

2. My own first fix had the same disease, and my own test passed for the WRONG REASON. I added the tally tripwire — then main() also appended its own missing-licence error, so all_errs was never empty when the tally would fire: the tally was unreachable dead code. The test went green through the other path and reported PASS. That is exactly the trap I recorded in R24 ("a gate that fails for the wrong reason is a gate that will pass for the wrong reason later") — one round later, on me, in the very file where I wrote it. The fix: the field check owns the precise error; the tally sits after the all_errs return and exists only for the day that check goes permissive again. Verified by stubbing validate_index permissive — it now fails via the tally (via_tally=True), not incidentally. Proved, 8/8: G's re-emit → PASS with the flag shown · license gone / empty → FAIL · British at top level → FAIL · British nested → FAIL · attribution / snapshot gone → FAIL · the R24 silent shape → FAIL · permissive-check simulation → FAIL via the tally.

3. C §7.2a — the sku form asserted, and I verified C's claim myself before building to it (ledger #3). Independently measured: 120/120 godverse ids also exist in the generic pack — F and C are exactly right, and an id-equality gate cannot discriminate while that holds. G's snapshot carries 120/120 unique skus, so the re-emit was feasible, and I proved the compliant shape passes before handing G the gate (4/4: sku_<POS sku> → PASS · positional → FAIL · sku-but-duplicated → FAIL · one id sneaking back into rec_ → FAIL at 1/120, so it can catch a real collision, not just an all-or-nothing one).

→ C: two things I did beyond your literal ask — say if you disagree.

  • Your ask was "id present, unique within the pack, sku_-prefixed". I also assert the actual property your rule exists to guarantee: the pack's ids are disjoint from the shipped generic packs, read from web/assets/models/stock_*_index.json. The prefix says ids should be disjoint; this measures whether they are, and it catches the reverse drift (a generic pack wandering into sku_) that a prefix check never could. If the generic packs are absent it warns rather than passing silently.
  • I flag British licence anywhere, including inside the nested provenance{} "colour" block. Your §7.3 retires the spelling and R25 is what a spelling fork costs, so I read the retirement as global. If you meant the block to stay G's business, say so and I'll scope it to the four gate fields.

4. The gate was RED by design for one wave, and that was the point. Between my commit-readiness and G's re-emit the collision was real and unfixed, so validate_atlas failed — deliberately. A gate that stayed green on a defect F had just found would be the vacuous species wearing a different hat. It went green the moment G landed 4314622, with no change from me.

Round 24 — the alpha completion: redhill, the honest validator, ballarat ⟨v5.0-alpha⟩

1. redhill_real — the crate's real street (ledger #3). BUILT. One bounded Overpass fetch under the standing authorization. Red Hill, Brisbane · centre -27.4553, 153.0064 · span 2.4 km · 36 shops (9 heroes + 27 texture, 29 subsampled) · 884 roads · median NN 73 m, hub 5/36. Musgrave Road is in roads[] — 41 ways, kinds [primary, residential]. That was the round's point: Monster Robot Party's real shop now has its real street. Raw snapshots in _raw/, ODbL, C's mapping, same discipline as the pack. → Lane G: this is your donor for godverse_town.py redhill_real redhill_godverse.

→ G AND A: TWO HAZARDS ON THIS DONOR. Please read before you seat the shop.

(a) The shop is NOT in OSM. There is no shop=music node and no "Monster Robot"/"monsterrobot" anywhere in the Red Hill bbox — I checked the raw snapshot, not just the emitted cache. It cannot arrive via my fetch; G must inject it from the census (id 3962749) into shops[]. If G assumes the donor already carries it, the town ships without the one shop the release is about.

(b) THE SEATING HAZARD — from G's geocode, the shop seats on the WRONG STREET. A's lift seats each shop on its nearest edge. Measured against the real roads[] geometry, from G's geocode -27.4553, 153.0064:

distance street kind
38.6 m Zig Zag Street residential ← A would seat it HERE
46.1 m Craig Street residential
51.6 m Musgrave Road primary ← the real address (147 Musgrave Rd)

Three streets are nearer than the addressed one, so Monster Robot Party lands on Zig Zag Street and the round's headline quietly becomes false. A street-address geocode sitting 52 m off its own centreline is itself the tell — the pin is roughly the block, not the shopfront.

The measured fix (E's recommendation, G's call): the point on the real Musgrave Road centreline nearest G's geocode is -27.455169, 153.006902. Seating there: 0 m to Musgrave Road, next candidate 43 m away — unambiguous, with margin. It moves the shop 51.6 m; the residual error along the street is G's geocode error and is below the game's resolution. Caveat, stated plainly: this snaps to the right street, it does not verify the house number — I have no address data to confirm where #147 sits along Musgrave Rd.

The robust fix (A's call, likely v5.x): a godverse shop knows its addressed street; A's lift could prefer a name-matching edge over the merely-nearest one. That fixes the whole class, not this one shop. The snap above is enough to ship the alpha honestly.

2. validate_atlas — made honest (ledger #4). F's catch was right; there were FOUR bugs, not one. The R23 gate could pass without ever touching its subject — the vacuous-gate law's exact species. Fixed:

# bug why it mattered
1 globbed the published-contract name (stock_shop_<id>_index.json) — a name the runtime can never load matched 0 files and returned 0. It passed identically whether atlases were absent, misnamed, or perfect
2 the code read provenance top-level while my own docstring said nested G nested it (correctly). The gate would have failed G's real, correct atlas
3 license vs licence same — a spelling fork silently failing a valid artifact
4 ceiling stale by one round drifted from C's §7

Per the R24 "the runtime is the authority" ruling the gate now globs the path the loader fetches: stock_godverse/<godverseShopId>/stock_<type>_index.json, type ∈ {record, book, toy}. The vacuous-gate law is implemented literally: 0 matches + stray filesFAIL, listing the unmatched paths; 0 matches + a truly empty diratlas-QA SKIP: no atlases present (0 files matched glob …), exit 0 — it skips loudly, with the glob printed, so a silent pass can't hide. It also asserts identity (shop.godverse_id == the directory name) — a falsifiable id equality, per the law's "prove it touched its subject".

Proved, not asserted — four cases: empty ⇒ SKIP (loud, exit 0) · misnamed (F's exact bug) ⇒ FAIL · unparseable ⇒ FAIL · G's real atlas ⇒ PASS: 3962749/stock_record_index.json — Monster Robot Party — items=120 atlases=2 px=2048 OK, licence printed, 0 warnings. My own test found a fifth hole: the glob still matched the old stock_shop_* name, so the misnamed case failed only incidentally — I added a STOCK_TYPES name check, and it now fails for the right reason. A gate that fails for the wrong reason is a gate that will pass for the wrong reason later.

→ Lane C: built to the runtime + G's real artifact per the ruling. LANE_C_PUB §7 line 271 still names the contradictory stock_shop_<godverseShopId>_index.json — when your corrected contract lands I'll reconcile; if it names anything other than the above, the gate and the loader disagree again.

3. Ballarat — RETIRED (my call, counted). A different illness from Toowoomba. Full rationale in SOURCES.md and the commented config entry. The short version, because the distinction matters: Toowoomba was dead everywhere. Ballarat is alive exactly where the game isn't. It has 103 POIs and a genuinely dense core (densest 400 m disc = 47 POIs; Toowoomba's was 3) — but that disc is cafés/clothes/bakeries, while the op-shops sit 854 m / 1042 m / 1709 m out. Heroes and density don't coincide, so no bbox holds both:

span shops heroes median NN hub ≤160 m op-shops
1.0 km (the live core) 8 2 184 m 4/8 (50%) 0 ← a thrift town with no thrift shop
2.0 km 12 3 186 m 3/12 (25%) 1
3.0 km (as shipped) 20 5 255 m 4/20 (20%) 2

Every row is worse than any shipped town (darwin 68 m, glebe 60 m, redhill 73 m, katoomba 22 m).

→ A, D: the binding constraint was MINE, not OSM's — and it's a real finding. With 2 heroes the 3× texture cap keeps 6 of 47 texture POIs, and the subsample is spatially blind (deterministic hash rank over osmId). So in any hero-poor/texture-rich town the survivors scatter no matter how tight the boxpart of the 255 m A's warn caught was my own cap's fingerprint, not Ballarat's. Seeding texture near heroes would fix the class, but it moves every town's golden → I'm filing it as a v5.x proposal, not doing it in a release round.

→ A: I OVERSTATED MY R23 ADVICE — please don't encode it as a threshold. I told you the hub fraction is the discriminator. It isn't, on its own: glebe — shipped and healthy — reads 7/52 (13%), and redhill reads 5/36 (14%). A hub threshold would false-flag both. What actually held up:

  • median NN is the robust primary (dead: toowoomba 396 m, ballarat 255 m; alive: 2277 m).
  • hub is only comparable at equal shop counts — toowoomba 2/12 vs darwin 7/12 was a fair comparison because both had 12 shops; hub% otherwise scales with town size and morphology.
  • The R23 calibration point still stands and still matters: your validateTownCache sees raw cache lat/lon; D's numbers are seated (post-lift) — darwin is 27 m seated but 68.3 m raw, so a threshold read off D's table will false-warn darwin.

4. A methodology note I'm recording because it nearly bit me. I simulated the re-bbox offline to pick Ballarat's span; the sim predicted 68 m / hub 5-8 and I had already written those numbers into a config comment as fact. The real build returned 184 m / 4-8same counts (2 heroes + 6 texture), different sample: my re-implementation of the hash subsample picked a dense six where the pipeline picks a spread six. Never trust a re-implementation of the pipeline's own selection — run the pipeline. The sim's numbers would have shipped as a claim about a town nobody would have re-measured.

5. Determinism receipt. The ballarat retirement forced a full 21-town rebuild from the cached raw snapshots (no refetches). Every existing cache came back byte-identical — only ballarat (deleted), redhill (new) and index.json moved. Pack now 21 real towns · 1196 shops · 299 heroes, 22 rostered with Lane G's newtown_godverse. Selfcheck ALL GREEN, 149,696 checks, synthetic golden 0x3fa36874 unchanged (the count moves with the roster: ballarat's 1144-edge graph out, redhill's 884 in).

6. → A: THE GOLDEN PINS ARE YOURS AND MY ROUND MOVED THEM (the batched pin, ledger law). Your own selfcheck printed the exact values — no round trip needed:

REAL_TOWN_GOLDENS['redhill_real']     = 0x6046700f      // ADD (new town, currently UNPINNED)
REAL_TOWN_GIG_GOLDENS['redhill_real'] = 0x673785ee      // ADD
REAL_TOWN_GOLDENS['ballarat_real']     = 0xf0a9e8a1     // REMOVE (cache deleted — dead config)
REAL_TOWN_GIG_GOLDENS['ballarat_real'] = 0xf097c331     // REMOVE

The ballarat pins are now exactly what your R23 note called the toowoomba pin: "a golden for a town that cannot load is dead config: it can never fire, so it can never fail — same species as the vacuous-gate law, one layer down." Your words, my retirement, same fix. And note the mirror on the other side: redhill_real currently warns (⚠ base UNPINNED) rather than failing, so the round's headline town is ungated until you pin it — an unpinned town is the same vacuous species pointing the other way. Your warn-with-the-hash design is what made this a two-line handoff instead of a bug hunt; worth keeping.

7. → F: an R23 orphan I created and owe you. tools/qa/tour_shots.py (your v4.0 release tour) still shoots plansrc=osm&town=toowoomba_real for its toowoomba_thintail frame — I deleted that cache in R23 and never swept the consumers, so re-running the v4 tour now breaks on a missing town. It's your file and it's a shipped historical artifact (the frame was the honest thin-tail proof), so I'm flagging rather than editing: either re-point the frame at a surviving thin town or retire it with a note. Same class, lower stakes: web/js/citygen/gigs.js:134 names ballarat/toowoomba in a comment as thin-town examples — stale prose only, no behaviour. The general lesson for me: a retirement isn't done when the cache is deleted; it's done when the consumers are swept.

Round 23 — v5.0-alpha: the atlas validator + the toowoomba call ⟨v5.0-alpha⟩

1. validate_atlas — the G2a format gate (ledger #4). pipeline/validate_atlas.py, wired into validate_manifest so it runs in qa gate 3. It judges G's tier-1 atlases from the committed files alone — no dealgod DB, no network (that's the point: the gate must run on m3). No atlases yet ⇒ clean pass, so it's in place before G's lands. → Lane G: this is the format you build to. It enforces:

rule source
provenance {license, attribution, generator, snapshot} present charter law #3 ("every atlas carries provenance") + snapshot pins determinism
no private-individual fields on items (seller/owner/email/phone/…) charter law #3 ("no private-individual data, ever")
1 atlas/shop, hard cap 2 LANE_C_PUB §7.1
≤1024²; 2048² only for a 60+ item shop LANE_C_PUB §7.2
declared atlas_px == the atlas's real pixels; every listed atlas exists index↔atlas consistency
schema + UV in range/not inverted, bands, dup ids, empty title/artist, atlas∈atlases[] §7.3 (schema confirmed verbatim)

The licence string is printed by the gate, not pattern-matched — clearing photos for public release is a human call (charter law #3), so the gate surfaces it rather than pretending to rule on it. Tested against a 10-case matrix (real 2048² WebP + 1024² PNG headers, dependency-free parser): good passes clean; all 9 violations caught. A gate that's never caught anything isn't a gate.

2. Toowoomba — RETIRED (my call, counted). I tried the re-bbox first; the data said retire. D's finding: DEAD — 0 patronage finds in 1417 checks. I re-bboxed 3.0 km CBD-wide → 1.6 km onto the Ruthven St heart. Result:

median NN (the named metric) hub ≤160 m (D's real test)
toowoomba @3.0 km 395.7 m 3/12
toowoomba @1.6 km (re-bbox) 89.4 m "passes" 2/12 (17%) — worst in the pack
darwin (D: ALIVE) 68.3 m 7/12 (58%)

The re-bbox fixed the metric and made the town worse. Toowoomba's shops are pairs strung along a road, not a high street — close-in-pairs flatters median NN while no hub exists. Its densest 300 m cluster is 3 shops (below MIN_TOWN_SHOPS), so no tighter box exists either. Shipping it would have gamed the number while leaving a town nobody shops in. Retired (config entry kept commented with the rationale so nobody re-adds it blindly). Pack: 21 real + 1 godverse = 22 rostered · 1180 shops · all 8 states/territories (QLD keeps westend, 11 heroes/44 shops). selfcheck ALL GREEN 152015/152015.

⚠️ → Lane A: two things before you freeze the spacing warn (#5)

  1. Your validator sees the CACHE (raw lat/lon); D's numbers are SEATED (post-lift). They differ: darwin — D 27 m seated vs 68.3 m raw; toowoomba 388 seated vs 395.7 raw (spread towns barely move, clustered ones move a lot). Calibrate off the raw spread, not D's table — a threshold picked near "darwin 27 = alive" would false-warn darwin at 68.3. Raw spread now: daylesford 11.6 · katoomba 22.4 · bowral 27.9 · … · launceston 76.8 · braddon 119.2 · ballarat 254.6. A ~200 m line has daylight on both sides.
  2. Median NN alone is gameable — the hub fraction is the discriminator. My re-bbox is the proof: NN improved 4.4× while the town got worse. Consider warning on hub density too (shops within 160 m of the densest shop). My raw hub metric reproduces D's darwin exactly (7/12), so it's directly encodable: alive ≈ 3875% (castlemaine/darwin/launceston/daylesford), dead ≈ ≤20% (ballarat 20%, retired-toowoomba 17%).
  3. ballarat is a second spread town D's two-town comparison never covered: NN 254.6 m, hub 4/20 (20%) — it will trip your warn (correctly). Not retired: the ledger named toowoomba only, and a warn is A's to raise, curation is mine to act on. Flagging for Fable's next-round call.

Round 22 — v4.0 THE EPOCH CLOSE: the tri diet (mine) · notes · provenance · the G2a review ⟨v4.0⟩

⚠️ Ledger #1b — the venue_night tri diet: MY four furniture GLBs. Fixed. → F: RE-MEASURE.

B's #1a diagnosis named it exactly right and I own it: 149,161 of 163,015 tris (91.5%) were four GLB street props of mine. Buildings 348, posters 222 (A's cap worked — never the lever), pools 0. B's sharpest point: it isn't the density 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)
Ped clip ⟨v5.1⟩ look.glb (E R28 — v6 Spike 1) — mesh-free, 65 mixamorig joints, 195 tracks, 136 KB; structurally identical to sit/walk/idle Adobe Mixamo Looking_Around.fbx (675 KB) — from the cached tailnet stash (johnking@ultra:~/Documents/mixamo-fetch/out/), not re-downloaded fbx_to_clip.py (Blender, mesh-strip) — the R16 path, unchanged 🟢 Mixamo royalty-free (motion only, mesh stripped) — same zone as walk/idle/sit
Ped clip ⟨v5.1⟩ NOT BUILT lean.glbthe source does not exist (see R28 §1)
Town pack ⟨v5.0-FINAL⟩ 21 real AU towns, all 8 states/territories1196 shops (299 heroes) + real street geometry (roads[]) OpenStreetMap via Overpass API — bounded per-town bbox; raw shops+roads snapshots committed in _raw/ build_towns.py: fetch → dedupe/suburb-fill/parody → C's §6 class map → 3× texture cap (drops counted) → v2 cache + roads 🟢 ODbL 1.0 (© OSM contributors; web/assets/towns/SOURCES.md + license/attribution on every cache)
Godverse towns (Lane G's, listed for completeness) ⟨v5.0-FINAL⟩ newtown_godverse (72 shops) · redhill_godverse (37) — the town whose record shop is THE shop. Both rostered in E's index.json (23 total) GODVERSE/thriftgod shop census + OSM road geometry; redhill_real is E's donor pipeline/godverse_town.py (Lane G owns this provenance) 🟢 ODbL 1.0 + GODVERSE census (G's own license/attribution fields)
Real-stock atlas ⟨v5.0-FINAL⟩ 1 crate, 120 items, 2×2048² webp — Monster Robot Party (3962749) the real POS (recordgod) — crate 550 "DEEP HOUSE"; covers are the shop's own product shots, cached by dealgod pipeline/godverse_stock.py (Lane G owns) — snapshot(DB-side, sha256-pinned) → pack(pure) 🟡 in-house-green — FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE · sourcing:"real" · ids sku_<POS id>
Mint atlases ⟨v5.0-FINAL⟩ 14 crates, 224 items, 14×1024² webp — keyed hero shops with no POS (5 record · 6 book · 3 toy across both godverse towns; the 6th record crate is the real one above) real dealgod listings (the sellers' own product shots) — a seeded sample, random.Random(shop_id), deterministic godverse_stock.py mint baker (Lane G owns) — same bake path, sampled source 🟡 in-house-green — same flag · sourcing:"mint" · ids mint_<listing id> · never claimed as that shop's stock

GLB fleet = 33 manifest (23 fit + 10 furniture) + 5 _hi = 38 published. Only external spend ever: ~$0.64 (fal-Hunyuan, R5). Everything else is $0 / on-device (electricity only).

⟨v5.0-FINAL⟩ The licence line's story — why one printed line is a provenance mechanism

The only 🟡 in this table is the v5 stock art, and the flag is the whole control. Charter law #3 says clearing photos for public release is a human call — so the gate does not pattern-match the licence and rule on it. It prints it, every run, for every atlas, and a human decides. That makes one printed line the mechanism the amber rows depend on, which is why it earned two rounds of work:

  • R23 — the gate globbed a name the runtime can never load: 0 files matched, returned 0. It passed identically whether atlases were absent, misnamed, or perfect. (The vacuous-gate law came from this.)
  • R25 — the line went silent and the gate still said OK. Not a typo: the check read licence or license (permissive) while the print read licence only. G's re-emit moved to US license; the check passed, the print resolved to {}, and FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE stopped reaching a human on a real shop's product photos. The fallback WAS the bug — it let one path stay permissive so it could not feel the other break.
  • The fix is structural, not a spelling. One spelling, no fallback (British licence now ERRORS, naming C's §7.3 retirement) · one lookup, one owner (validate_index returns the evidence string; main prints what it is handed and never re-looks-up the field — no second path left to drift) · and the line is COUNTED: judged N atlases ⇒ N licence lines shown, or FAIL. A gate that can say OK while hiding the flag cannot exist by construction.

Today, frozen: atlas-QA OK — 15 per-shop atlas(es) valid, 15 licence line(s) shown, 0 warning(s). Fifteen crates, fifteen flags, every one in front of a human. Both zones print — the mint rows carry the same in-house-green flag because a seeded sample of real dealgod listings is still the sellers' own product shots. Minting changes whose stock it claims to be, never whose photograph it is: that is why sourcing is honesty and the licence is unchanged by it.

⟨v5.0-FINAL⟩ The id-space fences — provenance carried in the id itself

Two disjoint namespaces, blessed as design (C §7.2a, R26 w4), asserted by validate_atlas: sku_<POS id> names a physical copy on Monster Robot's shelf · mint_<listing id> names a dealgod listing standing in for one. A crate wearing the other's prefix FAILS. This is not naming hygiene: tier-2's sold-means-gone looks up POS skus, so a mint_ id filed under sku_ doesn't fail a gate — it sells a real record out of a real shop to satisfy a minted stand-in. G refused that shortcut when it would have gone green the easy way, and that judgment is the fence. (Same standing note as plan-ids vs godverse-ids: one name, one space — never compare across.)

Audio inventory — 28 assets (web/assets/audio/, committed; contract = manifest.audio)

All procedural, seeded, $0, parody-safe; OGG/Opus + M4A/AAC; beds+music loudnorm 16 LUFS, SFX peak-norm.

group n assets gig?
ambience 7 street-day, street-night, rain, roomtone-{retail,milkbar,video}, crowd-walla crowd-walla gig:true
sfx 8 + 6 steps doorbell, door-open, till, riffle, toast, tram-bell, tram-rumble(loop), applause; footstep{pavement×3, timber×3} applause gig:true
music 7 record-shop, milkbar, video-synth, arcade, gig-pubrock, gig-grunge, gig-covers 3 gig beds gig:true
  • Gig beds (R13, the manifest key IS the gigKey gig-<genreKey>): gig-pubrock (pub, 0.6, 13.4s), gig-grunge (band_room, 0.6, 17.2s), gig-covers (rsl, 0.5, 19.2s). Seamless loops, non-silent (verified 15.7…17.6 dB mean). Old pubrock-live fully retired (files gone, no live refs).

GLB inventory — 38 (tri counts glb_stat-verified; footprints/heights match manifest; no Draco, tex ≤1024)

Fittings (23): record_crate 5k · wire_shelf 330 · clothes_rack 3.2k · bookshelf 5k · cube_shelf_wide 5k · counter 276 · coffee_table 495 · work_table 1.6k · box_crate 78 · crate_stack 3k · listening_booth 8k · drinks_fridge 8k · arcade_cabinet 8k · milkshake_mixer 8k · counter_till 8k (bake_lowpoly hero) · glass_case 8k · magazine_rack 8k (fal) · electric_guitar / bass_guitar / drum_kit / guitar_amp / mic_stand = 14k each (R13 decimate) · cash_register 6k (reuse). Furniture (10): bench 1.9k · bench_modern 640 · bench_wood 4k · longbench 1.2k · park_bench 5k · streetlight 5k · food_cart 5k · novelty_record 8k (bake_lowpoly) · bus_shelter 8k (fal) · bin 3k. Preserved _hi originals (5, on depot, NOT manifest-referenced — LOD/re-bake source): electric_guitar 43.5k · bass_guitar 36k · mic_stand 34.1k · drum_kit 26.2k · guitar_amp 96.6k.

Skins — 96 (web/assets/gen/*.jpg, local; all present, 0 missing/orphan)

facade 48 (43 blank + 5 baked-sign: djsim-video/milkbar/pub, market, grimy) · sky 10 · ground 10 · wall 8 · interior floor 6 · interior surface 7 · awning 3 · poster 4 (grunge/retro/screenprint/xerox, blank name-band). Per-shop-type facade coverage ≥2 for all types (validate_manifest asserts; min = 3).

Stock packs — 3 (indexes committed in pipeline/packs/; atlases on depot; read directly by Lane C)

record 350 (6 atlases, 256²) · book 311 (4 atlases, 176×256) · toy 273 (5 atlases, 256²). Real cover art (GODVERSE), parody-transformed metadata (sha256-seeded word-banks; no real trademarks). All pass validate_pack.

By-design notes (documented so a future reader doesn't re-flag them)

  • 5 _hi in _published.json but not the manifest — intentional archival hi-poly originals (R13).
  • Stock-pack indexes not in the manifest — by design; Lane C reads pipeline/packs/* directly, atlases ship on the depot.
  • glb_law tiers — ≤5k library / ≤8k baked hero (TRELLIS shell-soup, texture-intact) / ≤14k gig instruments; now stated in the manifest convention string + AUDIT.
  • validate_manifest prints "skins 85 groups" — a shallow sk.values() count (the nested interior dict counts as 1); true leaf total is 96. Cosmetic label, flagged for v3.1.
  • Minor stale ledger figures (non-live): food_cart 4991 vs _normalize_results 5000 (9-tri); a superseded cube_shelf_01 entry in _normalize_results (DROP-filtered from the manifest, rejected in AUDIT). Harmless.

Deferred to v3.1 (documented, not started)

  • street_bin decimation — B's non-blocking, conditional ask ("if the combined-lane budget tightens"); the bin is ~3k tris (within house law), and B measured ~100k tri / ~150 draw margin at the busiest block, so no action for v3.0.
  • Texture-bake-to-lowpoly / KTX2-Basis atlas pass — if a future budget tightens; the _hi instrument originals + the hero props are the LOD source (R9 VRAM note + AUDIT).

Round 13 — two genres, the gigKey rename (debt #1), lighter instruments

v3.0-beta THE DISTRICT. All local work landed + verified; the one gated step (depot publish) waits on John. Verification: core gates GREEN (scaffold · consistency 0-warn · citygen selfcheck 3273/3273 incl. v3 gig-layer + assets-on-disk), manifest validator --depot GREEN, and the real app boots clean flags-off / ?gigs=1 (A's 3-venue district, 14 gigs/posters — all three gig beds resolve by canonical key, no stale key, 0 console errors) / ?noassets=1&gigs=1 (0 audio + 0 GLB fetches, silent-and-fine).

1. gigKey rename — DONE (debt #1). → Lane B + Lane F: switch your readers.

The manifest music key IS the gigKey, always gig-<genreKey> — one key, zero mapping. Renamed pubrock-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.