PROCITY/pipeline/AUDIT.md
m3ultra c2e44821c7 Lane E R39 (5/n): two corrections to my own R39 arithmetic, both measured
- The magpie's white, restated exactly. The wing-bar occlusion claim is the strong one and it now
  carries its footprint proof: the bar's (x 0.174-0.316, z 0.005-0.055) lies ENTIRELY inside the
  wing's (x 0-0.34, z -0.085-0.065) with the bar 3 mm above it, so from below the wing hides it.
  The nape box tops out at 0.067 against a head surface at 0.0728 (I had written 0.065, and had
  the sliver on the wrong side). And the flank argument is now the honest one: nape and rump are
  14 mm and 20 mm slabs on a 166 mm bird = 1.3 px and 2 px of white at 64 px.
- THE CLASSIC ARGUMENT FOR THE ARCADE IS INVERTED relative to R38 and I nearly carried the wrong
  one over. R38's ground palette is classic-safe BY ABSENCE (character.js returns frozen literals
  for a null town key; classic has none). The arcade district exists ONLY on the synthetic, and the
  synthetic IS the classic boot — so the kit lands squarely on the covenanted town and needs an
  EXPLICIT flag test, ground.js's VERGE shape, not the by-absence one.
- arcade_measure.mjs now also counts chunks: arcade lots span exactly 2 (0,0 and 0,1) on all five
  seeds, so a per-chunk arcade class is +1 draw in 2 chunks and +0 in the other 23.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:58:26 +10:00

74 KiB
Raw Blame History

PROCITY Asset Audit — Lane E, Step 1

Generated 2026-07-14 from a live inspection of johnking@100.91.239.7:~/Documents/3D=models/ (the sorted house library) + the 3GOD depot GET /api/list. Tri counts / bounding boxes from pipeline/glb_stat.py (dependency-free GLB parser); orientation & final scale verified by rendering every candidate through Blender and looking at the thumbnails (never "should look right").

TL;DR

  • The library is a raw dump: almost nothing obeys the house GLB law. Scale is arbitrary (only record_crate_plastic_01 was native-metres), up-axis varies, tri counts span 12 → 960k, textures reach 4096px, and several assets carry branded/trademarked designs (quarantined).
  • pipeline/normalize.py fixes scale/orientation/tris/textures uniformly. 16 assets normalized clean, verified upright & metre-scaled, WebP ≤1024, ≤5k tris, +Y up, base origin — ready to publish.
  • Gaps with no usable source (→ MESHGOD_BATCH.md or in-engine primitives): glass display case, fridge, till/register, spinner rack, magazine rack, market stall, escalator, VHS shelving.
  • Depot already holds a few reusable house props (street-bench, council-bin, milk-crate-stack) and 138 assets total (mostly TRELLIS *-views-3d-* / image-3d-* MeshGod outputs).
  • Skin caveat: 5 of the 25 facades carry baked-in sign text (djsim-milkbar "MILK BAR", djsim-video, djsim-pub, market, grimy) — they break the blank-signboard convention, so the manifest tags them signboard:"baked" and Lane B must not overlay a name on them. The other 20 are genuinely blank.

Toolchain reality (differs from the lane brief)

thing brief said actual
Blender on ultra "runs on ultra against Blender" present but not on PATH/Applications/Blender.app/Contents/MacOS/Blender (v5.0.1). Renders 4 angles in ~0.9s.
coreutils / timeout none confirmed — used ssh ConnectTimeout, no timeout/setsid
python on ultra 3.9 3.9.6 — glb_stat.py/normalize.py written to that floor
OpenRouter key thriftgod/.env absent; house convention ~/Documents/dealgod/lore/keys.env (auto-read by gen scripts)
depot upload cred GOD3_PW env, not set here → publish is gated on John (see publish.py)

Normalized set (16 — ready; manifest.jsonfittings / furniture)

house law enforced on every one: metres · +Y up · origin at base (minY=0, centred X/Z — verified node-aware) · ≤5k tris · WebP ≤1024 · no Draco. Facing is not auto-corrected — symmetric props are orientation-agnostic; the few directional ones (counter, food_cart) keep their native front and Lane B/C rotate when placing (per-asset yaw is available in batch.json).

PROCITY id source GLB src tris → out footprint (m) height (m) notes
fit record_crate shop-fittings/record_crate_plastic_01 18320→5000 0.46 × 0.63 0.41 ★ native-metres, textured red milk crate; serves record bin too
fit wire_shelf shop-fittings/shelf_metal_wire_01 330→330 1.17 × 0.44 1.80 ★ clean upright wire shelving
fit clothes_rack shop-fittings/clothes_rack_01 3152→3152 1.34 × 0.43 1.50 ★ rolling clothes rack (opshop)
fit bookshelf shop-fittings/bookshelves 45128→5000 0.20 × 1.02 2.00 tall bookshelf w/ books (book/video/opshop)
fit cube_shelf_wide shop-fittings/shelf_cube_bookshelf_01 17734→4999 0.75 × 1.24 1.10 stocked cube shelving (toy/book)
fit counter shop-fittings/balcao 276→276 1.06 × 4.02 1.10 long bar counter — Lane C may want to scale to lot
fit coffee_table shop-fittings/coffeetable 495→495 0.67 × 1.49 0.45 low display/side table
fit work_table shop-fittings/computertable 1556→1556 0.44 × 1.04 0.75 work/trestle table w/ lower shelf
fit box_crate shop-fittings/warehouse_box 78→78 1.35 × 1.32 0.60 open bulk bin / bric-a-brac box
street bench street-furniture/bench 1942→1942 0.44 × 1.21 0.85 slatted seat
street bench_modern street-furniture/eb_hipster_bench 640→640 1.68 × 0.47 0.45 low modern bench
street bench_wood street-furniture/woodbench 11728→3999 1.34 × 0.59 0.85 timber park bench
street longbench street-furniture/longbench 1172→1172 5.86 × 1.08 0.85 long feature bench (arcade/market)
street park_bench street-furniture/park_bench_01 9884→5000 1.92 × 1.62 0.85 classic slatted park bench
street streetlight street-furniture/streetlight 6592→4999 1.64 × 1.82 5.00 ornate lamppost
street food_cart street-furniture/food_cart_bakso_01 30316→5000 3.00 × 2.07 2.00 textured food cart (~3MB — heavy, 28 tex; optimize later)

Rejected during normalize (needs-manual-fix — NOT in manifest)

source issue (verified by render) disposition
shop-fittings/cube_bookcase (cube_shelf) node-scale distortion → 6.3 × 8.4 m footprint dropped; cube_shelf_wide covers the need
shop-fittings/garage_shelves_metal_v2 imports lying flat (green pallet); baked node transform, up-fix didn't help needs manual re-origin in Blender; wire_shelf covers shelving
shop-fittings/table, saloon_table, table_02, desk_01, kitchen_table degenerate/flat or node-distorted (table → 55×55 m; desk → 17×17 m) rejected; work_table/coffee_table cover tables
shop-fittings/radiogram_furniture_v2_l1 Blender "Please select a file" on import (bad GLB) needs re-export
street-furniture/market_stall_01_CHECK import error (_CHECK = sorter-flagged) → MeshGod/primitive (see MESHGOD_BATCH.md)

QUARANTINE — licence unclear / branded (🔴 do NOT publish or ship)

The house rule is 🟢 web-ok only; anything branded or of unknown provenance is quarantined until cleared. These are excluded from the manifest:

asset reason
furniture/armchair-vitra-eames, ines-chair-calligaris protected designer marques (Vitra/Eames®, Calligaris®)
shop-fittings/ikea-cupboard IKEA® product design (+259k tris)
props-scenes/mk2_1210_turntable_-_old Technics SL-1210® industrial design (trademark)
props-scenes/tuborg_fest_scene Tuborg® brand scene
furniture/uploads_files_*, props-scenes/uploads_files_* (Couch William, Harland Office Chair, nv toufa, Backpack) CGTrader/free3d bulk dumps, licence not recorded
furniture/sofa-c, hug-armchair, dragonchest, kitty-bed-01, bean_bag_chair provenance unrecorded — verify before use

Blanket caveat (applies to the 16-asset publish set): the generic fittings we normalized (benches, shelves, counter, crate…) carry no explicit licence/provenance record — I cross- checked all 16 source filenames and none is a recognizable trademark, and they were pre-sorted into the house library by John for exactly this use, so they're treated as usable per the lane brief. But "unclear ⇒ quarantine" is the house rule: before publish.py pushes to the shared CDN, record a one-line source/licence per asset, and for any CC-BY source carry attribution. If provenance genuinely can't be established for one, pull it from the publish set. This is a reason the publish step is deliberately gated on John, not automated.

Provenance — round-3 hero props (for the depot publish sign-off)

Per the round-3 instruction ("record the per-asset provenance list in AUDIT.md first, then hand John the two-liner"). The hero props have the cleanest provenance in the whole set — they are 100% generated on-device, from no third-party source:

asset (procity_fit/street_<id>_01.glb) origin licence zone
5 generated — listening_booth, drinks_fridge, arcade_cabinet, milkshake_mixer (fit) · novelty_record (street) fully synthetic, local: concept image = MODELBEAST flux_local (FLUX.2-klein-4B, Apache-2.0 model) → mesh = trellis_mac (TRELLIS.2, an original reconstruction of that concept image). No scanned/third-party geometry, no copyrighted source asset. 🟢 original work (CC0-equivalent)
cash_register (vintage-cash-register.glb) reuse of John's own thriftgod_hero asset, already live on 3GOD (round-3 reuse check) — not regenerated. 🟢 same as existing depot set
magazine_rack not shipped — TRELLIS wire rack floored ~112k tris (thin-wire gotcha) → in-engine primitive. n/a
glass_case, bus_shelter not shipped — TRELLIS failed on the transparent glass → fal.ai fallback list (John's go — Hunyuan v2 ~1417¢/gen: ~$0.280.34 for the pair, ~$1 worst case w/ retries) or primitive. n/a
  • Prompts were generic period objects with explicit no text, no brands, no people. Every kept prop was eyeballed for accidental baked branding (contact sheet docs/shots/laneE/props_gen_local.png). The only textual marks are the drinks-fridge's weathered faux-stickers — verified at full concept resolution to be garbled, illegible pseudo-lettering (no real trademark; e.g. a faux barcode + nonsense glyphs), which reads as period flavour and blurs out at mesh-texture resolution. 🟢 cleared.
  • Tri/size note: TRELLIS meshes are dense multi-shell scans; normalize.py's COLLAPSE decimate floors well above the ≤5k library budget (2577k tris, ~0.33.5 MB/prop; neither COLLAPSE nor meshopt can reduce shell-soup without a voxel-remesh that destroys the baked UVs). Accepted for focal, optional, LOD-able props (texture-intact) — flagged for a later texture-bake-to-lowpoly / gltfpack pass.
  • These GLBs stage locally (web/assets/models/, git-ignored, served by ?localdepot=1) and await John's depot publish; they are not committed binaries (same policy as the library _normalized set).

Coverage vs the registry fittings needs

shop type fittings need covered by gap →
record bins, crates, counter, listening corner record_crate, counter, wire_shelf listening booth → MeshGod
opshop clothes racks, bric-a-brac shelving, book wall clothes_rack, wire_shelf, bookshelf, box_crate
toy cube shelves, display tables, glass case cube_shelf_wide, coffee_table glass case → MeshGod
book bookshelves, spinner racks, armchair bookshelf, cube_shelf_wide spinner rack, armchair → MeshGod/primitive
video aisle shelves, returns slot, poster wall bookshelf, wire_shelf returns slot, poster wall → primitive
pawn glass cabinets, wall hooks, barred window counter glass cabinet → MeshGod; pegboard = tex-pegboard skin
milkbar counter, fridge, magazine rack counter fridge, magazine rack → MeshGod/primitive
dept mixed sections, escalator prop (all above) escalator → primitive
stall trestle tables, crates work_table, box_crate, record_crate market stall → MeshGod/primitive
street bench/streetlight/bin/tree bench×4, longbench, park_bench, streetlight, food_cart bin (council-bin on depot), tree → billboard (Lane B)

Gaps with no clean source are shortlisted in MESHGOD_BATCH.md (budget-gated) — everything has an in-engine primitive fallback, so none of these block the game.

Depot cross-reference (GET /api/list, 138 assets)

  • 90 named + 48 TRELLIS-hash. Reusable house props already live (verify visually + licence before wiring): street-bench.glb, council-bin.glb, milk-crate-stack.glb, cashbot.glb. ⚠️ akai-mpc-live-iii-* is a trademarked product design — same quarantine class as Technics/ IKEA; do not treat as clean web-ok reuse without a licence check.
  • No procity_* assets yet — this lane is the first to publish that namespace.
  • Our 16 normalized GLBs are staged in pipeline/_normalized/ and upload via pipeline/publish.py once GOD3_PW is provided (idempotent, reversible).

Full library inventory (dispositions)

shop-fittings/ (33): normalized 9 · rejected 8 (7 node-distorted/degenerate + radiogram bad-GLB) · quarantine 1 (ikea) · unused/dup 15 (heavier or duplicate tables/shelves — shelf_storage_rack_01 alone is 960k tris). street-furniture/ (9): normalized 7 · error 1 (market_stall) · building_shell_01 reserved for Lane B (38k-tri building shell — not a fitting). furniture/ (17): mostly domestic (beds/sofas/chairs) — out of scope for shop fittings; 6 quarantined. props-scenes/ (10): dj_roomv01 + turntable = record-shop dressing (turntable quarantined: Technics); rest branded/scene dumps.

Raw stats for every parsable .glb (59 of the ~69 library entries; the rest are .fbx/.obj or subdir sources not counted): pipeline/_audit_stats.json. glb_stat.py reports world-space dims (node-transform-aware) and WebP/PNG/JPEG texture sizes. Normalizer output: pipeline/_normalize_results.json.

Round 4 — ped-rig decimation (gate-3 blocker)

The 19 street ped rigs in web/models/peds/ were ~31k56k tris each (924k total) — the cause of gate 3 failing (~279k tris at the busiest intersection vs ≤200k). Decimated to ≤3k tris each (integrator decision #1) via gltf-transform weldsimplify (meshoptimizer):

gltf-transform weld  in.glb  _w.glb
gltf-transform simplify _w.glb out.glb --ratio 0.05 --error 0.12

meshoptimizer works cleanly here (proper manifold skinned character meshes, NOT the TRELLIS shell-soup of the R3 hero props). Result: 924,153 → 46,551 tris (48.6k → 2.45k avg; range 15642805, all ≤3k), 36M → 5M on disk. Skinning fully preservedJOINTS_0/WEIGHTS_0 intact, skeleton unchanged (nodes + 65 joints identical to source), so the shared walk.glb/ idle.glb clips retarget unchanged (both left untouched). Contact sheet: docs/shots/laneE/peds_decimated.png — silhouettes/textures read clean, no melted faces/hands (fine for street distance). Originals backed up (gitignored) at pipeline/.peds_orig/; per-ped before/after in pipeline/_peds_decim.json. Lane D validates rig binding + anims (task D2); Lane F re-measures gate 3 (~24-rig near cap × ≤3k ≈ ≤67k fleet contribution).

Round 4 — E2 asset re-exports (published)

  • procity_fit_record_crate_01.glb re-export (fixed). The R3 normalize left a sourceless texture (packed AO/roughness map didn't survive the WebP export; the material's metallicRoughnessTexture + occlusionTexture pointed at it), which crashed three.js GLTFLoader with undefined 'uri'. pipeline/fix_glb_textures.py strips material slots that reference a sourceless texture (base-colour + normal kept; the orphan is never loaded). Verified it loads in the vendored GLTFLoader (5000 tris, textures ok); re-published to the depot.
  • procity_fit_counter_till_01.glb (new). ~2 m timber shop counter with a period beige till — flux_local concept (3 rolls for the ~2:1 aspect) → trellis_mac mesh → normalize.py. 1.61 × 0.64 m, 1.15 m tall, 32k tris (TRELLIS shell-soup floor, within decision-#2's 2577k for focal props). Provenance: 100% synthetic/on-device (FLUX.2-klein-4B + TRELLIS.2 on the M3 Ultra), no third-party source — cleanest licence zone. Replaces the R3 4 m till-less counter for the interior counter slot (Lane C maps countercounter_till); the balcao 4 m counter stays for long-bar use.

Round 5 — E1: hero-prop bake-to-lowpoly (un-parked)

The 6 heavy TRELLIS hero props were 25k77k tris (shell-soup — simplify-only floors there, R3's lesson). Rebuilt topology + re-baked base colour via pipeline/bake_lowpoly.py (Blender headless): voxel-remesh → triangulate → decimate ≤8k → smart-UV → Cycles DIFFUSE(COLOR) bake HP→LP → WebP export. All ≤8k tris, base colour baked onto fresh UVs, self-illuminated a touch (matches normalize). Originals backed up (gitignored) at pipeline/.props_orig/; batch spec pipeline/batch_bake.json; results pipeline/_bake_results.json.

prop tris before → after voxel size on disk note
drinks_fridge 77047 → 8000 0.03 5.3M → 0.9M hollow glass-door box — needed a coarser voxel (0.012 grabbed the internal shelves into wireframe)
milkshake_mixer 73492 → 7991 0.005 4.7M → 0.9M small part, fine voxel
listening_booth 53502 → 7986 0.012 3.4M → 0.9M panel + stool
counter_till 32226 → 7999 0.012 1.9M → 0.9M timber counter + till
novelty_record 26486 → 8000 0.014 1.8M → 0.9M thick disc + base
arcade_cabinet 25142 → 8000 0.012 1.5M → 0.8M boxy cabinet

Fleet: 288k → 48k tris, 18M → 5.2M. Every one eyeballed (contact sheet docs/shots/laneE/props_baked.png — no melted labels/faces; the fridge re-bake was the only redo) and verified loading in the vendored GLTFLoader (tris ≤8k, baked texture present). Re-published to the depot (validate --depot 0 errors). Provenance unchanged: 100% synthetic/ on-device (FLUX.2-klein-4B + TRELLIS.2 + Blender bake, no third-party source). → Lane C re-measures the record interior (was ~273k tris).

Round 5 — E3: fal.ai fallback props (glass-case + bus-shelter + magazine-rack)

The two R3 TRELLIS glass rejects + the optional magazine-rack, generated on fal.ai Hunyuan3D v2 (John-authorized, decision #4). Pipeline: flux_local concept (frosted/opaque surfaces so the reconstructor has something to grab — clear glass is what defeated TRELLIS) → pipeline/gen_props_fal.py (fal fal-ai/hunyuan3d/v2, key from MODELBEAST settings, never hardcoded) → normalize.py (scale/ origin) → bake_lowpoly.py (voxel-remesh → ≤8k → EMIT albedo bake; Hunyuan materials need the base-colour→emission route, DIFFUSE(COLOR) baked them near-black).

Paid gen log (fal Hunyuan3D v2, ~$0.16/gen):

prop attempts cost outcome
glass-case 1 (frosted-glass concept) ~$0.16 solid case, timber base, items visible — TRELLIS couldn't; 40k→8k baked
bus-shelter 2 (att.1 dropped the thin perspex panels → att.2 with an opaque back wall) ~$0.32 solid shelter, back wall + roof + bench; 40k→8k
magazine-rack 1 (optional; solid-timber, not wire) ~$0.16 tiered timber rack w/ magazines — beat the R3 wire failure; 40k→8k

Total fal spend this round: ~$0.64 of the $22 balance (well under the <$1 estimate). Guardrail held — nothing else on fal. All 3 ≤8k tris, base-colour baked, verified loading in the vendored GLTFLoader, eyeballed (docs/shots/laneE/props_fal.png — no melted glass, no legible branding), published, validate --depot 0 err. Provenance: FLUX.2-klein-4B concept + fal Hunyuan3D v2 mesh + Blender bake — synthetic, no third-party source (Hunyuan output is model-generated geometry). Raw fal GLBs + concepts kept in gitignored pipeline/.genprops/. These retire the last two primitive-only fittings (glass_case, magazine_rack) + add a street bus_shelter.

Round 5 — E2: depot growth (consumer-driven)

The round-5 v2 features are procedural (dig.js riffles generated sleeve art; B's window shader is 100% procedural; D's roster is spawn logic) — so no forced GLB needs. Per Fable "don't add manifest entries no lane consumes", E2 is deliberately light: two already-synthetic depot props with clear consumers, re-normalized to procity names + thumbs (both already ≤5k tris, metre-scale, base-origin):

new id source footprint / h consumer
street/bin (procity_street_bin_01.glb) council-bin.glb (thriftgod_hero, on depot) 0.57×0.68 m, 1.0 m Lane B street dressing (towns need bins)
fit/crate_stack (procity_fit_crate_stack_01.glb) milk-crate-stack.glb (thriftgod_hero) 0.71×0.72 m, 0.9 m Lane C record-store floor piles / dig context

Both 3k tris, verified loading in the vendored GLTFLoader, published, validate --depot 0 err. Provenance: MeshGod-generated (thriftgod_hero) — synthetic, web-ok. Note: also resolved Lane B's B3 ask — novelty_record (was 26.5k, spiking the busy view) is now 8k via the E1 bake. No speculative props added; further depot growth is available on request once C's dig / D's roster name concrete needs.

Round 7 — E1: ped sub-mesh merge to one draw (gates the roster flip)

The 19 ped rigs carried 19 primitives / 13 materials each = 92 draws for the fleet (≈116 at the 24-rig near cap) — the optimisation D's memo gates full-density roster on. pipeline/merge_ped.py (headless Blender) merges each ped to ONE primitive + one atlased material: join meshes → pack every material's UV islands into a non-overlapping atlas UV → EMIT-bake each source material's base-colour into one 1024 atlas (route base-colour→emission via the ORIGINAL uv so the atlas UV doesn't hijack the lookup) → single material. Skinning + skeleton fully preserved (65 joints intact on all 19, JOINTS_0/WEIGHTS_0 kept; walk/idle untouched).

Result: fleet 92 → 19 draws (1/ped); at the 24-rig near cap 116 → 24 draws (79% cut). Size 5M → 4M. Verified in the vendored GLTFLoader: all 19 load with isSkinnedMesh + bind the shared walk.glb clip; man_suit (2-mat) poses to byte-identical dimensions vs the original (109.038 × 62.5) proving the skin deforms unchanged; man_athletic (3-mat, most complex atlas) eyeballed correct (skin/top/pants regions all right). Pre-merge decimated peds backed up (gitignored) in pipeline/.peds_r6/; per-ped in pipeline/_peds_merge.json. → Lane D validates (D1) — full silhouette/material/impostor/identity pass + full-density numbers — then F flips the roster.

Round 7 — E2: GODVERSE stock pack pipeline (real sleeves for the dig)

pipeline/build_stock_pack.py builds C's ?stock=real content pack offline (nothing queries the dataset at runtime): read recordgod (dealgod Postgres discogs_release, 185k vinyl w/ cover URLs) → curate N sleeves (deterministic md5 sample, varied) → parody-transform title/artist → fetch covers → bake 8×8 atlas WebPs (256px cells) + a JSON index ({id,title,artist,price,price_band,atlas,uv}) → stage for ?localdepot / publish to depot.

Parody law (metadata): real (title, artist) → deterministic seed → recombined from period-AU word banks; the output shares no source token (Dark Side of the Moon/Pink FloydQuiet Milk Bar/ The Galahs) so there is zero real-trademark leakage, yet it reads as plausible AU record stock, and the same source row yields the same parody every build. Verified deterministic + token-disjoint.

Pipeline PROVEN end-to-end with --sample 24 (synthetic solid-colour sleeves): 24 → one 2048 atlas

  • index, UVs correct, price bands (bargain/standard/collector/grail) working; sample pack staged to web/assets/models/ so Lane C can wire the consumer against the real index format now.

The real 200-400 pack is blocked on two external deps (flagged to Fable):

  1. GODVERSE_DSN — the dealgod Postgres DSN (creds). Postgres is up on localhost:5432; the builder reads the DSN from env (I don't hunt for creds). --from-db --count 300 runs once it's set.
  2. Lane C's pack schema (their task 1, not yet posted) — I built to the proposed fields above; INDEX_FIELDS adjusts to C's LANE_C_NOTES contract when it lands. Provenance of the real pack: source = Discogs metadata + cover images (recordgod), metadata parody-transformed (no real titles/artists ship); covers are real release art (read-only, non-economy per decision #2). Small book/toy packs are a follow-up once the record pack lands.

Round 8 — E1/E2: book + toy stock packs + pack-QA gate

E1 — pipeline generalized (build_stock_pack.py now kind-parametrized: record 256×256 sleeves, book 96×256 tall-thin spines, toy 256×256 boxes). Per-kind parody word banks; same laws as the record pack (build-time extract, deterministic parody-transform of metadata, atlas WebPs + JSON index to C's schema — stock_<kind>_index.json, C reads uv not cell). Book/toy sample packs built + staged (web/assets/models/, ?localdepot) so Lane C wires book/toy shelves now; indexes committed to pipeline/packs/ as the contract. Parody verified deterministic + no distinctive-token leakage across all 3 kinds ("Nevermind"/"Nirvana" → "Endless Blue"/"Mullet Milk Crates"; "The Hobbit" → "Under the Homestead"/"C. Underwood").

E2 — pack-QA gate (validate_pack.py, wired into validate_manifest.py → qa gate 3): every pipeline/packs/stock_*_index.json entry must have a valid kind, in-range non-inverted UVs, a known price band, non-empty title/artist, unique id, and an atlas listed in atlases[] — a bad bake fails qa loudly (proven: inverted-UV + empty-title → exit 1). qa --strict GREEN 5/5.

Blocker (flagged to Fable/John — real data): the real ~150250-item book/toy packs (and the real record pack) need GODVERSE_DSN (dealgod Postgres; up on :5432, read from env, no cred hunt). One command each once set: build_stock_pack.py --kind book --from-db --count 200. The current staged packs are synthetic placeholders (real index format); provenance of the real packs = GODVERSE metadata (OpenLibrary/toygod) parody-transformed (no real titles/authors/brands ship) + real cover art (read-only, non-economy). No fal spend this round.

Round 8 (cont.) — REAL stock packs live (GODVERSE, John-authorized DB access)

The synthetic samples are replaced by real packs, built from GODVERSE via peer-auth on m1ultra (johnking@100.91.239.7, native Postgres, psql at /opt/homebrew/Cellar/libpq/18.4/bin). Access resolved after the deployment DB proved Docker-internal (port closed, SSH denied) — John pointed to the m1ultra clones. All extraction was read-only SELECT; no creds guessed, no writes.

pack items source (m1ultra) covers prices
record 350 discogs_full: inventory (John's real store stock) ⋈ releaserelease_image real Discogs cover art (monsterrobot.party CDN, from local_url) real store prices
book 311 dealgod.products WHERE category_path LIKE 'books%' real product photos (image_url, portrait 176×256) real current_price
toy 273 dealgod.products WHERE category_path LIKE 'collectables-hobbies-toys%' real product photos (image_url) real current_price

Build: build_stock_pack.py --kind <k> --from-tsv (rows extracted to a gitignored TSV, covers fetched at build time, resized into atlas cells). Metadata is parody-transformed (titles/artists/authors/ brands → house word banks, deterministic, shares no source token — "Dark Side of the Moon"/"Pink Floyd" → "Quiet Milk Bar"/"The Galahs") so no real trademark ships in the pack text; the cover images are real release/product art (read-only, non-economy, per decision #2 — John's own store inventory + AU marketplace listings). Books are info-only in the source → prices seeded deterministically (John's call). 15 atlases + 3 indexes: atlases published to the depot (/api/upload), indexes committed to pipeline/packs/ (the C contract) + staged for ?localdepot. All 3 pass the pack-QA gate. Toy filter is broad (some comics/coins among the collectibles) — acceptable for a collectibles shop; tighten later.

Round 9 — E1: VRAM / texture memory audit (v2.0 ship due-diligence)

Decoded GPU cost = w·h·4 (RGBA) ·1.333 (mipmaps). WebP/JPEG disk size is irrelevant to VRAM — a 2048² atlas is ~22 MB resident regardless of its 0.8 MB file. Measured across every texture source:

category files total VRAM notes / typical resident
facade skins 48 117 MB shared per-chunk; only in-view shops resident (~6085 MB)
sky domes 10 23 MB 1 dome resident (~2 MB)
ground 10 9 MB ~3 in view
interior tex (floor/wall/counter) 16 12 MB current room only (~3 MB)
wallpaper 8 6 MB current room only
ped fleet (19 merged rigs) 19 ~90 MB (15×1024²=5.6 + 4×512²=1.4) all resident (near-rig variety)
GLB fittings/furniture textures 28 ~3040 MB street furniture + current room (~25 MB)
stock: record 6 134 MB (256px cells, full) resident only under ?stock=real (eager-preloaded at boot)
stock: book 4 19 MB (half-res this round) lazy, on book-shop entry
stock: toy 5 28 MB (half-res this round) lazy, on toy-shop entry

Resident-VRAM by boot mode (peak, approximate):

  • Default boot (no flags): in-view skins ~85 + ped fleet ~90 + GLB ~25 + Lane-B runtime sign-atlas ~40 ≈ ~240 MB.
  • ?stock=real: +record 134 (eager) → ~375 MB; +book/toy after visiting those shops (stockpack caches, no dispose) +47 → ~420 MB.
  • Worst case (all flags, all 3 shop types visited): ~450 MB (weather rain = 1 instanced draw, winmap = procedural — both VRAM-negligible).

Cheap win taken this round: book + toy atlases half-res'd (256→128 / portrait 176→88 px cells; UVs are fractional so no index/UV change, only the atlas files shrank). Book/toy are quick pull-cards (buy-anywhere), not the deep record dig — covers verified still legible at half-res. Stock VRAM 323 → 181 MB (record kept full for close riffle examination). Atlases re-published to the depot.

VERDICT: fine to ship v2.0. ~240 MB default / ~420 MB worst-case is comfortable on desktop (GB-class VRAM) and within a low-end/integrated ~512 MB1 GB texture budget. The stock packs (181 MB) remain the dominant swing but no longer blow past the base. No compression pass or lazy-load required for the tag. Two follow-ups flagged for v3, not now (bigger than a ship-round change):

  1. stockpack cache-cap (Lane C's stockpack.js): dispose non-current shop-type atlases on room-type change → bounds stock VRAM to ≤134 MB (one type) instead of accreting all three.
  2. KTX2/Basis GPU-compressed atlases: 48× VRAM cut with no visible quality loss — the real long-term fix for the record pack's 134 MB (needs a transcoder + loader change; out of scope now).

Depot/manifest hygiene (E2): validate --depot 0 err · pack-QA 3/3 valid · provenance-drift clean (_published.json == 27 live procity GLBs) · 15 stock atlases live, 0 orphans. All green post-R8.

Round 11 — audio pack provenance

Every audio file is 100% original procedural synthesis (pipeline/gen_audio.py, numpy on the MODELBEAST box → ffmpeg). No neural model, no sample library, no third-party recording touched — so the whole pack is CC0-equivalent original work and parody-safe by construction: there is nothing to imitate, every waveform is an oscillator/noise render. Music beds are original chord progressions/melodies — no imitation of any specific real recording. Tool choice is documented in pipeline/README.md (§ audio): synthesis over local MusicGen/stable-audio because it's $0, deterministic/seeded, perfectly loopable, and licence-clean.

group files origin
ambience (6) street-day/night, rain, roomtone-retail/milkbar/video filtered-noise beds + synthesised events (birds/crickets/drops/hum) 🟢
sfx (13) doorbell, door-open, till, riffle, toast, tram-bell, tram-rumble, footsteps ×6 additive-bell + filtered-noise transients 🟢
music (4) record-shop, milkbar, video-synth, arcade original synth loops (bass+chords+drums+lead) 🟢
  • Format: OGG/Opus primary (48 kHz) + M4A/AAC fallback (44.1 kHz). Loudness: long beds/music ffmpeg loudnorm I=-16 LUFS / TP=-1.5 (single-pass EBU R128); short SFX peak-normalised (-3…-6 dBFS).
  • Verified (I synthesise, I can't subjectively listen): spectrogram sheet docs/shots/laneE/audio_spectrograms.png (content/character) + numeric pass — peaks ≤ -0.4 dBFS (no clip), all loops seamless (wrap-jump ≤ internal step), durations correct. Final subjective ear-check is John's/F's — flagged honestly.
  • Budget: 8.1 MB shipped (2.8 MB ogg + 5.3 MB m4a) vs the 25 MB cap; lazy-loaded per manifest.
  • Files ship in web/assets/audio/ (committed, like gen/ skins); WAV scratch pipeline/.genaudio/ is git-ignored. Contract in manifest.jsonaudio:{ambience,sfx,music}.

Round 12 — gig layer provenance (v3.0-alpha, ?gigs=1)

Gig audio (3, procedural — same discipline as R11): music-pubrock-live (original 90s pub-rock riff — overdriven power chords + bass + rock kit), ambience-crowd-walla (synth crowd murmur, decorrelated L/R), sfx-applause (400 filtered-noise claps + cheer swell). 🟢 100% original synthesis, $0, seamless loops, parody-safe. Flagged gig:true in the manifest; F plays them when a gig is on. Pack now 26 assets, 9.1 MB of 25 MB.

Gig posters (4 skins, skins.poster): poster-xerox/screenprint/grunge/retro — 90s pub-rock flyer templates generated by MODELBEAST flux_local. Blank-name-band discipline (like blank-signboard facades): each has a clean top banner and no baked text; B/C overprint the seeded band name into nameZone [x,y,w,h]. Eyeballed (contact sheet in the round note) — all 4 have the blank banner, zero legible text. 🟢 original generation, portrait 768×1024 JPEG in web/assets/gen/.

Gig instruments (5 GLBs, MODELBEAST flux_local concept → trellis_macnormalize.py): 🟢 100% original synthesis, $0. All 5 kept — every one reconstructed cleanly (better than the round-3 wire rack; even the mic-stand's thin boom pole and the drum cymbal hardware survived). Held by D's band rigs; primitive fallback if a GLB is absent (house law).

instrument tris footprint (m) h (m) note
electric_guitar 43k 0.38×0.08 1.0 sunburst solid-body, clean
bass_guitar 36k 0.34×0.12 1.15 black bass, clean
drum_kit 30k 1.50×1.24 1.3 full kit, cymbals survived
guitar_amp 96k 0.51×0.29 0.6 combo amp — heaviest (grille cloth → shell-soup)
mic_stand 34k 0.57×0.57 1.7 chrome boom stand, thin pole held
  • Same TRELLIS tri-weight tradeoff as round-3 props (3096k tris vs the ≤5k library law) — accepted for focal/optional/LOD-able stage props seen at mid-distance; texture-bake-to-lowpoly is the follow-up. Amp (96k) is the outlier — a candidate for that pass first. Eyeballed (contact sheet).
  • Staged to web/assets/models/ for local validation (?localdepot=1). Depot publish was PENDING John's OK at R12 → DONE in R13 (see the R13 section below; the 3096k figures in the table above are the R12 originals, decimated to 14k each in R13).

Round 13 — gig-layer finalization (v3.0-beta, ?gigs=1)

Supersedes the R12 instrument table + pending-publish note above. Three closes, all 🟢 provenance unchanged (100% on-device synthetic / procedural — decimation and renaming don't touch the licence zone):

1. Instrument decimation (pipeline/decimate_props.py, Blender collapse-decimate — NOT voxel-remesh, so thin features survive). The R12 instruments were the TRELLIS shell-soup tri-weight outlier; a district means multiple concurrent stages, so all 5 were taken to a uniform 14 000 tris (fleet 236k → 70k, 70%). Collapse (keeps original UVs/textures, no re-bake) held the mic boom pole + cable, drum cymbals/stands and guitar necks — verified by re-rendered thumbs. The high-poly originals are kept as _hi variants (published to the depot as archival source, not manifest-referenced).

instrument R12 tris → R13 _hi original footprint (m) h (m)
electric_guitar 43 451 → 14 000 43 451 (_hi) 0.38 × 0.08 1.00
bass_guitar 36 031 → 14 000 36 031 (_hi) 0.34 × 0.12 1.15
mic_stand 34 079 → 14 000 34 079 (_hi) 0.57 × 0.57 1.70
drum_kit 26 238 → 14 000 26 238 (_hi) 1.50 × 1.24 1.30
guitar_amp 96 583 → 14 000 96 583 (_hi) 0.51 × 0.29 0.61

2. gigKey rename + two new beds (debt #1). The manifest music key IS the gigKey, always gig-<genreKey>. music-pubrock-livegig-pubrock; added gig-grunge (band_room) and gig-covers (rsl, organ-led) — all procedural numpy synth, $0, seamless, parody-safe (new synth helpers organ() + grungechord()). Audio pack 26 → 28 assets. Old pubrock-live files deleted.

3. Depot publish — DONE (John OK'd direct publish, R13). Pushed via the passwordless tailnet ingress (this box is allowlisted): the 5 base names now serve 14k bytes + the 5 _hi originals are live. Verified on the public digalot.fyi depot (amp base 5.9 MB → 1.31 MB, _hi 5.9 MB live). _published.json 33 → 38. validate_manifest --depot GREEN; Gate-3 provenance flag cleared.

Round 36 wave 1 — the 2 a.m. ped assets, swept (provenance + depot regularization)

046668e (4 dance clips) + b339402 (5 trellis DJ bodies — the R36 brief said 3; the diffstat says 5) landed July 20 outside the round system. R36 wave 1 swept all 9 into the pipeline; the files themselves are untouched (byte-identical to both commits and to their 90sDJsim originals).

Dance clips — zone corrected by measurement: ROKOKO, not Mixamo. Source = Rokoko's free 263-asset mocap library (~/Documents/Rokoko_Free_Mocap_FBX_263, Rokoko Studio (Mocap)/Dancing/*_mixamo.fbx, Rokoko's own Mixamo-skeleton export), baked mesh-free by 90sDJsim tools/bake_clips.py (the fbx_to_clip.py port). Licence per the pack's rokoko.com page: royalty-free including commercial use. Structure (all 4): 0 meshes · 68 nodes · 198 tracks · 52-joint dangling skin with IBM · 0 NaN/Inf · binds 65/66 canonical targets on a shipped ped skeleton (only mixamorigReference unbound — dropped silently by three.js; matches 90sDJsim's in-game 64/65). Durations 30.58/30.50/44.04/52.42 s.

clip bytes sha1
dance_party.glb 688 220 38587f7b0cf4aaf199f55677a139db087acdd80d
dance_medium.glb 1 028 164 4a30fb62163b6e5137c8fb513dfc70d5f6f322b4
dance_drink.glb 962 464 d818ddb0868e19c87a435b26c8b6ffab14832be4
dance_sway.glb 1 132 560 8af9112538e5219fc6a7b6cb36bf1e87e5ac24ff

DJ bodies — on-device TRELLIS.2 mesh (CC0-equiv, the instrument zone) + Adobe Mixamo auto-rig, converted/decimated by MESHGOD scripts/fbx_to_ped.py. GLB law: no Draco, 1024² webp (EXT_texture_webp — roster-standard), 1 mtl/1 img, 65-joint mixamorig skin, IBM 0 NaN. Ped law ≤5k: PASS. ⚠ bind-pose skeleton spans ~0.950.99 units vs the existing roster's ~173180 (cm convention) — absorbed at spawn (rigs.js:163 height/span normalize), flagged to F's R36-w2 figure-scale gate.

body tris bytes sha1
dj_phrtt_01.glb 4 000 712 792 21cf0db5a291f88f7fd9b9cfdb0eac8413c7cf2e
dj_techno_01.glb 4 000 702 760 4cb6bf44a752f921e14d1355f9944b7859c92386
woman_dj_01.glb 3 999 731 440 7d364dfc7e7659c0b15bead82a5d8bbd285e5286
woman_dj_02.glb 3 999 715 116 2413ededff1a7684999d6fd6fd58417b77f25c10
woman_dj_03.glb 3 998 718 128 ec39692fa8019645bd200aab9eb6d1078535189c

Depot: staged to _normalized/ (the sit/look practice) and published via the passwordless tailnet ingress under John's R22 standing authorization — _published.json 44 → 53, drift check green, and all 9 fetched back from the public depot (digalot.fyi/3god/a/) sha1-identical to local. Gates after the sweep: validate_manifest 0 errors / 0 warnings · publish.py --verify ✓ 53/53 live.

Round 38 — v8 wave 1: the asset wave (pipeline swap · audio · grounds · the mesh probe)

Everything below is measured on this box today. Where the R38 review's costed figures differ from what the hardware actually did, the measured number is given and the difference is named.

1. THE PIPELINE FIX — trellis_mactrellis2_mlx, and the output-contract trap it hides

gen_props.py hardcoded trellis_mac (the review's farm telemetry: p50 245.4 s, 33 jobs/7 d). Swapped to trellis2_mlx (384 jobs/7 d, p50 134.6 s, better PBR bake). Operator contract read from its manifest.json, not assumed — pipeline_type / seed / steps / baker / texture_size / max_bake_faces / alpha_mode; we pass 1024_cascade, baker:metal, max_bake_faces:500000 (fal parity, M3-Ultra-verified), alpha_mode:opaque.

The two operators are NOT drop-in on output, and the naive swap would have shipped untextured meshes. trellis_mac writes one *.glb; trellis2_mlx writes BOTH candidate.glb (raw) and candidate_pbr.glb (baked) into the same tree, and the old code took sorted(rglob('*.glb'))[0] — which is alphabetically the raw one. Fixed with _pick_glb(): trust the operator's own result.json first, then candidate_pbr.glb by name, then anything. --legacy-trellis keeps the old operator for an A/B.

Also added the bg_remove_local (RMBG-2.0) cutout stage the review's path calls for (flux → bg_remove → trellis2 → normalize), fail-soft: if RMBG errors the raw concept is fed through and the mesh operator segments internally as before.

Measured on the 5-prop R38 batch (wall clock, mesh stage only): 122 s · 83 s · ~108 s · ~123 s → mean ≈ 109 s, i.e. faster than the farm's 134.6 s p50 and ~2.2× the old operator's 245.4 s. The cutout stage costs 56 s per prop. The review's ~2× throughput claim is confirmed with a real number. Retry budget went unused: 5/5 first-attempt success, 0 failures, 0 OOM-kills.

2. AUDIO — the inhabitation layer (item 1.1). 6 new PACK entries, $0, 🟢 original

Same doctrine as R11/R12 and for the same reasons: 100% procedural numpy synthesis, no neural model, no sample library, no third-party recording — CC0-equivalent original work, parody-safe by construction (every waveform is an oscillator or a noise render, so there is nothing to imitate). Deterministic through crc32(name) → the same dog is the same dog on every boot.

asset cat dur ogg m4a synthesis
sfx-dog-bark-fence sfx 3.2 s 16 KB 37 KB 8 barks: pitch-dropping saw through 2 vocal-tract formants + attack noise; 2.2 kHz lid + a 28 ms paling slap-back + yard reverb 🟢
sfx-magpie-carol sfx 2.6 s 28 KB 42 KB fluted glides under a 2852 Hz quaver, every note doubled a fifth up (the syrinx has two sides — that is why a carol sounds like two birds) 🟢
sfx-roller-door sfx 2.8 s 24 KB 45 KB slow-fast-slow clatter train + sheet-metal drum + panel modes amplitude-followed off the clatter + the stop 🟢
ambience-mower-distant ambience 12.0 s 78 KB 190 KB firing pulse train w/ load wander, blade hiss, 900 Hz lid. Loop genuinely folds: every LFO is a harmonic of 1/12 s and the firing rate is a whole number of cycles per loop 🟢
ambience-radio-through-window ambience 8.6 s 82 KB 136 KB an original 4-bar turnaround (D A G E) → 3802600 Hz little-speaker band → 1.5 kHz glass lid 🟢
ambience-sprinkler ambience 8.0 s 53 KB 127 KB impulse arm ticking across a 62% arc then swinging back silent, under a sweeping spray hiss 🟢

Budget: +281 KB ogg / +577 KB m4a = +0.86 MB. Pack 10 MB → 10.9 MB against the 25 MB cap, which matches the charter's ~11 MB estimate. Zero draws, zero tris — as chartered, this item cannot touch the render budget under any circumstance.

Verified (I synthesise; I cannot subjectively listen): spectrogram sheet docs/shots/laneE/inhabitation_spectrograms.png + a numeric pass — peaks 0.330.71 (no clip), loop wrap-jump ≤ 0.18× the 95th-percentile internal step on all three beds (radio 0.00×, mower 0.08×), and band-energy fractions that match the intent: mower 99% below 1 kHz, radio 87%/13%/0% across <1k/14k/>4k, carol 86% in 14 kHz, bark 89% below 1 kHz. One defect found and fixed by looking: at spray gain 0.5 the sprinkler's hiss masked its own ticks and it read as a hose — the tick is the sprinkler. Rebalanced to 0.28 and re-rendered; the ticks and the silent swing-back are now visible in the sheet. Final subjective ear-check is John's / F's — flagged honestly, same as R11.

3. GROUND SKINS — the per-town palette (item 1.2). 4 new, flux_local, $0, 🟢

gen_skins.py had no GROUND template at all — the ten shipped grounds predate it. Added one, plus a --grounds mode and a dedicated GROUND_STYLE prefix: the house STYLE bible says "warm fluorescent lighting", which is an interior cue and wrong for an outdoor overhead surface.

skin use px bytes note
ground-grass-dry yard 512² 157 801 straw-blond patchy summer lawn, bare soil through it 🟢
ground-grass-temperate yard 512² 183 917 dense damp deep-green lawn + clover 🟢
ground-coastal-sand yard 512² 131 231 pale wind-rippled sand + shell grit 🟢
ground-bitumen-reddust road re-rolled, see below

Cost correction. The review costs gen_skins.py --local at 5.7 s; measured here at ~9.7 s each. That is not a regression — the review's figure is for the 1024×576 facade geometry and these are 1024² square tiles, 1.78× the pixels. Per-pixel the two agree. Square is the deliberate choice: grounds tile through RepeatWrapping, and the two djsim-* grounds (the only 512×512 ones) are the correct precedent — the eight flux-era grounds are 512×279 and stretch under a square UV tile. That is a pre-existing defect, recorded not fixed.

4. MESHES — the proof batch (items 1.3 + 1.4), flux_localbg_remove_localtrellis2_mlx

Licence: 🟢 CC0-equivalent under AUDIT.md:101's standing doctrine — a flux_local concept on an Apache-2.0 ungated model (FLUX.2-klein-4B), reconstructed by TRELLIS.2 into original geometry. Nothing downloaded, nothing pooled. The 3GOD depot was not read as a pool (971 live, 53 recorded, known trademarked items inside — it stays a read-only archive) and the hardyards GLBs are REJECTED on provenance, as the round ruled: 39 GLBs with provenance documented for one file. Every one of these five is ours.

Operator timings, all five, wall clock (includes the 56 s RMBG cutout):

prop trellis2_mlx raw tris outcome
magpie 129.0 s 473 513 shipped 894 tris
hills-hoist 88.1 s 503 716 thin-structure failure
paling-fence-panel 112.3 s 479 611 shipped 1 121 tris
tv-aerial 128.0 s 494 204 thin-structure failure
water-tank 489.0 s 475 047 shipped 500 tris

median 128.0 s — inside the farm's 134.6 s p50 and 1.92× faster than trellis_mac's 245.4 s. Mean is 189.3 s, dragged by one 489 s outlier (3.8× the median, same params, same box, no error) — worth naming: the operator's tail is long even when nothing fails. 5/5 first-attempt success, 0 retries used against the 45% budget, 0 OOM-kills.

4a. SHIPPED — three assets, all GLB-law clean

House law verified on every one: metres · +Y up · base origin · 1 material / 1 image / 512² WebP · no Draco.

id tris footprint (m) height (m) notes
magpie 894 0.15 × 0.40 0.358 the 1.3 asset. Silhouette excellent at 894 — beak, legs, folded wing and tail all read. Albedo flagged, see 4c. 🟢
paling_fence 1 121 0.28 × 3.59 1.788 weathered grey palings with gaps + two rails. Overshot its 400 target (see 4b) 🟢
water_tank 500 1.73 × 1.64 2.186 corrugated galvanised tank, domed lid, outlet tap. Cleanest asset in the batch 🟢

Contact sheet (4 angles each): docs/shots/laneE/r38_props_shipped.png.

⚠ NOT PUBLISHED — one step outstanding. All three are normalized, baked to budget and staged in pipeline/_normalized/, but they are not on the 3GOD depot and therefore deliberately not in manifest.json: pushing to a public depot is an outward-facing action and this lane had no in-session authorization for it. validate_manifest.py correctly fails any manifest GLB with no _published.json record, so the results file is held at pipeline/_r38_results.PENDING_PUBLISH.json. build_manifest.py is already wired to _r38_results.json: publish, rename, re-run and the three land. The gate is green as committed (0 errors, 0 warnings).

4b. THE DECIMATION FLOOR — a real finding, not a tuning miss

normalize.py's collapse decimator cannot reach small budgets on TRELLIS output, and nobody had ever asked it for ≤900 before. Measured, all five: 470 442→13 849 (target 900) · 478 236→250 068 (600) · 476 974→18 791 (400) · 485 565→89 306 (400) · 475 047→70 893 (500). The cause is the documented shell-soup floor — collapse cannot merge across disconnected shells, so the floor is the shell count. bake_lowpoly.py (R5's voxel-remesh → decimate → Cycles bake) is the correct tool and clears it: magpie 894, water_tank 500, tv_aerial 299, hills_hoist 496. paling_fence floored at 1 121 against a 400 target — a flat panel of separate palings has an irreducible shell count at voxel 0.012; it is shipped at 1 121 rather than remeshed coarser, because coarser merges the palings into a slab and the gaps are the whole point of a paling fence.

4c. THE TWO FAILURES — thin structures, exactly as the review predicted

hills_hoist and tv_aerial FAIL and are NOT shipped. Both were deleted from _normalized/ rather than left where a later manifest build could catalogue them. Evidence: docs/shots/laneE/r38_thin_structure_failure.png.

TRELLIS reconstructs the solid parts well — the hoist's pole, concrete footing and four arms are all there at 250 k tris, and it is recognisably a Hills Hoist. The wire is what dies: it comes back as thousands of tiny disconnected fragments, which is simultaneously why the mesh reads as frayed and why it will not decimate. And both escape routes fail in opposite directions: collapse floors at 250 k / 89 k with the fraying intact, and a voxel remesh coarse enough to reach 600 tris erases the pole entirely — the 496-tri hoist is a floating horizontal smear with no upright, and the 299-tri aerial is scattered debris. There is no voxel size that keeps a 30 mm pole and a 2 mm wire at 600 tris.

This is a DECISIVE capability answer, and it is the one the round asked for. Thin-structure props are not a TRELLIS class. → Lane B: use primitives for the hoist and the aerial, which is the boxMats path anyway and costs +0 draws — strictly better than the GLB would have been.

4d. THE ALBEDO PROBE — TRELLIS crushes high-contrast markings, and the input is exonerated

The magpie came back as an all-black bird — a crow, not a magpie. Traced properly rather than guessed at, because the fix differs completely depending on where it happened:

  1. The input is provably perfect. The bg_remove_local cutout fed to TRELLIS has a bright white nape, white wing patches, a white tail tip and a pale blue-grey beak. Not the concept's fault, not RMBG's.
  2. The raw 500 k TRELLIS mesh has already lost it (docs/shots/laneE/r38_magpie_albedo_probe.png): the white nape survives only as a pale blue-grey wash and the wing bar and tail tip are gone. Its 1024² albedo atlas is near-uniformly black with sparse pale slivers. So the loss is the operator's PBR bake — our normalize/decimate/re-bake chain is exonerated.
  3. The documented workaround does not work through our normalizer. The manifest advertises baker: "vertex" as "the 1s clean-albedo bake, cleanest colors". Re-ran the magpie with it (159 s): the GLB comes back with 0 materials and 0 images — colour lives only in vertex attributes, and normalize.py (join → decimate → re-export) drops them. The result renders pure white. baker: vertex is unusable here without normalizer work.

Shipped anyway, flagged honestly, because the silhouette is what a swooping bird reads as at street distance and 1.3 cannot ship without a mesh. Fix path for a later round (stated, not done): bake the LP's albedo from the concept image rather than from the reconstructed mesh, or hand-tint the nape/wing/tail regions of the 512² baked atlas. Both are cheap; neither is this round's job.

5. REUSE BEFORE GENERATING — the review's nine, re-measured, and its "one line each" corrected

All nine confirmed idle and re-measured off the bytes. But "one line each" is wrong for four of them, and the reason is in furniture.js's own merge contract: extractGLB merges every mesh into ONE geometry and keeps only the FIRST material. So a multi-material GLB does not fail — it renders entirely in material #1, silently.

id tris mtl verdict
novelty_record 8 000 1 true one-liner. furniture.js:275 already calls glb('novelty_record',…); GLB_FILES has no key, so the call can never fire. The blocking comment at furniture.js:30 says "26.5k tris" and is STALE — Lane E baked it to 8 000 in R5.
streetlight 4 999 1 ⚠️ decimate first. It is placed every ~18 m — at ~50 instances that is ~250 k tris on a 200 k budget. The review is right that it needs decimate_props.py to ~800 before it is wired.
bench_modern 640 1 true one-liner
longbench 1 172 1 true one-liner. 5.86 m — the arcade/market feature bench
park_bench 5 000 2 ⚠️ renders in material #1 only
bench_wood 3 999 0 ⚠️ zero materials → falls to extractGLB's grey 0x888888 default
coffee_table 495 2 ⚠️ material #1 only
box_crate 78 3 ⚠️ material #1 only
counter (balcao) 276 1 1.06 × 4.02 m, 1.10 m — exactly the bar_counter fitting still primitive-only in all three venue types. Different loader (interiors/glb.js), not furniture.js.

Nothing was re-generated that already existed. No duplicate of any of these was made this round.

6. LICENCE — every R38 asset lands 🟢

class count zone method
audio 6 🟢 original by construction pure numpy synthesis, gen_audio.py, deterministic via crc32(name)
ground skins 4 🟢 original flux_local / FLUX.2-klein-4B (Apache-2.0, ungated), on-device
meshes 3 shipped 🟢 CC0-equivalent (AUDIT.md:101 doctrine) flux_local concept → bg_remove_local (RMBG-2.0) → trellis2_mlx (TRELLIS.2-4B) → normalize.pybake_lowpoly.py

The 3GOD depot was NOT used as a pool — it stays a read-only archive (971 live, 53 recorded, known trademarked items inside). The hardyards set stays REJECTED on provenance (39 GLBs, provenance documented for one file); every mesh here is ours. $0 spent — no cloud key touched.

7. SESSION 2 — the depot step, the magpie tint, and the sizing law

Everything in §7 is measured on this box after 1568c9b; nothing in §1§6 is retracted.

7a. PUBLISHED — three GLBs, verified by fetching the bytes back off the PUBLIC depot

Pushed under John's R22 standing authorization for Lane E assets (the same authority as sit.glb in R28 and the nine R36 sweep assets) through the passwordless tailnet ingress (GOD3_DEPOT=http://100.94.195.115:8788, decision #4 — no secret read, no cookie, publish.py never logs one). _published.json 53 → 56; post-publish drift check ✓ all 56 recorded assets live. Thumbs uploaded with each (glb:200 thumb:200).

Then verified the way this lane always verifies: fetch it back off the public host and compare hashes, not "the upload said 200".

file HTTP local B depot B sha1 (identical both sides)
procity_street_magpie_01.glb 200 132 592 132 592 b5f02b26d15f7a58f8761f54fdb98b14fa65ee10
procity_street_paling_fence_01.glb 200 165 752 165 752 a580a8833f02bccb2433776072bca2d4fbee8050
procity_street_water_tank_01.glb 200 77 152 77 152 9b08fb9774e892f04d5c36a09ab2100383f4e360

_r38_results.PENDING_PUBLISH.json_r38_results.json, build_manifest.py re-run — manifest GLBs 33 → 36, all three under furniture. validate_manifest.py: 0 errors, 0 warnings.

7b. THE MAGPIE TINT — a 3-D repaint of a 2-D atlas, because the atlas is unreadable by eye

§4d left the bird an all-black crow with the loss traced to trellis2_mlx's PBR bake. The stated fix path ("hand-tint the 512² atlas") is not a paint job: the atlas is a smart-UV bake, ~100 scattered islands, and no island announces itself as a nape. New tool pipeline/tint_atlas.py:

  1. rasterize every UV triangle into the atlas grid → texel → (position, normal) maps (barycentric, glTF top-left UV origin, 1-texel conservative pad) — 101 086 / 262 144 texels covered, 38.6 %;
  2. evaluate soft anatomical windows on the POSITIONS in the mesh's own metric frame (x ±0.0777 lateral · y 0→0.3583 up · z 0.2021 tail → +0.2021 beak): white nape behind a black crown, white shoulder/wing bar on the flanks only so the dorsal midline stays black, white rump + tail base stopping short of the black terminal band — Australian magpie (Gymnorhina tibicen), adult, black-backed form;
  3. paint with the bake's own luminance kept as shading (0.88 + 0.12·smoothstep(luma)), so it is white with the bake's light in it, not a flat decal;
  4. dilate 5 texels into the bake's UV padding, or bilinear/mip at an island edge would pull the old black through;
  5. re-embed as WebP and rewrite the GLB. The image bufferView is verified last in the buffer, so geometry is untouched byte for byte.

Because the masks live in 3-D, the soft edge is consistent across every island simultaneously — a 2-D blur cannot do this at all (it would bleed island-to-island through the black background).

Result, measured: painted texels' median luma 0.843 (p90 0.949) vs the bake's 0.133; whole atlas 0.075 → 0.207. GLB law re-checked after the rewrite: 894 tris · 1 mesh · 1 material · 1 image · 512² WebP · no Draco · 0.155 × 0.358 × 0.404 m — every number unchanged.

Verified in the game's own renderer, not just in Blender. The published file was loaded off https://digalot.fyi/3god through the vendored GLTFLoader + three.js (MeshStandardMaterial, hemisphere + sun, sky background): 1 mesh, 894 tris, 512×512 texture decoded — which also proves the PIL-written WebP decodes in a browser. Sheet: docs/shots/laneE/r38_magpie_tint.png — before/after × 5 angles (EEVEE, the GLB's own material incl. the 0.28 emissive) + a 64 / 48 / 32 px distance strip (a 0.36 m bird at 310 m on a 1080-high screen is 50100 px) + the three.js frame.

VERDICT: it reads at the size and speed the bird is seen. Pied and unmistakable at 64 px and 48 px from flank and rear-quarter; still clearly black-and-white at 32 px. Honest caveat: from directly below-and-in-front it stays dark — correct behaviour, because a real magpie's bib and belly ARE black; the white is nape, shoulders and rump, i.e. what you see as it comes over and as it leaves. Thumbnail re-rendered from the rear quarter of the same bake_lowpoly camera rig (render_views.py --thumb --rear): the roster's standard front-quarter looks straight at a magpie's black face and would have recorded none of the plumage.

Standing after Lane B's 4514cb9: the tinted GLB is an UPGRADE PATH, not a dependency. B shipped a 182-tri procedural bird with the marks as vertex colours, fetching nothing — 4.9× lighter, and the right call. The GLB is on the depot and in the manifest; nothing probes for it and nothing should (R26's blind-probe ruling). Cost if ever swapped in at birdGeometry() / birdMaterial(): +1 draw, +894 tris at one instance.

7c. THE SIZING LAW — paling_fence is a good asset and a rejected one

Lane B's measurement: 5 runs × 181 lots = 905 instances × 1 121 tris = 1.01 M triangles against ~75 k spare (~47× over), and a GLB cannot ride the boxMats InstancedMesh that makes the other yard props +0 draws. REJECTED; it ships as a primitive. water_tank (500 tris, one per yard, low count, replacing a 24-tri pair) is ACCEPTED.

The law for every later asset round: a mesh's triangle budget is tris × the instance count the placement rule implies, not "≤5 k like a prop". A fence panel repeated on every lot boundary is in a different budget class from a one-per-town water tank, and the number to beat is set by the placement rule, not by the asset. Get the instance count from the consuming lane BEFORE generating anything that tiles, edges, or repeats per-lot. The published fence stays on the depot and in the manifest — harmless, provenance-recorded, available if a low-count use ever appears.

7d. New pipeline tools (all $0, on-device)

tool what it does
tint_atlas.py the 3-D-region atlas repaint above; --debug renders the region masks as flat colours so the anatomy can be checked before any paint lands
render_views.py Blender view sheet from the angles an asset is seen from (flank / rear-quarter / swoop / top / rear), --eevee for the GLB's own material under sky, --thumb [--rear] to re-render a roster-consistent 256 px thumbnail
view_sheet.py composites those into one labelled sheet + the distance strip — the check no 256 px thumbnail can make

Licence unchanged: 🟢. The tint is our own numeric edit of our own baked atlas; no asset, no model and no third-party image entered the pipeline for it. $0 — no cloud key touched.

Round 39 — the magpie A/B, and the arcade's kit (v9 39.4)

Every number below was measured on this box today. $0 — no cloud key touched, no third-party asset or image entered the pipeline, and the 3GOD depot was not read as a pool (it stays the read-only archive R38 declared it).

0. STANDING INSTRUCTION — the sizing law, promoted out of the round notes

Before generating anything that tiles, edges, repeats per lot, or is placed by a rule rather than by hand: get the instance count from the CONSUMING LANE. The budget is tris × that count, not "≤5 k like a prop". R38 paid for this with paling_fence — a good 1,121-tri panel that became a rejected asset at 5 runs × 181 lots = 905 instances = 1.01 M triangles. Two clauses were added in R39 and both are now part of the law:

  1. Draws are counted per CLASS, and where the class lives decides everything. A GLB cannot ride boxMats (BoxGeometry, one shared untextured shell material), so every distinct GLB geometry is +1 draw. As ONE town-wide InstancedMesh (magpie.js's pattern) that is +1 total; as a per-chunk furniture class it is +1 × up to 25 live chunks, which against 8 draws of margin is a breach, not a cost. Ask which one before generating.
  2. For a SKIN the budget is not bytes, it is the SLOT. skins.js:facadeAtlasUV has 36 slots (6×6); past 36 it console.warns and reuses one, i.e. a 37th facade skin silently repaints an existing shop. Measured: the synthetic uses 22 distinct facade skins (21 at seed 123456) ⇒ 14 free. That is the real budget line for a facade skin.

The law is now runnable rather than conversational: node pipeline/arcade_measure.mjs reads Lane A's own generatePlan and prints the counts. It changed this round's deliverable — one of the four commissioned assets stopped being an asset (§3, item 4).

1. THE MAGPIE A/B — rendering Lane B's own geometry, not a re-implementation of it

The R38 debt was "two magpies exist and nobody has seen them side by side." The trap in settling it is that a hand-ported strawman proves nothing, so B's bird is not ported:

  • pipeline/dump_bird.mjs imports web/js/world/magpie.js unmodified and calls the real createMagpie(). The bare three / three/addons/ specifiers its imports use are resolved to the repo's own vendored build (web/vendor/three.module.js) through a node:module registerHooks resolver — the importmap from index.html:22-25, as a node resolver. The vertices dumped are the vertices the browser gets. Measured: 182 triangles, 216 verts, bbox 0.680 × 0.166 × 0.670 m. The arithmetic agrees independently: sphere(8,6) = 8×(2·62) = 80 · sphere(6,5) = 48 · cone(5) = 5 side + 5 cap = 10 · 3 boxes = 36 · 4 planes = 8 ⇒ 182. magpie.js:9 and :123 say 154 and are stale.
  • pipeline/bird_to_glb.py wraps the dump as a GLB in E's frame (glTF +Y up, head at +Z; three's frame is +Y up, nose at Z, and Blender's export_yup writes glTF = (Bx, Bz, By), so the Blender vertex is (xt, zt, yt) — a 180° yaw, det +1, winding preserved). Without that the two rows of the sheet would be shot from opposite ends of the bird. --fold applies the perch pose (place(..., folded) scales the instance X by 0.42). glb_stat.py re-measures 182.
  • pipeline/render_views.py --noemit strips emission from both candidates, and this is the correction that makes the picture usable at all. normalize.py gives every baked GLB emissiveFactor 0.28 with an emissiveTexture — 0.28 × its own albedo, so blacks stay black and whites brighten. A vertex-coloured mesh cannot express that in glTF (COLOR_0 multiplies base colour only), and Blender's exporter silently writes a flat 0.28, which lifted B's black bird to grey and produced a first sheet that was worthless in E's favour. Off both sides. The consequence, stated on the sheet: a baked GLB reads slightly better in game than it does here — the safe direction to be wrong in.
  • pipeline/view_sheet.py --ab lays the distance strip out as a grid (one line per candidate, same view and same pixel size stacked) and repeats it under linear motion blur — a true N-position average, not a gaussian, with the smear length scaled to the SUBJECT's on-screen width. Two levels, both derived: 25 % (the approach) and 100 % (the pass — the swoop covers 34 m in 1.25 s = 27.3 m/s = 0.45 m per 60 fps frame ≈ one body length).

The measurement that decided it (dump_bird.mjs --sim, magpie.js's own clock: DEFEND_R 34 m, drawn out to 74.8 m, COOLDOWN 5.5 s, SWOOP_T 1.25 s, RETURN_T 1.9 s; a 60 s walk at WALK 4.6 m/s passing 4 m off the perch):

the bird is drawn in 1,949 of 3,600 frames (54.1 %) — and of those it is perched 1,569 (80.5 %) · swooping 150 (7.7 %) · returning 230 (11.8 %).

And the mechanism, which is why no re-tint could have saved B's bird. Three of its four white marks cannot be seen from where the player stands:

mark magpie.js why it does not read
wing bars ×2 :145-148, bar plane at y 0.048, wing plane at y 0.045 both horizontal (rotateX(-π/2)), both DoubleSide, and the bar's footprint (x 0.1740.316, z 0.0050.055) lies entirely inside the wing's (x 00.34, z 0.0850.065) ⇒ from underneath the black wing occludes its own white bar, and the player is always underneath (perch 4.32 m vs EYE 1.62 m)
nape :140, box 0.052×0.014×0.055 at y 0.06, z 0.10 at z 0.10 the head sphere's surface is y 0.0728; the box tops out at 0.067 ⇒ buried, and only its rear ~12 mm (z > 0.085) ever clears both head and body
nape + rump, from the flank :140-141 horizontal slabs 14 mm and 20 mm thick on a 166 mm tall bird. At 64 px for a 0.67 m bird one pixel is ~10 mm ⇒ 1.3 px and 2 px of white — and the flank is 80 % of the frames

VERDICT — Lane E recommends TAKING THE GLB, with the cost stated: +712 tris at one instance, draws a wash (both are one town-wide InstancedMesh at count 0 or 1), +1 fetch / +1 material / +1 512² texture, and it is a use-if-ready second path, not a substitution — ?noassets and ?classic still need the procedural bird. No blind probe is implied: magpie has been in manifest.json and on the public depot since R38 §7a, so this is furniture.js's existing shape. Sheet: docs/shots/laneE/r39_magpie_ab.png. Fable rules.

2. THE ARCADE, MEASURED (pipeline/arcade_measure.mjs, 5 seeds, off Lane A's generatePlan)

measured
lane ONE edge kind:'arcade', 42.00 m × 5 m, 2 blocks
shops 1617 (17 at 20261990 / 20260101; 16 at 7 / 123456 / 999)
frontage 3.004.94 m, mean 3.874.05 m; depth 5.107.99 m; 411 two-storey
walkable lot faces at ±2.50 m ⇒ 5.00 m
roof AWNING_DEPTH 2.2 from each face ⇒ slabs reach ±0.30 m ⇒ roofed except a 0.60 m centre slot, ceiling y 2.95 m
floor ground.js's arcade branch: ONE quad 42.00 × 12.00 m = 504 m², wearing footSkin
⚠ posts buildings.js:643 at d/2 + 2.2 0.2±0.50 m from the centreline: two post rows 1.00 m apart down a 5.00 m lane. No collider, so it walks — it just reads as a colonnade in its own doorway. → Lane B

Synthetic-only as a code rule: plan_osm.js:305 is one unconditional addDistrict('mainstreet') ⇒ no cache town has an arcade DISTRICT, while the corpus has 1,102 arcade-KIND edges = 39 km of ordinary suburban footpath. Key on district.kind, never edge.kind.

3. THE KIT — ranked by value per draw, because the margin is eight

# asset form instances cost licence
1 facade-shuttered facade skin 768×595 12 shops +0 draws, +0 tris, 1 of 14 free atlas slots 🟢
2 ground-arcade-floor ground skin 512² 1 class +1 draw town-wide, +0 tris (the 504 m² quad moves class) 🟢
3 ground-arcade-roof ground skin 512² 1 quad +1 draw in the 12 arcade chunks, +2 tris 🟢
4 hanging blade signage no asset — a refusal with a better answer 17 +0 draws, +0 assets, +4 tris/shop = 68

Item 4 is the most useful line in the table. buildings.js already draws each shop's sign into a per-chunk 512×168 canvas atlas cell and pushes ONE quad into the merged signQuads mesh. Push a second, perpendicular quad with the same signUV.get(shop.id) rect and the arcade gets 17 projecting signs carrying each shop's real name, riding a mesh that already exists: +0 draws, +2 tris single-sided / +4 back-to-back = 68 triangles for all 17. The bracket is two boxMats boxes, also +0 draws. A GLB blade sign at 17 instances cannot beat that; it can only look different.

Aspect is not decoration. skins.js paints every facade into a square 341 px atlas slot, and arcade lots are 3.87 × 3.0 m ⇒ 1.29:1, against the main-street pool's 1.78:1 — a 38 % horizontal stretch if shot to the pool. facade-shuttered is therefore shot at 1.29:1 and lands nearly undistorted (1024×768 → square → 1.29:1 quad ≈ 1.03 net). Recorded; not retro-fitted to the other 48.

use values that name no slot — this time on purpose. R38 recorded gravel/reddust as selected by nothing by accident. arcade-floor and arcade-ceiling are selected by nothing with the price printed: the arcade cannot ride footpath (one town-wide merged mesh — re-using it lays terrazzo over every footpath in town), so each needs its own class. Measured for the ceiling: arcade lots span exactly 2 chunks on all five seeds (0,0 and 0,1, CHUNK 64 m).

⚠ THE CLASSIC ARGUMENT IS INVERTED HERE relative to R38, and reusing R38's would move a golden. R38's ground palette is classic-safe by absencecharacter.js:83-87 returns frozen v1 literals for a null town key and classic has none, so that path contains no if (classic) at all. The arcade district exists ONLY on the synthetic, and the synthetic IS the classic boot, so every piece of this kit lands on the covenanted town and needs an explicit flag test: ground.js:41-47's VERGE shape, where ?classic=1 forces it off.

4. pipeline/seamless_tile.py — and the guard it needed

A tiled floor is the one skin class where the repeat seam is a mistake rather than a texture: grass hides a joint, terrazzo does not, and the arcade floor is 504 m². The tool finds the pattern's pitch by autocorrelating the mean-absolute column/row gradient (a grout line is a gradient spike) and crops to a whole number of periods — arithmetic instead of feathering, which would smear the grout.

It needed a guard, because the naive version made things worse and said nothing. Measured:

skin pitch found (corr) seam before → after kept
ground-arcade-roof x 212 px (r 0.25) x 26.7 → 8.3 x cropped to 848
ground-arcade-floor x 196 (r 0.26) / y 108 (r 0.30) x 27.9 → 31.9, y 29.6 → 33.4 rejected, passed through

So the crop is now a proposal that has to beat the number it is trying to improve, per axis, and the tool prints NO IMPROVEMENT AVAILABLE, passed through when it cannot. The finding underneath it: a diffusion-drawn tiled floor is not periodic enough to cut on — the model draws tiles with perspective and irregular spacing, so the autocorrelation peaks weakly (r ≈ 0.26) and at the wrong lag. Fix path for a later round, stated not done: synthesise the terrazzo procedurally in numpy (the gen_audio.py doctrine — original by construction, exactly periodic, 🟢), instead of photographing it with a diffusion model. The shipped floor is the flux one, consistent with the ten photographic grounds already in the pool, seam measured and recorded rather than hidden.

5. LICENCE — every R39 asset lands 🟢

class count zone method
skins 3 🟢 original flux_local / FLUX.2-klein-4B (Apache-2.0, ungated), on-device
meshes see §6 🟢 CC0-equivalent (AUDIT.md:101 doctrine) flux_local concept → bg_remove_local (RMBG-2.0) → trellis2_mlx (TRELLIS.2-4B) → normalize.pybake_lowpoly.py
tooling 4 files dump_bird.mjs, bird_to_glb.py, arcade_measure.mjs, seamless_tile.py — all ours

$0 spent. No cloud key touched. The hardyards set stays REJECTED on provenance (R38), and the 3GOD depot stays a read-only archive.