Fable (integrator): ROUND 32 CLOSED — v7.0-beta wave 1 all green; smoke_bible + the routing amendment

qa.sh --strict --matrix on the final tree: 7 passed · 0 failed · 0 warn · 0 skipped, MATRIX 10
towns green. New STRICT gate smoke_bible: band law over EVERY bin off the digOffers surface
(80 offers/5 bins), the buy-everything ledger runs on THE GEM'S BIN so the finder-margin leg is
non-vacuous (bin1: 2 gems, grail asking $19 vs keeper $30; ledger closes to the cent — Σask
$333 → Σoffer $135, gems alone +$21, every non-gem strictly negative), scarcity measured
(emptied bin 0/16 on re-open AND across a full reload; 16 pulls in the save) with the ?game=0
negative control re-opening 16/16 (the detector discriminates). sell_routing amended from the
alpha inequality (credited < paid) to the exact formula (credited == min(basis−1, basis//2),
basis = bandLow-or-pricePaid) — the fence moved from the receipt to the band, which is what
charter law #2 always said; it passed on a banded standard item (+$4 on basis $8). _at_bin
grows an idx param (back-compatible). Money shots in docs/shots/v7_beta/ (the guide card + the
crate at the counter). LANE_F_NOTES §32 (incl. the JING5 fresh-checkout trap: gitignored
web/assets/models synced from m3ultra — also un-warned the validator's 15 cross-pack checks).
README manual: the guide/gem/scarcity rows. Remaining beta charter queued in the round doc:
wantlist + rumors · travel costs a day · Esc-in-person · then GodBay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-20 18:27:19 +10:00
parent bb30767b62
commit 9aae167d64
7 changed files with 303 additions and 11 deletions

View File

@ -4,6 +4,23 @@
---
## Round 32 (v7.0-beta wave 1) — THE BIBLE + scarcity + the §31.6 queue (Fable, solo, JING5)
Guide bands derived in `js/interiors/bible.js` (E's thresholds mirrored, no pack re-emit) →
the pull/sell cards carry the **guide stamp**; parody bins roll seeded bands + 10% collector/
grail **gems** (asking under the keeper's bandLow offer — margin by construction); §9.1 sell
basis swapped to bandLow exactly as pre-ratified (alpha entries keep the pricePaid basis).
**Scarcity:** optional `pulls` on procity-save/1 — a bought dig slot stays gone all day
(pick-then-omit, zero collateral), pruned on sleep; real-sourced pulls never expire. §31.6
queue: cluster-pose spawn (main-street doors, classic keeps the frozen pose) · THE CRATE +
game bar live in interiors (C anywhere, yields to the cards) · DAWN dome swaps to high-cirrus
(weather override wins). **Gates:** new STRICT `smoke_bible` (band law over every bin ·
ledger closes to the cent on the GEM's bin, Σask $333 → Σoffer $135, gems +$21 · emptied bin
0 across reload · `?game=0` control re-opens 16/16) + sell_routing amended to the exact
formula (the fence moved from the receipt to the band). Full record: LANE_F_NOTES §32.
---
## Round 31 (v7.0-alpha CLOSE) — THE DIG FLIP + the tag
**Fable's wave-0 playtest found the one alpha-blocker: the dig was default-OFF on a plain boot.**

View File

@ -23,7 +23,8 @@ Click **START** to lock the mouse. Then:
| **W A S D** + mouse | walk / look (first person) |
| walk into a shop door | enter the interior (open shops only — check the hours tooltip) |
| **E** (inside a shop) | open the crate at a record bin — then it's cursor-driven: click sleeves to riffle, click BUY |
| **E** (at the counter) · **C** (street) · **SLEEP** (top bar) | sell a find to the keeper (always under what you paid) · open THE CRATE — your collection · sleep = save, the day advances, stock rotates |
| **E** (at the counter) · **C** (anywhere) · **SLEEP** (bottom bar) | sell a find to the keeper (he pays under the **guide band**, never over) · open THE CRATE — your collection, in shops too now · sleep = save, the day advances, stock rotates |
| read the **guide** stamp on every price card | the 90s price-guide band ("guide $824"). An item **asking under its band is a gem** — buy it, sell it to a keeper, pocket the margin. Bought slots stay gone for the day (the bin remembers) |
| click a book spine / toy box | pull-and-buy card (real price stickers in godverse shops) |
| walk out the door / **Esc** | leave the shop (Esc also frees the mouse anywhere) |
| **M** | 2D town map (street only) |

View File

@ -1754,3 +1754,62 @@ Fable's preview holds :8130, untouched):
5. Automation-only, unverified in person: **dig Esc-close under synthetic events**.
6. *(carried from §30.6)* no per-day `gone` tracking on parody/mint dig stock — scarcity-per-day
needs the save to carry pull records; rule it with the guide bands.
## §32 — R32: v7.0-beta wave 1 — THE BIBLE + scarcity + the §31.6 queue (Fable, solo round, JING5)
**The round in one line:** the guide bands land (derived, not baked), gems exist (parody bins only,
by construction), a bought slot stays gone for the day (pulls ride the save), and §31.6 items 13
are fixed. All gates green; the no-pump law survives the §9.1 basis swap structurally.
### §32.1 What shipped, by seam
- **`js/interiors/bible.js`** (C) — the band table mirroring E's pipeline thresholds
(record 8/25/60 · book 5/15/40 · toy 6/20/50), `bandRange`/`guideText`/`sellBasis`. Derivation
ruling: NO pack re-emit (charter risk #3 never opens); if E's thresholds move, bible.js moves in
the same commit.
- **dig.js** (C) — parody sleeves roll seeded bands (25/45/22/8%) + a 10% gem on collector/grail
(asking strictly under the keeper's own bandLow offer: collector $29 vs $12 · grail $520 vs
$30); fixed 9-draw stream shape per item; `rare` now ≡ grail (matches real packs). Real offers
carry `band` off `price_band`. `open({gone})` omits pulled slots AFTER the draw (the R27
pick-then-omit seam, zero collateral) — real via `'sku:<id>'`, parody via original offer index
(`o.i` survives filtering, identity preserved). The pull card + sell card gain the **guide
stamp** (`guide $824` / `$60+`) — present-fields-only, and the card NEVER says "gem": spotting
asking-under-guide is the player's skill.
- **sell.js** (C) — §9.1 basis swap executed exactly as pre-ratified: `sellOffer(sellBasis(entry))`
= `min(bandLow1, max(1, floor(bandLow·0.5)))` when banded, pricePaid basis for alpha-era
entries (they sell exactly as before — no migration needed).
- **save.js** (F) — optional `pulls: string[]` on procity-save/1 (validated loudly when present;
absent on alpha saves — old savers drop it silently, never reject). `recordPull`/`pullsFor`
seams; pruned on sleep/import (day-tagged keys from other days are unreachable by construction;
untagged = real-sourced, kept — sold means gone). Cap 600 FIFO. Collection entries gain
optional `band` (recordFind stamps dig + shelf buys).
- **interior_mode.js** (F) — pull keys `${shopId}|${binKey}${pullTag}#${slot}`; pullTag =
`@d<game.day>` for rotating crates (INCLUDING day 1 — the seed stays untagged per the day-1
convention, the ledger doesn't), `''` for real-sourced. The SEED input is untouched (A's
rotation boundary holds). `digOffers` honest surface (index/ask/band/sku) for the gate.
- **hud.js + index.html** (B/F) — §31.6-2: `#pc-game` rides document.body (position:fixed, z 30),
ticks in every mode (`hud.tickGame()` from the interior/map branches), yields to the dig/sell
cards and the map; C toggles the crate anywhere. §31.6-1: cluster-pose spawn — main-street
shop doors (block kind, both generators), stand at the door nearest the cluster heart, face the
longer run of shopfronts; pure plan math, zero draws; `?classic=1` keeps the frozen pose.
§31.6-3: DAWN swaps the dome to `high-cirrus` (lighting `dawnSky` opt, null under classic);
a weather `setSky` is now an explicit override that always wins.
### §32.2 THE GATE (`smoke_bible`, STRICT) + the R30 amendment
Scans EVERY bin of the record shop off `digOffers`: band law on all offers (in-band unless a
collector/grail gem), then runs the buy-EVERYTHING ledger on **the gem's bin** (leg is
non-vacuous by construction — 80 offers/5 bins, bin1 holds 2 gems on the pinned seed, grail
asking $19 vs keeper $30). Ledger closes exactly: Σask $333 → Σoffer $135 (gems alone +$21,
every non-gem strictly negative). Scarcity: the emptied bin re-opens at 0, STAYS 0 across a full
reload (16 pulls in the save); negative control: `?game=0` re-opens it at 16 — the detector
discriminates. 0 console errors. **R30 amendment (sell_routing):** `credited < paid` was the
alpha inequality; the gate now asserts the exact formula (`credited == min(basis1, basis//2)`,
basis = bandLow-or-pricePaid, `credited < basis`) — the fence moved from the receipt to the
band, which is what charter law #2 always said.
### §32.3 Environment note (JING5)
`web/assets/models/` is GITIGNORED build output — a fresh checkout runs stock=real/book-toy/
no-pump gates RED with 404s (the R31-green tree lives on m3ultra). Fixed here by rsync from
m3ultra. If a third machine joins the rotation, sync that dir (49 M) or re-run E's pipeline.

View File

@ -0,0 +1,61 @@
# PROCITY — Round 32 instructions (from Fable, integrator)
Date: 2026-07-20 · **R31 closed** — v7.0-alpha tagged and pushed; the loop plays. Round 32 opens
**v7.0-beta — THE THRILL, wave 1**: the guide bands land ([V7_THE_GAME.md](../V7_THE_GAME.md)
stage 2), scarcity rides the save, and the alpha playtest's friction queue (LANE_F_NOTES §31.6)
gets its cosmetic items. **Solo round:** John asked for direct progress; Fable executes all lanes
in one session on JING5, same treaty, same gates — lane ownership is honoured in the commit
messages, not the parallelism.
## The design rulings this round encodes (from the ratified charter + §31.6 queue)
1. **THE BIBLE derives, it is not baked.** E's band thresholds (build_stock_pack.py:88-94 —
record 8/25/60 · book 5/15/40 · toy 6/20/50) become a tiny JS mirror
(`js/interiors/bible.js`). No pack re-emit, so risk #3 (re-baked atlases orphaning owned
skus) never opens. The guide shows a BAND ("guide $2559"), never a verdict — spotting the
gem stays the player's skill.
2. **Gems live in the parody bins.** Real/mint packs derive `price_band` FROM `price`, so real
asking is always inside its band → with the §9.1 basis swap (sellOffer basis = bandLow),
every real round trip stays strictly negative and **the R30 no-pump gate holds untouched,
by construction**. Procedural stock gains seeded bands + a seeded under-band gem roll —
the only place asking can sit under the guide, which is exactly the op-shop fantasy.
3. **Scarcity is what makes the gem loop safe** (§31.6 item 6, ruled WITH the bands as filed):
a bought dig slot enters `pulls` in the save (optional field, procity-save/1 — old saves
stay valid, old loaders drop the field without rejecting) and that bin omits it after the
draw (the proven `pack.gone` pick-then-omit seam — zero collateral). A gem is profit ONCE;
tomorrow's rotation re-keys the bin. Growth is bounded by finds, not arbitrage (charter
law #2's real meaning).
4. **§31.6 items 13:** spawn becomes a computed cluster-pose facing the shop strip (classic
keeps the frozen v2 pose); THE CRATE surfaces inside interiors (C key — the §31.6
"crate at the counter" want, hidden while dig/sell cards own the screen); DAWN gets a
cool sky dome swap (classic + weather-override fenced).
## Gate additions (F)
`smoke_bible` in flags_check.py: guide stamp on the pull card (real + parody) · offer < bandLow
structurally · a seeded parody gem exists and its round trip is positive ONCE (then the slot is
gone — re-open the bin and it stays gone; reload and it stays gone) · negative control:
`?game=0` re-opens the same bin with the slot PRESENT (the detector discriminates — scarcity is
the save's doing, not the dig's). Classic purity untouched (no new module constructs under
`?classic=1` / `?dig=0`).
## ROUND CLOSED (Fable) — ALL GREEN
`qa.sh --strict --matrix`: **7 passed · 0 failed · 0 warn · 0 skipped**, MATRIX 10 towns green.
`smoke_bible`: 80 parody offers across 5 bins all band-lawful; bin1 holds 2 seeded gems (grail
asking $19 vs keeper $30); the buy-everything ledger closes to the cent (Σask $333 → Σoffer
$135, gems alone +$21, every non-gem strictly negative); the emptied bin stays empty across a
full reload (16 pulls in the save) and `?game=0` re-opens it 16/16 (the control discriminates);
0 console errors. sell_routing now asserts the exact §9.1 formula and passed on a BANDED item
(standard, basis $8 → +$4). Money shots: `docs/shots/v7_beta/guide_card.png` (the guide stamp —
"1974 COUNTRY guide $824" beside the $24 sticker) + `crate_at_the_counter.png` (THE CRATE open
inside a shop, game bar riding bottom-right). Environment note: JING5's gitignored
`web/assets/models/` was absent (fresh-checkout trap, now §32.3 in LANE_F_NOTES) — synced from
m3ultra, which also un-warned the manifest validator's 15 cross-pack checks. Full record:
LANE_F_NOTES §32. Shipped: bible.js + guide stamps + parody bands/gems (C) · pulls on the save
+ gone-wiring + digOffers surface (F) · crate-in-interiors + cluster-pose spawn + dawn sky (B)
· smoke_bible + the routing amendment (F) · README/docs.
**The beta's remaining charter** (next rounds): the WANTLIST + seeded rumors (D's keepers
finally gossip) · travel costs a day (23 towns become the map) · §31.6-5 (dig Esc under real
hands) · then v7.0 = GodBay offline auctions (G) + onboarding + the tour.

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@ -1633,16 +1633,17 @@ def _game_state(pg):
exNorm: JSON.stringify(ex), day: g.day, cash: g.cash, n: g.collection.length }; }""")
def _at_bin(pg):
"""Stand at the first bin of the open room, aimed at it (smoke_buy's proven setup)."""
return pg.evaluate("""() => {
def _at_bin(pg, idx=0):
"""Stand at the idx-th bin of the open room, aimed at it (smoke_buy's proven setup)."""
return pg.evaluate("""(idx) => {
const P = window.PROCITY;
const bins = []; P.interiorMode.current.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bins.push(o); });
if (!bins.length) return { err: 'no bins' };
const bp = new P.THREE.Vector3(); bins[0].getWorldPosition(bp);
if (idx >= bins.length) return { err: 'bin index out of range', bins: bins.length };
const bp = new P.THREE.Vector3(); bins[idx].getWorldPosition(bp);
P.camera.position.set(bp.x, 1.6, bp.z + 1.4); P.camera.lookAt(bp.x, bp.y + 0.4, bp.z); P.camera.updateMatrixWorld();
return { ok: true };
}""")
return { ok: true, bins: bins.length };
}""", idx)
def _dig_buy(pg):
@ -1757,7 +1758,7 @@ def smoke_sell_routing(p):
FAIL(f"routing: dig buy wallet/collection delta wrong — {rb}"); return
ent = pg.evaluate("""() => { const g = window.PROCITY.game;
const e = g.collection[g.collection.length - 1];
return { type: e.type, pricePaid: e.pricePaid, hasId: !!(e.sku || e.slotId) }; }""")
return { type: e.type, pricePaid: e.pricePaid, hasId: !!(e.sku || e.slotId), band: e.band || null }; }""")
if ent['type'] == 'record' and ent['hasId'] and ent['pricePaid'] == paid:
OK(f"routing: entry shape honest — type=record, sku|slotId set, pricePaid == debit (${paid})")
else:
@ -1778,10 +1779,21 @@ def smoke_sell_routing(p):
hudCash: cashEl ? cashEl.textContent : null,
hudCrate: crateBtn ? crateBtn.textContent : null }; }""")
credited = after['cash'] - cash_before
if rs.get('sold') == 1 and after['n'] == n_before - 1 and 0 < credited < paid:
OK(f"routing: SELL — +${credited} credited (< ${paid} paid, no-pump by construction), collection {n_before}{after['n']}")
# [R32 amendment] the no-pump law's beta form: the offer is EXACTLY min(basis1, max(1, basis//2))
# where basis = bandLow when the entry carries a band (the §9.1 swap), pricePaid otherwise. The
# old `credited < paid` held for every alpha sale but a banded GEM legitimately credits above
# what was paid — the fence moved from the receipt to the guide band, so the gate measures the
# formula itself, not the alpha inequality.
R32_LOW = {'bargain': 2, 'standard': 8, 'collector': 25, 'grail': 60}
basis = R32_LOW.get(ent.get('band'), paid)
want = min(basis - 1, max(1, basis // 2)) if basis >= 1 else 0
if rs.get('sold') == 1 and after['n'] == n_before - 1 and credited == want and credited < basis:
OK(f"routing: SELL — +${credited} credited == min(basis1, basis/2) with basis ${basis} "
f"({'band ' + ent['band'] if ent.get('band') else 'pricePaid'}; < basis, no-pump by construction), "
f"collection {n_before}{after['n']}")
else:
FAIL(f"routing: sale delta wrong — sold={rs.get('sold')}, credited {credited} vs paid {paid}, n {n_before}{after['n']}")
FAIL(f"routing: sale delta wrong — sold={rs.get('sold')}, credited {credited} want {want} "
f"(basis {basis}), n {n_before}{after['n']}")
if after['hudCash'] is not None:
if after['hudCash'] == str(after['cash']) and after['hudCrate'] == f"CRATE {after['n']}":
OK(f"routing: B's HUD follows — $" + after['hudCash'] + f" · {after['hudCrate']}")
@ -1983,6 +1995,147 @@ def smoke_nopump(p):
b.close()
def smoke_bible(p):
"""R32 gate — THE BIBLE + per-day scarcity (v7.0-beta wave 1). Legs: (a) the pull card carries the
guide stamp on a NAKED boot (lesson #4 — no explicit flags; parody bin); (b) structural: every parody
offer prices INSIDE its band unless it's a seeded gem (asking < bandLow, collector/grail only) —
read off F's digOffers honest surface, checked against the BIBLE mirror of E's thresholds;
(c) buy the WHOLE bin, sell it all: cash moves exactly ΣofferΣask with offer = min(low1,
max(1, low//2)) per item gems positive, everything else negative, the ledger closes to the cent;
(d) scarcity: the emptied bin re-opens EMPTY, stays empty across a reload (pulls ride the save),
and (e) negative control: ?game=0 re-opens the same bin FULL the detector discriminates
(scarcity is the save's doing, not the dig's). (f) real stock carries the stamp too."""
head('GATE R32: THE BIBLE — guide bands · gems · per-day scarcity (+ ?game=0 control)')
BIBLE = {'bargain': (2, 7), 'standard': (8, 24), 'collector': (25, 59), 'grail': (60, None)}
exp_offer = lambda band: min(BIBLE[band][0] - 1, max(1, BIBLE[band][0] // 2))
b, pg, errs = new_page(p)
try:
boot(pg, '') # NAKED — dig/game default-on carry the feature
r0 = _enter_record_shop(pg)
if r0.get('err'): FAIL(f"bible: {r0['err']}"); return
# bankroll the buy-everything leg through the REAL import path (5000 covers any 16-sleeve bin)
rich = pg.evaluate("""() => { const g = window.PROCITY.game;
const ex = JSON.parse(g.export()); ex.cash = 5000; ex.savedAt = Date.now();
return { ok: g.import(JSON.stringify(ex)), cash: g.cash }; }""")
if not (rich['ok'] and rich['cash'] == 5000):
FAIL(f"bible: bankroll import failed — {rich}"); return
pg.evaluate("() => window.DBG.setSegment(2)") # import's onDay woke us at DAWN — back to midday
# ── (a)+(b): scan EVERY bin in the room off the honest surface — band law on all of them, and
# find a bin holding a GEM (seeded: whichever bin it is today, it's that bin forever). The
# buy-everything ledger leg then runs on THE GEM'S BIN, so the finder-margin arithmetic is
# exercised for real, not vacuously (a 0-gem bin would prove only the losing half).
n_bins = (_at_bin(pg) or {}).get('bins', 1)
bad, all_offers, gem_bin, gems = [], {}, None, []
for bi in range(n_bins):
_at_bin(pg, bi)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(350)
offs = pg.evaluate("() => window.PROCITY.interiorMode.digOffers")
pg.evaluate("() => { const o = document.querySelector('.pcdg-out'); if (o) o.click(); }")
pg.wait_for_timeout(200)
if not offs: bad.append(f"bin{bi} unreadable"); continue
all_offers[bi] = offs
bin_gems = []
for o in offs:
band = o.get('band')
if band not in BIBLE: bad.append(f"bin{bi}#{o['i']} band={band!r}"); continue
lo, hi = BIBLE[band]
if o['price'] < lo:
if band in ('collector', 'grail'): bin_gems.append(o)
else: bad.append(f"bin{bi}#{o['i']} {band} ${o['price']} under-band but not gem-eligible")
elif hi is not None and o['price'] > hi:
bad.append(f"bin{bi}#{o['i']} {band} ${o['price']} over band")
if bin_gems and gem_bin is None: gem_bin, gems = bi, bin_gems
if bad: FAIL(f"bible: band-law violations — {bad[:3]}")
else: OK(f"bible: {sum(len(v) for v in all_offers.values())} parody offers across {n_bins} bin(s) "
f"all inside their bands (or seeded gems)")
if gem_bin is None:
FAIL(f"bible: NO gem in any of {n_bins} bins — the thrill leg is unreachable on this seed"); return
for g in gems:
if not g['price'] < exp_offer(g['band']):
FAIL(f"bible: gem #{g['i']} asks ${g['price']} ≥ keeper offer ${exp_offer(g['band'])} — no finder margin")
OK(f"bible: bin{gem_bin} holds {len(gems)} gem(s) — e.g. {gems[0]['band']} asking ${gems[0]['price']} "
f"vs keeper ${exp_offer(gems[0]['band'])} (margin by construction)")
offers = all_offers[gem_bin]
_at_bin(pg, gem_bin)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(350)
# pull the front sleeve → the guide stamp on the card
pg.evaluate("""() => { const c = window.PROCITY.renderer.domElement;
c.dispatchEvent(new PointerEvent('pointerdown', { clientX: 640, clientY: 360, bubbles: true }));
c.dispatchEvent(new PointerEvent('pointerup', { clientX: 640, clientY: 360, bubbles: true })); }""")
pg.wait_for_timeout(300)
stamp = pg.evaluate("""() => { const p = document.querySelector('.pcdg-panel');
return p && p.style.display === 'block' ? p.textContent : null; }""")
if stamp and 'guide $' in stamp: OK('bible: the pull card carries the guide stamp (naked boot, parody)')
else: FAIL(f"bible: no guide stamp on the pull card — {stamp[:80] if stamp else 'no panel'}")
pg.evaluate("""() => { const p = document.querySelector('.pcdg-panel');
const x = p && p.querySelector('.x'); if (x) x.click(); }""")
# ── (c): buy the WHOLE bin through the real card, then sell it all at the counter ──
cash0 = pg.evaluate("() => window.PROCITY.wallet.cash()")
bought = 0
for _ in range(len(offers)):
rb = _dig_buy(pg)
if rb.get('err') or rb.get('disabled'): break
bought += 1
if bought < len(offers): _at_bin(pg, gem_bin) # walk-out re-closed the dig; re-aim the gem's bin
if bought != len(offers):
FAIL(f"bible: bought {bought}/{len(offers)} — the buy-everything leg died early"); return
ask_total = sum(o['price'] for o in offers)
offer_total = sum(exp_offer(o['band']) for o in offers)
rs = _counter_sell(pg, max_sales=bought + 2)
sell_card = pg.evaluate("() => { const p = document.querySelector('.pcsl-panel'); return p ? p.textContent : ''; }")
if rs.get('err') or rs.get('sold', 0) != bought:
FAIL(f"bible: sold {rs.get('sold')}/{bought}{rs}"); return
cash1 = pg.evaluate("() => window.PROCITY.wallet.cash()")
gem_profit = sum(exp_offer(o['band']) - o['price'] for o in gems)
if cash1 - cash0 == offer_total - ask_total:
OK(f"bible: the ledger closes — Σask ${ask_total} → Σoffer ${offer_total} "
f"(net {cash1 - cash0:+d}; gems alone {gem_profit:+d}, every non-gem strictly negative)")
else:
FAIL(f"bible: ledger mismatch — cash moved {cash1 - cash0:+d}, expected {offer_total - ask_total:+d} "
f"(basis law broken somewhere)")
# ── (d): scarcity — the emptied bin is EMPTY on re-open, and STAYS empty across a reload ──
_at_bin(pg, gem_bin)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(300)
n_re = pg.evaluate("() => (window.PROCITY.interiorMode.digOffers || []).length")
pg.evaluate("() => { const o = document.querySelector('.pcdg-out'); if (o) o.click(); }")
pulls_n = pg.evaluate("() => (JSON.parse(window.PROCITY.game.export()).pulls || []).length")
if n_re == 0 and pulls_n >= bought:
OK(f"bible/scarcity: re-opened bin is cleaned out (0 of {bought}) · save carries {pulls_n} pull(s)")
else:
FAIL(f"bible/scarcity: re-open shows {n_re} sleeves (want 0) · pulls in save {pulls_n}")
boot(pg, '') # full reload, same origin — the save must carry it
r1 = _enter_record_shop(pg)
if r1.get('err'): FAIL(f"bible/scarcity reload: {r1['err']}")
else:
_at_bin(pg, gem_bin)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(300)
n_reload = pg.evaluate("() => (window.PROCITY.interiorMode.digOffers || []).length")
if n_reload == 0: OK('bible/scarcity: still cleaned out after a full reload — pulls rode the save')
else: FAIL(f"bible/scarcity: reload restocked the bin ({n_reload} sleeves) — pulls not persisted")
# ── (e): negative control — ?game=0 must re-open the SAME bin FULL (rotation gate: game=0 ≡ day1) ──
boot(pg, 'game=0')
r2 = _enter_record_shop(pg)
if r2.get('err'): FAIL(f"bible/control: {r2['err']}")
else:
_at_bin(pg, gem_bin)
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
pg.wait_for_timeout(300)
n_ctl = pg.evaluate("() => (window.PROCITY.interiorMode.digOffers || []).length")
if n_ctl == len(offers): OK(f"bible/control: ?game=0 re-opens the bin FULL ({n_ctl}) — "
"scarcity is the save's doing, the detector discriminates")
else: FAIL(f"bible/control: ?game=0 bin shows {n_ctl}, want {len(offers)}")
if 'guide $' in (sell_card or ''): OK('bible: the sell card carries the guide line too')
else: WARN('bible: no guide line read off the sell card (cosmetic leg)')
if errs: FAIL(f"bible: {len(errs)} console error(s); first: {errs[0][:140]}")
else: OK('bible: 0 console errors')
finally:
b.close()
def smoke_classic_purity(p):
"""R30 gate (c) — ?classic=1 and ?game=0: ZERO Storage-prototype calls (instrumented before any
page script), zero game DOM (B's ids), game null. Default boot is the POSITIVE control (the
@ -2136,6 +2289,7 @@ def main():
smoke_nopump(p) # R30 gate b — 5 real round trips monotonic loss + C's negative control
smoke_classic_purity(p) # R30 gate c — instrumented Storage zero + zero game DOM + positive control
smoke_rotation(p) # R30 gate d — vertex-data rotation determinism (the wave-3 binding)
smoke_bible(p) # R32 — THE BIBLE (bands · gems · ledger) + per-day scarcity + ?game=0 control
smoke_tier2(p) # R27 — the kill-the-server gate (charter law #1)
smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path)
finally: