diff --git a/F-progress.md b/F-progress.md index 85a4d2f..68f3243 100644 --- a/F-progress.md +++ b/F-progress.md @@ -4,6 +4,55 @@ --- +## Round 24 (v5.0-alpha — THE ALPHA COMPLETION) — **the crate WALKS; F held the tag on A's gate** + +**THE CRATE WALKS.** Boot `redhill_godverse`, walk down Musgrave Road into **Monster Robot Party**, +riffle **its own real crate**, buy a real record at its real price. Proven falsifiably: room base = +`assets/stock_godverse/3962749/` · **titles 120/120 in its atlas, 0/120 in the generic pack** · **the +GPU samples `stock_godverse/3962749/stock_record_atlas_0*.webp`, 0 textures from the generic pack** · +bought *"Abstract Latin Lounge II (Part II)"* **@ $15**, cash **191 → 176**, exact debit, count 0→1 · +reboot restores 191/0 (gone-this-session, back next boot). Ladder re-proved: `?noassets` → **zero +network requests**; keyed-but-atlas-less shop 2289 → 404 → **parody, live**; `newtown_real` → base +`null`, 350 items — **F's wire is a no-op off the godverse path**. Full record: `LANE_F_NOTES` §24. + +**F held `v5.0-alpha` anyway. The tree is red — 156,193/156,201 — and none of it is the product.** +2 reds are goldens G's re-emit legitimately moved (A ran *before* G, so A could not pin them). 6 are +**conflict #2: A's identity sweep contradicts Fable's own standing Ruling 2**, which *mandates* mixed +caches ("merge census heroes **WITH** the donor's texture shops"). A cafe from OSM has no GODVERSE +identity and never will. G measured it, escalated it to "A's call, Fable's ruling", nobody ruled, and +it arrived at F. + +**F measured the hazard that gate guards: it does not exist.** 10/10 and 18/18 unique ids, **zero +duplicates**; and partial keying does not mis-stock — F watched 2289 fall soft to parody in the +browser, exactly as charter risk #3 prescribes. **A's own two checks disagree** — `validateTownCache` +reasons it correctly ("Missing = tier 0 (soft). Duplicate = mis-stocked (hard). They are not the same +failure") and warns; the sweep hard-fails the same condition. + +**Why F didn't just fix it: F is the party that benefits.** Changing a red gate to green, in another +lane's file, to unblock F's own tag, on the one check guarding "never mis-stocked" with Monster Robot's +real crate as the prize — that move would retroactively devalue every green F has ever reported. The +cost of waiting is an hour. Recommended (A's call): the sweep follows A's own validator; A pins the +four goldens in the same commit; F re-gates and tags. One commit. + +**Four findings:** +1. **The honest-gate spec was itself vacuous.** The brief specified id equality — but both packs number + items **positionally** (`rec_0000…`), so **all 120 atlas ids are also generic-pack ids**. An + id-equality gate goes green on the generic pack: the exact failure the vacuous-gate law was written + to stop, reproduced inside its own first application. Titles (0 overlap) and the **texture URL** — + the file the pixels come from — are what discriminate. F's gate asserts on those. +2. **F's own selector was lying about the epoch's shop.** `DBG.enterShop(3962749)` → "no shop matching": + a GODVERSE shop has two ids and F's R22 selector knew only the plan id. Fixed. +3. **A's uniqueness check is a silent risk-#3 hole** (new — G didn't catch this): on a mixed cache every + texture shop maps to `undefined` and they **all collapse into one Set entry**, so it fails on the + undefineds and **cannot detect a real duplicate**. Fix it *over the defined ids* — that makes the + guard stronger, not weaker. +4. **The licence flag stopped reaching a human.** E's validator now sees the atlas (R23's blind gate is + closed ✓) but prints `provenance.licence` (British) while G now emits `license` (American, per C) — + so `FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE`, on real product photos, **silently never prints** + while the gate says OK. E, one line, G already specified it. + +--- + ## Round 23 (v5.0-alpha — THE FIRST REAL CRATE) — **F HELD the tag** **`v5.0-alpha` is NOT tagged. The crate is real; it has no street.** G's 120 real records off a real diff --git a/docs/LANES/LANE_F_NOTES.md b/docs/LANES/LANE_F_NOTES.md index 3b97545..15362ce 100644 --- a/docs/LANES/LANE_F_NOTES.md +++ b/docs/LANES/LANE_F_NOTES.md @@ -960,3 +960,155 @@ keying) → F (the wire + re-gate)**, and the crate walks. own R17 wiring gates the cache fetch on `!NOASSETS`, correctly (noassets = zero network), but the tier ladder's *"same shop under `?noassets`"* can't mean the same shop for a real town. Tier 0 for **stock** is provable on the default boot; tier 0 for the **town** is a fixture fallback by design. Wording, not a bug. + +--- + +## 24. Round 24 — v5.0-alpha THE ALPHA COMPLETION. **The crate WALKS. F held the tag anyway — A's gate.** + +**THE CRATE WALKS.** You can boot `redhill_godverse`, walk down Musgrave Road into **Monster Robot +Party**, riffle **its own real crate**, and buy a real record for its real price. Proven falsifiably, +not by eyeball: + +| assertion | result | +|---|---| +| room resolved base | **`assets/stock_godverse/3962749/`** (not the town-wide base) | +| **title equality** | **120/120 in Monster Robot's atlas · 0/120 in the generic town pack** | +| **texture provenance** (the pixels) | **2 atlases, both `stock_godverse/3962749/stock_record_atlas_0*.webp` · 0 from the generic pack** | +| network | `stock_godverse/3962749/` index + both atlases, `200` | +| **buy** | `rec_0000` *"Abstract Latin Lounge II (Part II)"* @ **$15** → cash **191 → 176**, debited exactly 15, count 0→1 | +| gone-this-session → back next boot | reboot ⇒ wallet 191/0 restored (no persistence layer exists) | + +**Tier ladder, re-proved end to end:** tier 0 `?noassets` on a godverse town → **zero network requests**, +no base, no pack (the offline law survives the wire) · **soft fall live**: `Restoration Station` +(keyed 2289, no atlas) → `404` → null → parody, and the boot log shows those 404s happening in +production · **no cross-contamination**: after visiting other shops the real crate still resolves its +own 120 · **plain towns untouched**: `newtown_real` → 0 keyed, base `null`, 350 items, textures from +`models/` — **F's wire is a complete no-op off the godverse path**, byte-identical to pre-v5. + +### What F built (#6a) +`godverseBaseFor(shop)` — **exported and used by both** the shell's boot preload and the room's lookup, +deliberately: C's cache identity is the LITERAL `(type, base)` pair, so two sides building that string +differently would miss the cache and drop a real shop to parody — a bug that looks exactly like "no +stock" and would sail straight past a covers-render eyeball. One function, one string, one key. +Boot-preloads keyed shops (`getStockPack` is sync and the room is built the instant the door opens, so +a pack first requested at `enter()` is not resolved in time), gated on `stockReal` and never on the id +alone, so `?noassets` fetches nothing. + +### ⚠ FINDING 1 — the honest-gate spec was itself vacuous. The law's first application broke the law. +The brief specified #7a assert **"rendered titles ∈ the atlas index (id equality, not 'covers render')."** +**Id equality cannot discriminate.** Measured: both packs number their items **positionally** — +`rec_0000, rec_0001, …` — so the atlas's ids are `rec_0000..rec_0119` and the town-wide pack's are +`rec_0000..rec_0349`. **All 120 atlas ids are also generic-pack ids (120/120 overlap).** An id-equality +gate goes **green on the generic pack** — the exact failure the law was written to stop, reproduced +inside the fix for it. *Ids here are slot numbers, not identity.* +**What actually discriminates:** titles (**0 overlap**) and, strongest, the **texture URL** — the file +the GPU samples. A pack that merely looks right cannot fake the URL its pixels come from. F's gate +asserts on both, and `DBG.stockInfo()` exposes them so the assertion is scriptable, not eyeballed. + +### ⚠ FINDING 2 — F's own selector was lying about the epoch's shop +`DBG.enterShop(3962749)` returned **`no shop matching`**. A GODVERSE shop has *two* ids — its plan id +(local, renumbered per town: Monster Robot is **8**) and its `godverseShopId` (**3962749**, the real POS +id, which is what the atlas, every G-side query and every cross-repo bug report names it by). F's R22 +selector only knew the first. Fixed (plan id still matches first, so no existing call shifts). *The +harness F shipped for Lane G could not name the one shop the epoch is built on.* + +### The 8 reds — F did NOT clear them, and did not touch A's file +`156,193/156,201`. They are **not** product defects; F proved the product correct above. They split: + +**2 — goldens moved by G's re-emit** (`newtown_godverse` base `0xcf69b387`, gig `0x1e266f60`), exactly as +ledger #2 intends: adding `godverseShopId` changes the cache, so it changes the plan. A ran *before* G, +so A could not pin what G had not yet emitted. Plus 4 UNPINNED warns (`redhill_godverse`, +`redhill_real`). **F did not re-pin**: if conflict #2 resolves by changing the caches, these move again, +and the amendment law wants the spec change and the pin in ONE commit. Pin them with the resolution. + +**6 — conflict #2: A's identity sweep contradicts Fable's own standing Ruling 2.** Ruling 2 +([FABLE_TO_LANE_G.md:33](FABLE_TO_LANE_G.md)) *mandates* mixed caches — "merge census heroes **WITH** the +donor's texture shops (cafes/bakeries…)". A cafe lifted from OSM has no GODVERSE identity and never will; +minting one would invent identity, which is charter risk #3 itself. A's sweep asserts +`keyed === cache.shops.length` — i.e. that no such cache may exist. It is a gate authored in the same +wave as the thing it gates, with no mixed cache in existence to test against: **the identical species as +E's blind validator in R23**, one round later, in the opposite direction. G measured it and escalated it +to "A's call, Fable's ruling"; nobody ruled; it arrived at F. + +**F's measurement of the hazard the gate guards: it does not exist.** `redhill_godverse` 10 keyed / +**10 unique**; `newtown_godverse` 18 keyed / **18 unique**; **zero duplicates**. And partial keying does +not mis-stock — F watched shop 2289 fall soft to parody in the browser, which is precisely what charter +risk #3 prescribes ("fail-soft to tier 0, never mis-stocked"). **A's own two checks already disagree**: +`validateTownCache` reasons it out correctly — *"Missing = tier 0 (soft). Duplicate = mis-stocked (hard). +They are not the same failure"* — and warns; the sweep hard-fails the same condition. + +### ⚠ FINDING 3 — the uniqueness check is a silent risk-#3 hole (new; G did not catch this one) +`ok(new Set(seated.map(s => s.godverseShopId)).size === seated.length, 'godverse ids unique')`. On a +mixed cache every texture shop maps to `undefined`, and **all of them collapse into ONE Set entry** — so +the check fails on the undefineds, *not* on a duplicate, and it **cannot detect a real duplicate** while +any shop is unkeyed. It is the arm A called "the arm that earns its keep" for charter risk #3, and on +every cache Ruling 2 mandates it is **structurally incapable of doing its job, in both directions**. +Whoever fixes the coverage assert must not paper over this one: uniqueness belongs **over the defined +ids** (`seated.filter(s => s.godverseShopId !== undefined)`), where it would actually bite. That change +makes the risk-#3 guard **stronger**, not weaker. + +### Why F held the tag — and why this hold is not R23's +R23 held because the **product** was broken. **R24's product is finished and proven.** What is red is a +**gate**, in **Lane A's file**, on a **schema question that is A's to answer** ("what marks the godverse +layer?" has at least three valid designs). F did not resolve it, for one reason above all: **F is the +party that benefits.** F changing a red gate to green, in another lane's file, to unblock F's own tag, +on the one check guarding "never mis-stocked" with Monster Robot's real crate as the prize — that move +would retroactively devalue every green F has ever reported. F's only asset is that F never shades a +result. The cost of waiting is an hour; the cost of F being quietly wrong is the charter's worst risk. +**Recommended (A's call): the sweep follows A's own validator — missing ⇒ warn (tier 0, soft), duplicate +⇒ error (hard, over defined ids), coverage asserted over the keyed layer, not the inherited one.** Then +A pins the four goldens in the same commit and F re-gates and tags. It is one commit's work. + +### ⚠ FINDING 4 — the licence flag stopped reaching a human, and the gate says OK +R23's blind validator is **fixed** — E's `validate_atlas` now finds and validates the atlas +(`3962749/stock_record_index.json · Monster Robot Party · items=120 atlases=2 px=2048 OK`). But G's +conflict-#1 leftover is real and still open: `validate_atlas.py:198` prints from +`provenance.licence` — **British only** — while G's re-emit carries **`provenance.license`** (American, +per C's corrected contract). So `lic = {}`, and the licence line **silently never prints**. E's *check* +(:105) accepts either spelling, so the gate goes **OK**; only the evidence vanished. + +That evidence is not decoration. E's own comment at :199 says *"the licence is PRINTED — clearing it is +a human call"* — it is the **mechanism** by which charter law #3 reaches a person, and the string it +failed to print is **`FLAG BEFORE ANY PUBLIC/COMMERCIAL RELEASE`**, on an atlas of a real shop's real +product photographs with real artist names. A gate that is green while the one thing it exists to +surface is invisible is the vacuous-gate law's own species, wearing the licence law's coat. +**Fix (E, one line, G already specified it):** `.get("licence") or .get("license")` at :198. + +**F did not patch it.** It is E's file, F is not blocked on it (the tag is held regardless), and G +already routed it. Filing beats reaching across a lane boundary for a one-liner — the same restraint F +applied to A's gate, for the same reason. + +### ⚠ FINDING 5 — F's own gate cried the loudest wolf we own (F's bug, F fixed it) +The classic-regression gate reported **`plan fingerprint != golden 0x3fa36874 — determinism/prime-law +broken`**. The covenant was never touched (`0x3fa36874`, node-side, unmoved; G said so too). F wrote +this gate in R16 with `if hash_ok and r.returncode == 0` — **two assertions, one verdict, reported with +the fingerprint's message** — so *any* unrelated red anywhere in Lane A's selfcheck (here: 8 +godverse-identity checks in a different town) screams that the byte-identical-forever covenant is +broken. Fixed: two assertions, two verdicts, and the selfcheck failure now names its own lane and +quotes its own reds. *A gate must name what it actually measured — the same law as the vacuous gates, +one turn further round: it isn't enough to fail for a reason, you have to fail for YOUR reason.* + +### ⚠ FINDING 6 — the id spaces collide, and it bit F's own selector inside the anti-vacuous gate +Plan ids are per-town (1..N); godverse ids are real POS ids (31, 767, 3962749). **They share a numeric +space.** Silky Oak Furnature's godverseShopId is **31** — and another shop's *plan* id is also **31**. +F's first cut at the R24 selector tried plan id first, so `enterShop(31)` silently entered **Wholefood +Cafe** instead. The smoke's soft-fall assertion then **passed on an unkeyed cafe** — a vacuous pass, +inside the gate F wrote to stop vacuous passes, in the round whose whole theme is *ids that aren't +identity*. Fixed both ends: `'g:'` selects a godverse id explicitly; a bare number stays the plan id +(back-compatible) and now **reports the collision** rather than choosing quietly; and the smoke enters +via `g:` and asserts the shop it landed in **is the one it asked for** before asserting anything about it. + +*The lift itself is clean* — F checked: cache 10 keyed → plan 9 keyed (Empire Revival didn't seat), every +one a real secondhand shop. **No cafe is keyed. There is no mis-stocking.** + +### ⚠ FINDING 7 — fail-soft-by-404 vs the 0-console-errors law (a contract gap, filed not papered) +F's boot preload must resolve a keyed shop's pack *before* the door opens (`getStockPack` is sync and +the room builds on the same tick), so it probes every keyed shop. 9 of Red Hill's 10 have no atlas → +`404` → null → parody. **That is the fail-soft law working**, and it is also **8 console errors on every +godverse boot** — a browser-level log no `.catch()` can suppress. The house law is zero. +**The tension is structural: you cannot probe blind and stay silent.** The fix is not to loosen the law +but to stop probing for what isn't there: **an atlas index in G's namespace** (or a `hasStock` flag on +the cache's shop entries, A+G) so the shell preloads exactly the shops that have atlases — 404s then +mean a genuinely missing file, which is what fail-soft is *for*. **Filed as a contract ask, not +invented.** Until it lands, F's smoke counts the probes, names them, and fails on any other error — +tolerance stated out loud, never blanket-ignored. diff --git a/tools/flags_check.py b/tools/flags_check.py index e8db607..b65b161 100644 --- a/tools/flags_check.py +++ b/tools/flags_check.py @@ -146,8 +146,19 @@ def classic_regression(p): capture_output=True, text=True) hash_ok = (f'{GOLDEN_HASH:#010x}'.replace('0x', '0x') in r.stdout) or (hex(GOLDEN_HASH) in r.stdout) \ or ('3fa36874' in r.stdout) - if hash_ok and r.returncode == 0: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} (selfcheck green)') + # [R24] These are TWO assertions and they get two verdicts. This gate used to require + # `hash_ok and r.returncode == 0` and report BOTH failures with the fingerprint's message — so + # any unrelated red anywhere in Lane A's selfcheck (R24: 8 godverse-identity checks in a + # different town) screamed "determinism/prime-law broken", the loudest alarm we own, about a + # covenant that was provably intact (0x3fa36874, node-side, unmoved). A gate must name what it + # actually measured; F wrote this one, so F fixed it. The covenant is asserted on its own. + if hash_ok: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} — the classic covenant holds') else: FAIL(f'plan fingerprint != golden {hex(GOLDEN_HASH)} — determinism/prime-law broken') + if r.returncode == 0: OK('Lane A selfcheck green') + else: + tail = [l for l in r.stdout.splitlines() if l.strip().startswith('✗')][:3] + FAIL('Lane A selfcheck RED (separate from the covenant above): ' + + ('; '.join(t.strip() for t in tail) if tail else 'see selfcheck output')) # winmap flips OFF under ?classic (it's default-on now); dig stays opt-in. The streamed roster IS the # v2.0 baseline (R7 flip / prime-law amendment) — so it must be ON under ?classic. @@ -992,6 +1003,105 @@ def smoke_gigs(p): b.close() +GODVERSE_TOWN = 'redhill_godverse' +GODVERSE_SHOP = 3962749 # Monster Robot Party, 147 Musgrave Rd, Red Hill QLD — the real shop +GODVERSE_BASE = f'assets/stock_godverse/{GODVERSE_SHOP}/' + + +def smoke_real_crate(p): + """R24 — THE REAL CRATE, gated so it cannot pass without touching its subject (the vacuous-gate law). + + R23's #7a would have gone green on "real covers render" while the room held the generic v2 pack and + stock_godverse/ was never fetched once. R24's brief then specified id equality — which ALSO cannot + discriminate, because both packs number items positionally (rec_0000…), so all 120 of the atlas's ids + are generic-pack ids too. Presence proves nothing; slot numbers prove nothing. This smoke asserts the + two things a wrong pack cannot fake: + 1. TITLE equality — the room's pack titles are the atlas's (0 of them exist in the generic pack). + 2. TEXTURE provenance — the URL the GPU samples IS the shop's atlas file. + Plus the rungs either side: a keyed shop with no atlas falls SOFT to parody, and a plain town is + untouched. Subject absent (no town/atlas) ⇒ explicit SKIP with the reason, never a quiet pass.""" + head(f'SMOKE: the real crate (R24 — {GODVERSE_TOWN} · Monster Robot Party · per-shop atlas identity)') + b, pg, errs = new_page(p) + missing = [] # atlas probes that 404 — the fail-soft path, counted not hand-waved + pg.on('response', lambda r: missing.append(r.url) if (r.status == 404 and 'stock_godverse/' in r.url) else None) + try: + pg.goto(f'{HOST}/assets/towns/{GODVERSE_TOWN}.json') + if 'godverse+osm' not in pg.content(): + print(f'· SKIP: {GODVERSE_TOWN} cache absent or not godverse+osm — subject absent, nothing asserted (Lane G/E)') + return + boot(pg, f'plansrc=osm&town={GODVERSE_TOWN}&stock=real&dig=1') + res = pg.evaluate("""async ({shop, base}) => { + const D = window.DBG, P = window.PROCITY; + const idxR = await fetch(base + 'stock_record_index.json'); + if (!idxR.ok) return { skip: 'atlas absent for ' + shop }; + const atlas = new Set(((await idxR.json()).items || []).map(i => i.title)); + const town = new Set((((await (await fetch('assets/models/stock_record_index.json')).json()).items) || []) + .map(i => i.title)); + const entered = D.enterShop(shop); + await new Promise(r => setTimeout(r, 1400)); + const si = D.stockInfo(); + if (!si || si.error) return { fail: 'no room: ' + JSON.stringify(si) }; + // a keyed shop with NO atlas must fall soft to parody, not inherit this crate + const other = (P.plan.shops || []).find(s => s.godverseShopId && s.godverseShopId !== shop); + let soft = null; + // 'g:' — NOT the bare number: plan ids and godverse ids share a numeric space, so a + // bare id can silently enter a different shop and pass this assertion for the wrong reason. + if (other) { D.exitShop(); await new Promise(r => setTimeout(r, 300)); + D.enterShop('g:' + other.godverseShopId); await new Promise(r => setTimeout(r, 900)); + const o = D.stockInfo(); + soft = { name: o.name, gid: o.godverseShopId, want: other.godverseShopId, + packItems: o.packItems }; } + return { + entered, base: si.base, name: si.name, gid: si.godverseShopId, + inAtlas: si.packTitles.filter(t => atlas.has(t)).length, + inTown: si.packTitles.filter(t => town.has(t)).length, + packItems: si.packItems, + fromAtlasTex: si.texUrls.filter(u => u.includes(base)).length, + fromTownTex: si.texUrls.filter(u => u.includes('assets/models/stock_')).length, + soft, + }; + }""", {'shop': GODVERSE_SHOP, 'base': GODVERSE_BASE}) + if res.get('skip'): + print(f"· SKIP: {res['skip']} — subject absent, nothing asserted (Lane G)"); return + if res.get('fail'): + FAIL(f"the crate: {res['fail']}"); return + print(f" room: {res['name']} (godverse {res['gid']}) → base {res['base']}") + OK(f"resolves its OWN base ({res['base']})") if res['base'] == GODVERSE_BASE else FAIL(f"base is {res['base']}, expected {GODVERSE_BASE}") + OK(f"TITLE equality: {res['inAtlas']}/{res['packItems']} titles are the shop's own atlas") \ + if (res['packItems'] > 0 and res['inAtlas'] == res['packItems']) \ + else FAIL(f"TITLE equality: only {res['inAtlas']}/{res['packItems']} titles are the shop's atlas") + OK(f"TITLE equality: 0/{res['packItems']} titles from the generic pack — the crate is not the v2 pack") \ + if res['inTown'] == 0 \ + else FAIL(f"TITLE equality: {res['inTown']}/{res['packItems']} titles came from the GENERIC pack") + OK(f"TEXTURE provenance: {res['fromAtlasTex']} tex from the shop's atlas, {res['fromTownTex']} generic — the pixels' own file") \ + if (res['fromAtlasTex'] > 0 and res['fromTownTex'] == 0) \ + else FAIL(f"TEXTURE provenance: {res['fromAtlasTex']} atlas / {res['fromTownTex']} generic — the GPU is sampling the wrong file") + if res.get('soft') is not None: + sf = res['soft'] + # the subject must BE a keyed shop, or this assertion proves nothing (it passed on an + # unkeyed cafe first time round — a vacuous pass inside the anti-vacuous-pass gate) + if sf.get('gid') != sf.get('want'): + FAIL(f"soft fall: entered the WRONG shop — wanted godverse {sf.get('want')}, got {sf.get('gid')} (\"{sf['name']}\")") + elif sf['packItems'] == 0: + OK(f"soft fall: keyed-but-atlas-less \"{sf['name']}\" (godverse {sf['gid']}) → parody, not this crate") + else: + FAIL(f"soft fall BROKEN: \"{sf['name']}\" inherited {sf['packItems']} items") + # Console errors: the ONLY tolerated ones are the atlas probes that 404 — that IS the fail-soft + # path (a keyed shop with no atlas → null → parody), and it is counted and named here, never + # waved through. Any other error fails. The 404 noise itself is a real cost of probing blind; + # the fix is an atlas index in G's namespace so we never ask for what isn't there (F, R24 §24). + probes = len(missing) + if probes and len(errs) == probes: + print(f' · {probes} atlas-probe 404(s) — the fail-soft path, expected; 0 other console errors') + print(f' (cost of probing blind; fix = an atlas index so we never probe an absent shop)') + elif errs: + FAIL(f'the crate: {len(errs)} console error(s), {probes} explained as atlas probes; first: {errs[0][:120]}') + else: + print(' · 0 page errors') + finally: + b.close() + + def smoke_continuity(p): """R15 identity continuity (ledger #3): the ped who queued/surged into a venue turns up in its crowd. Exercises F's real relay end-to-end — VenueQueue.admitOne() → onAdmit → citizens.recordVenueEntry → @@ -1101,6 +1211,7 @@ def main(): smoke_audio(p) # R11 — audio house-law (silent-happy · pre-gesture · mute · noassets · leak) smoke_gigs(p) # R12/13 — the district gig layer (schedule · band · crowd · budget · cover · noassets) smoke_continuity(p) # R15 — identity continuity (the ped you followed is in the crowd · settled tris) + smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path) finally: if srv: srv.terminate() diff --git a/web/index.html b/web/index.html index b951660..2452fc1 100644 --- a/web/index.html +++ b/web/index.html @@ -52,7 +52,7 @@ import { createChunkManager } from './js/world/chunks.js'; import { createPlayer } from './js/world/player.js'; import { createHUD } from './js/world/hud.js'; import { createMinimap } from './js/world/minimap.js'; -import { createInteriorMode } from './js/world/interior_mode.js'; // [Lane F integration] Lane C interior bridge +import { createInteriorMode, godverseBaseFor } from './js/world/interior_mode.js'; // [Lane F integration] Lane C interior bridge import { createWallet } from './js/interiors/wallet.js'; // [Lane F R8] Lane C buy loop v0 (session wallet) import { CitizenSim } from './js/citizens/sim.js'; // [Lane F integration] Lane D street pedestrians import { createWeather } from './js/world/weather.js'; // [Lane F R8] Lane B seeded weather (?weather=1) @@ -196,7 +196,23 @@ const DIG_ON = params.has('dig') && params.get('dig') !== '0'; const STOCK_REAL = params.get('stock') === 'real' && !NOASSETS; // [Lane F R9] preload ALL three packs so record dig sleeves AND book-spine/toy-box shelves (buy-anywhere) // are real. Fail-soft per pack (missing → parody canvas). getStockPack(type) is what buildInterior reads. -if (STOCK_REAL) { preloadStockPack('record'); preloadStockPack('book'); preloadStockPack('toy'); } +// [Lane F R24 §6a — THE REAL CRATE] The town-wide packs stay exactly as they were (every shop without a +// GODVERSE identity keeps them, byte-identically). On TOP of them, every shop carrying A's `godverseShopId` +// preloads its OWN pack under its OWN base — because `getStockPack` is sync and the room is built the +// instant you open the door, so a pack first requested at enter() would not be resolved in time and the +// real shop would show parody on the walk-in. Cache identity is (type, base) since C's R24 fix, so these +// coexist with the town-wide packs rather than clobbering them. A keyed shop with no atlas 404s → null → +// parody: fail-soft runs through the same path, and that is the tier-0 rung of the ladder, live. +if (STOCK_REAL) { + preloadStockPack('record'); preloadStockPack('book'); preloadStockPack('toy'); + const seen = new Set(); + for (const s of (plan.shops || [])) { + const gBase = godverseBaseFor(s); + if (!gBase || seen.has(gBase + s.type)) continue; + seen.add(gBase + s.type); + preloadStockPack(s.type, { base: gBase }); + } +} // [Lane F R8 — Lane C buy loop v0] ONE session wallet (seeded start cash) bound to the dig BUY across shops. // Runtime-only: never writes back into the plan/room build, so goldens + draw-counts are unaffected. const wallet = createWallet(seed); @@ -466,7 +482,7 @@ import('./js/world/audio.js') if (params.get('dbg')) { import('./js/world/dbg.js') .then((m) => m.installDBG({ plan, camera, renderer, composer, chunks, lighting, player, hud, - setMode, getMode: () => MODE, enterShop })) + setMode, getMode: () => MODE, enterShop, interiorMode })) // [R24] interiorMode → DBG.stockInfo() .catch((e) => console.warn('[procity] DBG harness load failed:', e)); } } // end main() diff --git a/web/js/world/dbg.js b/web/js/world/dbg.js index a2923c3..03ff545 100644 --- a/web/js/world/dbg.js +++ b/web/js/world/dbg.js @@ -12,7 +12,7 @@ import { busShelterStops } from './furniture.js'; export function installDBG(deps) { const { plan, camera, renderer, composer, chunks, lighting, player, hud, - setMode, getMode, enterShop } = deps; + setMode, getMode, enterShop, interiorMode = null } = deps; const CS = 64; // Force the renderer + composer + camera to the live viewport before every harness render, then @@ -268,22 +268,53 @@ export function installDBG(deps) { // DBG.enterShop('record') → first shop of that registry type // DBG.enterShop('The Exchange Hotel')→ by exact name, else name substring (case-insensitive) // Resolution order is most-specific-first so an id-shaped string can never be shadowed by a name. + // [R24] `godverseShopId` joins the selector — carefully, because THE TWO ID SPACES COLLIDE. + // A GODVERSE shop has two ids: its plan id (local, renumbered per town, 1..N) and its + // godverseShopId (the real POS/dealgod id — what the atlas, every G-side query and every + // cross-repo bug report names it by). Monster Robot is plan 8 / godverse 3962749. But Silky Oak's + // godverse id is 31 and some *other* shop's PLAN id is also 31 — so a bare number is genuinely + // ambiguous, and F's first cut at this silently entered the wrong shop (a cafe) while looking + // like it worked. Bare number ⇒ plan id (unchanged, back-compatible); `'g:'` ⇒ godverse id, + // explicitly. When a bare number matches BOTH, we say so in the return rather than pick quietly. enterShop(sel) { const shops = plan.shops || []; - let s = null; - if (typeof sel === 'number') s = shops.find((x) => x.id === sel); - else if (typeof sel === 'string') { + let s = null, ambiguousWith = null; + if (typeof sel === 'number') { + s = shops.find((x) => x.id === sel) || null; + const g = shops.find((x) => x.godverseShopId === sel) || null; + if (s && g && g !== s) ambiguousWith = { godverseShopId: sel, name: g.name }; + if (!s) s = g; + } else if (typeof sel === 'string') { const q = sel.trim().toLowerCase(); - s = shops.find((x) => String(x.id) === q) // id-as-string - || shops.find((x) => (x.name || '').toLowerCase() === q) // exact name - || shops.find((x) => (x.type || '').toLowerCase() === q) // registry type - || shops.find((x) => (x.name || '').toLowerCase().includes(q)); // name substring + const gm = /^g(?:odverse)?:(\d+)$/.exec(q); + if (gm) { + const n = Number(gm[1]); + s = shops.find((x) => x.godverseShopId === n) || null; + if (!s) return { error: `no shop with godverseShopId ${n}` }; + } else { + s = shops.find((x) => String(x.id) === q) // id-as-string + || shops.find((x) => (x.name || '').toLowerCase() === q) // exact name + || shops.find((x) => (x.type || '').toLowerCase() === q) // registry type + || shops.find((x) => (x.name || '').toLowerCase().includes(q)); // name substring + } } if (!s) return { error: `no shop matching ${JSON.stringify(sel)}` }; enterShop(s.id, s.name); - return { entered: s.id, name: s.name, type: s.type }; + const out = { entered: s.id, name: s.name, type: s.type, + godverseShopId: s.godverseShopId || null }; + if (ambiguousWith) out.ambiguous = `${sel} is also the godverseShopId of "${ambiguousWith.name}" ` + + `— entered by PLAN id; use 'g:${sel}' for the godverse shop`; + return out; }, exitShop() { window.dispatchEvent(new CustomEvent('procity:exitShop')); return { exited: true }; }, + + // [R24] The identity of the stock in the open room — the falsifiable the #7a gate asserts on. + // Returns null with no room open (the caller must enter first; a null here FAILS the gate rather + // than passing quietly — vacuous-gate law). + stockInfo() { + if (!interiorMode) return { error: 'interiorMode not wired into DBG' }; + return interiorMode.stockInfo || { error: 'no interior open — enterShop() first' }; + }, }; window.DBG = DBG; diff --git a/web/js/world/interior_mode.js b/web/js/world/interior_mode.js index 7454f50..5e49c06 100644 --- a/web/js/world/interior_mode.js +++ b/web/js/world/interior_mode.js @@ -15,7 +15,7 @@ // The interior renders into its OWN dark THREE.Scene (the street scene is frozen, not disposed — // CITY_SPEC L3 "pause, not dispose"), so returning to the street is instant. -import { buildInterior, getStockPack, makeStockAdapter } from '../interiors/interiors.js'; +import { buildInterior, getStockPack, makeStockAdapter, preloadStockPack } from '../interiors/interiors.js'; import { KeeperManager } from '../citizens/keepers.js'; // Lane D — shopkeeper behind the counter import { GigCrew } from '../citizens/band.js'; // Lane D — band trio + audience (R12, ?gigs=1) import { createDig, binSeed } from '../interiors/dig.js'; // Lane C — crate-riffle (v2, gated on ?dig=1) @@ -36,6 +36,17 @@ const EXIT_DIST = 1.0; // …then coming back within this distance leaves to th // lives in CITY_SPEC §v3. See LANE_F_NOTES §12. const WALLA_KEY = 'crowd-walla'; // [R13] manifest.ambience — crowd murmur layered under the gig bed while 'on' +// [Lane F R24 §6a] The per-shop stock base for a GODVERSE shop — A's `godverseShopId` → C's per-shop +// pack location (LANE_C_PUB §7.3/§7.5). Exported and used by BOTH the shell's boot preload and the room's +// lookup, deliberately: C's cache identity is the LITERAL (type, base) pair, so if the two sides built +// this string even slightly differently the lookup would miss and a real shop would fall silently to +// parody — a bug that looks exactly like "no stock" and would sail past a covers-render eyeball. +// One function, one string, one cache key. Shops without the field return undefined → town-wide packs. +export function godverseBaseFor(shop) { + const id = shop && shop.godverseShopId; + return id ? `assets/stock_godverse/${id}/` : undefined; +} + export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false, dig: digEnabled = false, stockReal = false, wallet = null, occupancyOf = null, gigState = null, rosterOf = null }) { @@ -189,7 +200,15 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null if (current) exit(); // never stack interiors // [F2 §stock=real] real GODVERSE sleeves in this shop's crates. makeStockAdapter(null) → null → // Lane C fails soft to the parody canvas, so a missing/unpreloaded pack just shows placeholders. - currentAdapter = stockReal ? makeStockAdapter(getStockPack(shop.type)) : null; + // [R24 §6a — the per-shop base] A shop that carries A's `godverseShopId` is a REAL shop with its own + // real stock, so it resolves its OWN pack under its OWN base (C's §7.5 seam); everything else keeps + // the town-wide packs, byte-identically. Cache identity is (type, base) since C's R24 fix, so the two + // coexist instead of the per-shop pack silently inheriting the town-wide one (the R23 bug that held + // the tag). Missing atlas → 404 → null → parody: the fail-soft law runs through the SAME path. + // Gated on stockReal, never on the id alone: ?noassets ⇒ stockReal false ⇒ not one byte fetched. + const gBase = stockReal ? godverseBaseFor(shop) : undefined; + if (gBase) preloadStockPack(shop.type, { base: gBase }); // idempotent; re-entry is a cache hit + currentAdapter = stockReal ? makeStockAdapter(getStockPack(shop.type, gBase)) : null; // [Lane F R13 THE DISTRICT] Is THIS shop the venue with a gig on right now? Per-venue query — a town has // 2–4 venues, each with its own latch keyed by shop id. Lane C only stamps room.audio.gigKey when we pass // opts.gig, so pass NULL unless it's really on: a quiet-night venue then builds as a normal interior @@ -332,5 +351,53 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null get crew() { return crew; }, // [R12] Lane D GigCrew (null until a gig has been entered) get crewInfo() { return crewInfo; }, // [R12] { band, crowd } actually spawned — F's smokes assert on this get digActive() { return !!(dig && dig.active); }, // [F2] shell reads this to keep pointer-lock/Esc sane while riffling + + // [Lane F R24 — the honest-gate surface, per the vacuous-gate law] What stock is in THIS room, BY + // IDENTITY. R23's #7a could go green on "real covers render" while the room held the generic v2 pack + // and `stock_godverse/` was never fetched — presence, not identity. So the gate needs falsifiables: + // which base the room resolved, how many items, and the actual item ids on the rendered buy meshes + // (C tags those with `buyItems[].item`; a plain sleeve carries only `isStock`, so ids come from the + // buyables and the pack, never from "it looked right"). Null when no room is open. + get stockInfo() { + if (!current || !currentShop) return null; + const base = stockReal ? godverseBaseFor(currentShop) : undefined; + const pack = stockReal ? getStockPack(currentShop.type, base) : null; + const renderedIds = [], renderedTitles = [], texUrls = new Set(); + let stockMeshes = 0; + scene.traverse((o) => { + const u = o.userData || {}; + if (!u.isStock && !u.buyMesh) return; + stockMeshes++; + if (Array.isArray(u.buyItems)) { + for (const b of u.buyItems) if (b && b.item) { + if (b.item.id != null) renderedIds.push(String(b.item.id)); + if (b.item.title) renderedTitles.push(b.item.title); + } + } + // The atlas the GPU is actually sampling. This is the ONLY assertion that survives everything: + // ids are positional slots (`rec_0000`) that BOTH packs reuse, so id equality cannot tell the + // real crate from the generic v2 pack — every atlas id is also a town-pack id (measured R24). + // A texture URL is the file the pixels come from. Falsifiable, and it cannot be faked by a + // pack that merely looks right. + for (const m of (Array.isArray(o.material) ? o.material : [o.material])) { + const src = m && m.map && m.map.image && (m.map.image.src || m.map.image.currentSrc); + if (src) texUrls.add(String(src)); + } + }); + return { + shopId: currentShop.id, + godverseShopId: currentShop.godverseShopId || null, + name: currentShop.name, + type: currentShop.type, + base: base || null, // null ⇒ the town-wide pack (or no stock at all) + packItems: pack && pack.items ? pack.items.length : 0, + packIds: pack && pack.items ? pack.items.map((i) => String(i.id)) : [], + packTitles: pack && pack.items ? pack.items.map((i) => i.title).filter(Boolean) : [], + stockMeshes, + renderedIds, + renderedTitles, + texUrls: [...texUrls], + }; + }, }; }