THE RECONCILIATION. The brief's naive sum (292 + washing 1 + magpie 1 = 294) is wrong, in our
favour. The pinned worst-DRAW frame is a NIGHT frame and both of wave 1's draw-spending layers are
structurally zero at night: the washing comes in (count 0), the magpie roosts (count 0). Wave 1's
one draw is spent in daylight, where the walked peak sits ten draws lower.
Two pinned trees, assets symlinked to the live tree on BOTH sides, 802 stations at 2 m down the
main spine at yaw 0 (the worst bearing), citizen sim and tram LIVE, 1280x720:
NIGHT worst draws 292 -> 292 (+0) tris 120,145 -> 122,879
NOON worst draws 282 -> 283 (+1) tris 99,872 -> 103,118
?classic=1 269 / 90,580 unmoved
fitzroy_real 172 / 130,727 unmoved bendigo_real 204 / 131,492 unmoved
(even with ?magpie=1 forced)
The control reproduced R37's pin TO THE BYTE, twice, on a fresh checkout. Every revert is exact:
?yards=0 at night returns all three control numbers; ?yards=0 at NOON returns 282/99,872, which
isolates the noon +1 to the washing and nothing else. MARGIN IS STILL EIGHT DRAWS.
LANE B's "control 346", answered rather than dismissed. As a delta it is the wall-clock ped-spawn
artifact R37 named, and B was right to hide the sim. But 346 > 300 is a claim about the world, and
neither stepwalk (walks the chunks in, photographs a half-dressed street) nor dwell (fills the
street, arrives clean at 27 chunks) can adjudicate it. budget_walk.py gains --method stepsettle:
the audit's fixed-yaw stations, each DRAINED then held N real rAF frames — R+1 dispose residue AND
a fully-acquired crowd in the same frame. Like for like on the same 101 stations, the settle is
worth +3 draws at noon and -9 at night, and the SIGN FLIP is the finding: at night the settled
street retires near rigs to impostors (tris 120,145 -> 100,939) — the crowd goes home, and a
sampler that never waits does not see it happen. The highest number anywhere in the reconciliation
(4 methods x 2 trees x 2 segments x 5 towns) is 292, the pin. NOTHING READ OVER 300.
A CORRECTION TO MY OWN R37 PIN: the corpus TRI worst is 130,727 on fitzroy_real, not 124,793 — and
it reads 130,727 on the CONTROL tree, so it is my R37 station spacing (12-bearing 8 m sweep) versus
this round's 2 m stations, not a wave-1 cost. Station spacing, not settle policy, is the biggest
lever on a walked worst frame. A pin is only as good as its spacing, and mine had not said so.
FOUR GATES, FOUR CONTROLS DEMONSTRATED (smoke_ambient / smoke_character / smoke_magpie /
smoke_yards, wired into flags_check.main()). A gate that cannot fail is not a gate — this round
earned the sentence twice, R37 killing two vacuous gates and B catching its own season mechanism
producing a constant that would have gated green under ?magpie=1.
1.1 audio — 6 sources anchored 63/8/81/32/39/57 (B's numbers exactly); dog AUDIBLE at 5.9 m with
1 play scheduled (for a one-shot, `audible` is only published once plays>0, so one word covers
fetch/decode/gain/schedule/counter); HOURS enforced (sprinkler audible 06:30 -> idle 12:30, same
pose); DAY-OF-WEEK enforced (mower idle Mon -> audible Sat, same pose); PAN is a bearing (yaw
0/90/180/270 -> +0.00 +0.85 -0.00 -0.85); ZERO DRAWS byte-identical to ?ambient=0. THE CONTROL:
a second server whose root is web/ symlink-for-symlink except one patched manifest with
sfx['dog-bark-fence'] renamed -> no-entry, 0 plays, never audible, other five unchanged, 0
console errors. Printed on every run: audible OUTPUT is unverifiable headlessly. Nobody in this
loop has ears.
1.2 character — classic is the frozen v1 literals, all tints null, 5 ground meshes; all 23 shipped
keys matched PER TOWN including the two carrying state:''; the control fires (unknown key ->
matched:false); 21 distinct palettes across 23 towns (one shared palette FAILS by name); and the
two orphans are ON THE GPU — ground-gravel.jpg bound on Castlemaine, ground-reddust.jpg on
Darwin, read off the live material.
1.3 magpie — the latch arm verbatim with its positive control FIRST; day-roll clears it;
localStorage gained NO new key (baseline taken after a game.save() — the first cut blamed the
magpie for v7's own write); the season is 23 distinct positions across 24 towns run UNFORCED,
against the first cut's 24-of-24-at-214, newtown_real in season on day 1, 129/1000 seeds against
a 11.5% design ratio; territory 6 -> 157 synthetic -> bendigo; +1 draw / +182 tris at ONE pose
reused across both boots.
1.4 yards — 181 lots, 181 hoists -> 394 items, 256 active, 0 at night; cost decomposition at one
pose with the sim quiet: box props +0 draws (in fact -2, the 36 `yard` lots used to fall through
to buildShopfront) with +1,936 tris, washing exactly +1 draw / +512 tris; and on real towns it
SKIPS LOUDLY — a ⊘ SCOPE line printing 0 house/yard lots / 0 hoists / mesh never created, beside
the synthetic 181 that makes the zero attributable.
Caught in my own work: the pan arm's first draft used two bearings and read +0.00/-0.00 because
that anchor sat on the forward axis — it would have called a working panner inconclusive. Four
bearings now. The gate's first draft failed the standard the gate exists to enforce.
SUITE: tools/qa.sh --strict --matrix GREEN on the true final tree (74d6257) — 7 passed, 0 failed,
0 warn, 0 skipped; flags_check GREEN 0/0 with all four new gates inside it; MATRIX GREEN 10 towns
x 7 gates, 0 fetches under ?noassets on all ten.
Held: nothing. Recorded not fixed: magpie.js says 154 triangles at :9/:123 and 182 at :42 (measured
182); E's three published GLBs are manifest-and-depot but not under web/assets/models (staged in
pipeline/_normalized, nothing loads them, the validator's contract covers it — not a defect).
NO TAG — wave 1 is content; the epoch tags when it is whole.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4317 lines
266 KiB
Python
4317 lines
266 KiB
Python
#!/usr/bin/env python3
|
||
"""PROCITY Lane F — v2 flag enforcement harness (round-6 F1).
|
||
|
||
The machine version of what the R5 review did by hand. Three things, headless, <2 min:
|
||
1. FLAGS-OFF REGRESSION — the enforcement arm of the prime law. Boot with no flags → the plan
|
||
fingerprint must equal the golden 0x5f76e76, the settled street view must stay within the v1
|
||
budget (≤300 draws / ≤200k tris) AND within tolerance of the v1.1 snapshot, every v2 subsystem
|
||
must report inactive, and there must be ZERO console errors. A v2 flag leaking into the default
|
||
boot trips this.
|
||
2. PER-FLAG SMOKE — for each landed flag (winmap, dig, roster, +plansrc when A lands): boot flag-on,
|
||
cross ≥2 chunks, enter one interior (dig: also open+close a riffle), 0 console errors.
|
||
3. ALL-ON COMBO (flag-composition law, decision #4): every landed flag on at once, same smoke.
|
||
|
||
Run: cd web && python3 -m http.server 8130 (or let this script spawn its own)
|
||
tools/.venv/bin/python tools/flags_check.py
|
||
Wired into tools/qa.sh as a WARN-level gate this round (strict in round 7). Exit 0 = all pass;
|
||
non-zero = a check failed (qa.sh reports it as a warning this round).
|
||
"""
|
||
import sys, json, time, socket, subprocess, pathlib, os, urllib.request, urllib.error
|
||
try:
|
||
from playwright.sync_api import sync_playwright
|
||
except ImportError:
|
||
sys.exit("playwright not installed — python3 -m venv tools/.venv && "
|
||
"tools/.venv/bin/pip install playwright && tools/.venv/bin/python -m playwright install chromium")
|
||
|
||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||
# [R31] PROCITY_QA_PORT: run the suite on an isolated port. The default (:8130) is also the house preview
|
||
# port — ensure_server() would silently REUSE a dev server already parked there, and a shared server is
|
||
# somebody else's cache semantics + somebody else's lifecycle. Gates want their own server or a named one.
|
||
PORT = int(os.environ.get('PROCITY_QA_PORT', '8130'))
|
||
HOST = f'http://127.0.0.1:{PORT}'
|
||
SEED = 20261990
|
||
GOLDEN_HASH = 0x5f76e76 # [R27w5] AMENDED. Was 0x3fa36874 "forever" since R16 — and that town
|
||
# was a row of BACK WALLS since v1 (A measured 0 toward / 681 away;
|
||
# walked behind them and found NUMBAT PLAYTHINGS facing the dirt).
|
||
# John's ruling: the covenant is anti-drift machinery, NOT a defect
|
||
# preservative. It did its job — it never drifted; it was wrong from
|
||
# the first commit, and no hash can catch what was never right.
|
||
GIG_GOLDEN = 0xec7a2d39 # [R27w5] re-pinned with the facade fix (was 0xb1d48ea1)
|
||
# flags-off DBG.shot('street_noon') snapshot. RE-PINNED at the R7 roster flip (prime-law amendment):
|
||
# old (v1.1, roster-off): 163 draws, 19000 tris
|
||
# new (R7, roster default-on): 164 draws, ~66000 tris
|
||
# Draws barely move — E1's ped sub-mesh merge makes each near-rig ~1 draw, which is exactly what let
|
||
# the roster flip to full density. Tris jump is the streamed crowd (each near-rig ~2.4k tris); it is
|
||
# inherently variable frame-to-frame (how many rigs are near at sample time), so the drift WARN below
|
||
# is DRAWS-ONLY — tris is guarded strictly by the STREET_TRIS_MAX hard cap, not a soft snapshot.
|
||
BASE_DRAWS, BASE_TRIS = 164, 66000
|
||
STREET_DRAWS_MAX, STREET_TRIS_MAX = 300, 200_000
|
||
KNOWN_FLAGS = ['winmap=1', 'dig=1', 'roster=stream'] # plansrc=osm auto-appended if Lane A landed it
|
||
# [R12] gig-night interior budget (ROUND12 law: ≤350 draws incl. the crowd — a room with a band and an
|
||
# audience in it is allowed more than a shop, which is what the ≤300 street/room numbers were tuned for).
|
||
GIG_DRAWS_MAX = 350
|
||
# `cover` is seeded per gig, so which door-charge path the default seed exercises is luck. Pin one seed
|
||
# per half of John's "free AND paid" ruling: 20261990's tonight is $10 (Screaming Utes at The Thornbury
|
||
# Hotel), 1234's is free (Dazza & The Dropbears at The Imperial). Re-pin if Lane A re-seeds the gig stream.
|
||
FREE_GIG_SEED = 1234
|
||
|
||
fails, warns = [], []
|
||
def FAIL(m): fails.append(m); print(f" \033[31m✗ FAIL\033[0m {m}")
|
||
def WARN(m): warns.append(m); print(f" \033[33m! WARN\033[0m {m}")
|
||
def OK(m): print(f" \033[32m✓\033[0m {m}")
|
||
def head(m): print(f"\n\033[1m{m}\033[0m")
|
||
|
||
def stature_ok(f):
|
||
# [R16 ledger #3] seated figs (the drummer — fig.userData.procitySeated) pass the no-giants floor at a
|
||
# SEATED band [0.9,1.5] m; standing figs stay [1.4,2.0]. A seated human fails the standing floor by design.
|
||
lo, hi = (0.9, 1.5) if f.get('seated') else (1.4, 2.0)
|
||
return lo <= f['stature'] <= hi
|
||
|
||
def port_up(port):
|
||
with socket.socket() as s:
|
||
s.settimeout(0.4)
|
||
return s.connect_ex(('127.0.0.1', port)) == 0
|
||
|
||
def ensure_server():
|
||
if port_up(PORT):
|
||
return None # reuse a running dev server
|
||
proc = subprocess.Popen(['python3', '-m', 'http.server', str(PORT)],
|
||
cwd=str(ROOT / 'web'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
for _ in range(50):
|
||
if port_up(PORT): return proc
|
||
time.sleep(0.1)
|
||
proc.terminate(); sys.exit(f"could not start http.server on :{PORT}")
|
||
|
||
# ── page helpers ─────────────────────────────────────────────────────────────
|
||
def new_page(p):
|
||
b = p.chromium.launch()
|
||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||
errs = []
|
||
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
|
||
pg.on('pageerror', lambda e: errs.append(str(e)))
|
||
return b, pg, errs
|
||
|
||
def boot(pg, query):
|
||
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query else ''))
|
||
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
|
||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||
try:
|
||
pg.wait_for_function("() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)", timeout=12000)
|
||
except Exception:
|
||
pass
|
||
|
||
def gigs_landed(p):
|
||
"""Boot ?gigs=1; landed iff Lane A's augmentation produced a schedule (else the flag is inert)."""
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
return bool(pg.evaluate("() => !!(window.PROCITY.plan.gigs && window.PROCITY.plan.gigs.length)"))
|
||
except Exception:
|
||
return False
|
||
finally:
|
||
b.close()
|
||
|
||
def plansrc_landed(p):
|
||
"""Boot ?plansrc=osm; landed iff the shell exposes an 'osm' plan source (else the flag is ignored)."""
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, 'plansrc=osm')
|
||
src = pg.evaluate("() => (window.PROCITY.plan && (window.PROCITY.plan.source||window.PROCITY.plan.planSource)) "
|
||
"|| window.PROCITY.planSource || null")
|
||
return src == 'osm'
|
||
except Exception:
|
||
return False
|
||
finally:
|
||
b.close()
|
||
|
||
# ── [R37 item 0.5] THE CLASSIC NETWORK ALLOW-LIST ────────────────────────────────────────────────
|
||
# classic_regression made NO network assertion at all for 21 rounds. That is why R36's fetch-surface
|
||
# widening (5 trellis DJ bodies joining PED_NAMES.normal) needed a brand-new smoke to catch it: the
|
||
# covenant gate was watching the plan fingerprint, the flags, the draws and the console, and nothing
|
||
# was watching the wire.
|
||
#
|
||
# THE AUTHORITY IS GIT, NOT AN OPINION. The covenant preserves "the frozen v2 baseline", so the
|
||
# allow-list is DERIVED from the v2.0 tag's own file tree at gate time:
|
||
# • same-origin asset ⇒ its path must exist in `git ls-tree -r v2.0 -- web/`;
|
||
# • depot asset (cross-origin digalot.fyi/3god/a/<file>) ⇒ its basename must appear in v2.0's own
|
||
# `web/assets/manifest.json`. This is how the four street furniture GLBs (bench · bin ·
|
||
# bus_shelter · food_cart, `furniture.js` GLB_FILES, byte-identical at 24cef5a and v2.0) and the
|
||
# five record-shop fittings reached through `useGLB: !NOASSETS` (index.html, byte-identical at
|
||
# v2.0) are admitted. Those nine fetches are CORRECT — they are in the baseline the covenant
|
||
# preserves, not a breach — and this gate must never be "fixed" by removing them.
|
||
# • CODE is allowed as a class (same-origin .js/.mjs + the document). A new module that fetches
|
||
# nothing is not a fetch-surface change; every ASSET is enumerated.
|
||
# • blob:/data: are not network — GLTFLoader mints one blob per embedded texture.
|
||
#
|
||
# ALLOW-LIST, NEVER DENY-LIST: a deny-list only catches what you already thought of. The R36 breach
|
||
# was SAME-ORIGIN (`/models/peds/woman_dj_01.glb`), so an allow-list phrased as "the four depot GLBs
|
||
# plus same-origin" would have been green straight through it. Same-origin is NOT a free pass here;
|
||
# `/models/peds/` is enumerated by the v2.0 tree, which contains exactly the covenanted 21 files.
|
||
#
|
||
# THE BASELINE TAG IS `v2.1`, AND THAT IS FABLE'S R37 RULING, NOT A CONVENIENCE. F measured classic's
|
||
# live fetch surface against the actual `v2.0` tree and found a delta of +6 files: five audio and
|
||
# `assets/towns/index.json`. Fable ruled (ROUND37_INSTRUCTIONS, 4a7c0f1): the covenant preserves "the
|
||
# exact v2 town", and **v2.1 was v2's final tagged state**, so the audio pack shipped inside v2 exactly
|
||
# as the furniture GLBs did — RATIFIED BASELINE. Git makes that ruling exact rather than generous:
|
||
# `git diff v2.0 v2.1 -- web/` is the audio pack, `js/world/audio.js`, and four wiring edits. **Zero
|
||
# other assets, zero deletions.** So moving the authority from v2.0 to v2.1 admits precisely the
|
||
# ratified audio and nothing else, and the depot GLB names survive in v2.1's manifest unchanged.
|
||
#
|
||
# `assets/towns/index.json` is **NOT baseline** — it is v4 selector machinery (R20/R21). Fable ruled it
|
||
# carried as a NAMED, PRINTED EXCEPTION: no geometry, no identity, no seeded draw, so not the R36
|
||
# species and not gettable in a floor round. It is labelled EXCEPTION, never baseline, so a reader of
|
||
# this log can never mistake the two. "Should classic show the town selector at all?" is on the v8 debt
|
||
# ledger for R38+.
|
||
V2_TAG = 'v2.1'
|
||
DEPOT_HOSTS = ('digalot.fyi',)
|
||
# (prefix, label) — carried, NOT baseline. Printed on every run, tagged EXCEPTION in the output.
|
||
POST_V2_EXCEPTIONS = (
|
||
('/assets/towns/index.json',
|
||
'v4 town-selector machinery (078245f R20, 4b01449 R21) — NOT v2 baseline; carried by Fable\'s '
|
||
'R37 ruling as a named exception (no geometry, no identity, no seeded draw). Debt ledger R38+: '
|
||
'should ?classic=1 show the town selector at all?'),
|
||
)
|
||
# The core that must ALL be observed, or the observer is not looking at a live boot (anti-vacuous):
|
||
# the four v2-era depot street GLBs + the covenanted ped fleet's shared clips.
|
||
CLASSIC_REQUIRED = ('procity_street_bench_01.glb', 'procity_street_bin_01.glb',
|
||
'procity_street_bus_shelter_01.glb', 'procity_street_food_cart_01.glb',
|
||
'/models/peds/walk.glb', '/models/peds/idle.glb',
|
||
'/models/peds/man_worker_hivis_01.glb', '/assets/manifest.json')
|
||
_v2_cache = {}
|
||
|
||
|
||
def _v2_baseline():
|
||
"""{paths, depot_basenames} from the v2.0 TAG. Empty dict ⇒ the tag is unreachable and the gate
|
||
must FAIL rather than pass with no authority."""
|
||
if _v2_cache:
|
||
return _v2_cache
|
||
ls = subprocess.run(['git', 'ls-tree', '-r', V2_TAG, '--name-only', '--', 'web/'],
|
||
cwd=str(ROOT), capture_output=True, text=True)
|
||
mf = subprocess.run(['git', 'show', f'{V2_TAG}:web/assets/manifest.json'],
|
||
cwd=str(ROOT), capture_output=True, text=True)
|
||
if ls.returncode != 0 or mf.returncode != 0:
|
||
return {}
|
||
paths = {ln.strip()[3:] for ln in ls.stdout.splitlines() if ln.strip().startswith('web/')}
|
||
depot = {tok.split('/')[-1] for tok in mf.stdout.split('"') if tok.endswith('.glb')}
|
||
_v2_cache.update({'paths': paths, 'depot': depot})
|
||
return _v2_cache
|
||
|
||
|
||
def _classify_fetch(url, host, paths, depot):
|
||
"""→ (verdict, tier). verdict ∈ {'ok','violation','ignored'}."""
|
||
if url.startswith(('blob:', 'data:', 'about:')):
|
||
return 'ignored', 'blob/data (not network — GLTFLoader mints one per embedded texture)'
|
||
if url.startswith(host):
|
||
path = url[len(host):].split('?')[0].split('#')[0] or '/'
|
||
if path == '/':
|
||
path = '/index.html'
|
||
if path.endswith(('.js', '.mjs', '.html')):
|
||
return 'ok', 'code (same-origin module/document)'
|
||
if path == '/favicon.ico':
|
||
return 'ignored', 'browser noise'
|
||
for pre, why in POST_V2_EXCEPTIONS:
|
||
if path.startswith(pre):
|
||
return 'ok', f'CARRIED EXCEPTION (not {V2_TAG} baseline) — {why}'
|
||
if path in paths:
|
||
return 'ok', f'same-origin asset present in the {V2_TAG} tree'
|
||
return 'violation', f'same-origin asset NOT in the {V2_TAG} tree'
|
||
h = url.split('/')[2] if '://' in url else ''
|
||
if any(d in h for d in DEPOT_HOSTS):
|
||
f = url.split('/')[-1].split('?')[0]
|
||
if f in depot:
|
||
return 'ok', f'depot asset named in the {V2_TAG} manifest'
|
||
return 'violation', f'depot asset NOT in the {V2_TAG} manifest'
|
||
return 'violation', f'THIRD-PARTY origin ({h})'
|
||
|
||
|
||
def classic_network_verdict(reqs, host):
|
||
"""Run every captured request through the allow-list → (violations, tiers, observed)."""
|
||
b = _v2_baseline()
|
||
if not b:
|
||
return None, None, None # the authority is unreachable; the caller FAILS
|
||
paths, depot = b['paths'], b['depot']
|
||
violations, tiers, observed = [], {}, []
|
||
for u in reqs:
|
||
v, tier = _classify_fetch(u, host, paths, depot)
|
||
if v == 'ignored':
|
||
continue
|
||
observed.append(u)
|
||
tiers.setdefault(tier, []).append(u)
|
||
if v == 'violation':
|
||
violations.append((u, tier))
|
||
return violations, tiers, observed
|
||
|
||
|
||
# ── the three checks ─────────────────────────────────────────────────────────
|
||
def classic_regression(p):
|
||
# [R16 — THE FLIP] The prime-law covenant MOVED from "flags off" to "?classic=1": the classic boot must
|
||
# stay byte-identical to the frozen v2 baseline FOREVER — synthetic fingerprint 0x5f76e76, zero gig
|
||
# layer, no opt-in flags, the R7 streamed roster (which IS the v2.0 baseline). Same assertions as the old
|
||
# flags-off gate, now booted with ?classic=1 (the four flipped flags are off, everything else v2-default).
|
||
head('CLASSIC REGRESSION (?classic=1 → the frozen v2 covenant)')
|
||
b, pg, errs = new_page(p)
|
||
reqs = [] # [R37 0.5] the wire, watched from before the first byte
|
||
pg.on('request', lambda r: reqs.append(r.url))
|
||
try:
|
||
boot(pg, 'classic=1')
|
||
state = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG;
|
||
D.shot('street_noon'); // fixed pose → comparable draws/tris
|
||
const i = D.info();
|
||
const c = P.citizens || {};
|
||
return { draws:i.drawCalls, tris:i.tris, mode:i.mode,
|
||
winmap: !!(P.winmap && P.winmap.active),
|
||
digActive: !!(P.interiorMode && P.interiorMode.digActive),
|
||
streamMode: !!c.streamMode, // R7: real roster-mode flag (was reading a nonexistent prop)
|
||
// [R12] the gig layer must be wholly absent flags-off: no state machine, no schedule
|
||
// in the plan, no pub converted, and none of Lane B's venue geometry in the scene.
|
||
gigState: !!P.gigs, planGigs: (P.plan.gigs||[]).length, posters: (P.plan.posters||[]).length,
|
||
pub: !!(P.plan.shops||[]).find(s => s.venue || s.type === 'pub'),
|
||
venueGeo: !!P.scene.getObjectByName('venue-presentation') };
|
||
}""")
|
||
# determinism anchor — run Lane A's selfcheck in-process (golden fingerprint)
|
||
r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT),
|
||
capture_output=True, text=True)
|
||
# [R27w5] The hardcoded literal that used to ride here (`or '3fa36874' in r.stdout`) is GONE —
|
||
# A caught it. It was dead the moment the covenant moved, and worse than dead: an OR-clause
|
||
# matching a bare string anywhere in stdout would go green off a sentence ABOUT the old covenant.
|
||
# The check derives from GOLDEN_HASH or it doesn't exist.
|
||
hash_ok = (f'{GOLDEN_HASH:#010x}' in r.stdout) or (hex(GOLDEN_HASH) in r.stdout)
|
||
# [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 (then 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.
|
||
if state['winmap'] or state['digActive']:
|
||
FAIL(f"a flipped/opt-in flag leaked into ?classic (winmap/dig): {state}")
|
||
else: OK('flipped + opt-in flags off under ?classic (winmap/dig)')
|
||
# [R31 — THE DIG FLIP, classic side] dig is default-ON now, so classic's dig-lessness must be
|
||
# MEASURED at a bin, not inferred from an idle digActive (which is vacuously false on the street):
|
||
# enter a record shop, E aimed at a bin — the riffle must NOT open and ZERO dig DOM may exist
|
||
# (createDig is never constructed off-flag; the E falls through trySell(null game) + shelf no-op).
|
||
dug = pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG; D.setSegment(2);
|
||
const s = (P.plan.shops || []).find(x => x.type === 'record' && P.isOpen(x));
|
||
if (!s) return { err: 'no open record shop' };
|
||
D.enterShop(s.id);
|
||
const bins = []; P.interiorMode.current.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bins.push(o); });
|
||
if (!bins.length) { D.exitShop(); return { err: 'no bins' }; }
|
||
const bp = new P.THREE.Vector3(); bins[0].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();
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
|
||
const opened = !!P.interiorMode.digActive;
|
||
const dom = document.querySelectorAll('[class^="pcdg"]').length;
|
||
D.exitShop();
|
||
return { opened, dom };
|
||
}""")
|
||
if dug.get('err'): WARN(f"?classic dig-inert leg unmeasured — {dug['err']}")
|
||
elif not dug['opened'] and dug['dom'] == 0:
|
||
OK('?classic dig INERT at the bin — E opened nothing, zero dig DOM (measured, not assumed)')
|
||
else:
|
||
FAIL(f"?classic dig LEAKED: E at a bin → opened={dug['opened']}, dig DOM nodes={dug['dom']}")
|
||
if state['streamMode']:
|
||
OK('R7 roster flip in effect — streamed roster ACTIVE under ?classic (the v2 baseline)')
|
||
else: FAIL('roster flip REGRESSED — ?classic is not streaming (expected streamMode:true)')
|
||
|
||
# [R12/R16] gig layer wholly absent under ?classic. Any of these present means the gig layer escaped
|
||
# the classic gate and the frozen v2 covenant is broken.
|
||
gig_leaks = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
|
||
if not gig_leaks:
|
||
OK('v3 gig layer wholly absent under ?classic (no state machine, no plan.gigs/posters, no pub, no venue geometry)')
|
||
else:
|
||
FAIL(f"v3 gig layer LEAKED into ?classic: {[(k, state[k]) for k in gig_leaks]}")
|
||
|
||
if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX:
|
||
OK(f"?classic street view within budget (draws {state['draws']}≤{STREET_DRAWS_MAX}, tris {state['tris']}≤{STREET_TRIS_MAX})")
|
||
else: FAIL(f"?classic street view OVER budget: draws {state['draws']}, tris {state['tris']}")
|
||
|
||
# Drift WARN is DRAWS-ONLY — tris is crowd-variable post-flip (guarded by the hard cap above).
|
||
dd = abs(state['draws'] - BASE_DRAWS) / BASE_DRAWS
|
||
if dd <= 0.40:
|
||
OK(f"draws within 40% of re-pinned baseline ({state['draws']} vs {BASE_DRAWS}); tris {state['tris']} (crowd-variable, ≤cap)")
|
||
else:
|
||
WARN(f"draws drifted >40% from baseline ({state['draws']} vs {BASE_DRAWS}) — investigate")
|
||
|
||
if errs: FAIL(f"{len(errs)} console error(s) under ?classic; first: {errs[0][:140]}")
|
||
else: OK('0 console errors under ?classic')
|
||
|
||
# ── [R37 item 0.5] THE NETWORK ALLOW-LIST ────────────────────────────────────────────────
|
||
# Everything above this line has been true since R16 and none of it watches the wire. The
|
||
# capture covers the whole gate path: boot + the fleet + the dig-inert leg's real shop entry
|
||
# (which is what reaches the depot's interior fittings).
|
||
pg.wait_for_timeout(1200) # let late/streamed fetches land before the verdict
|
||
vio, tiers, observed = classic_network_verdict(reqs, HOST)
|
||
if vio is None:
|
||
FAIL(f"classic network allow-list has NO AUTHORITY — `git ls-tree {V2_TAG}` / "
|
||
f"`git show {V2_TAG}:web/assets/manifest.json` failed. The allow-list is derived from "
|
||
f"the {V2_TAG} tag; without it this arm asserts nothing and must not pass.")
|
||
else:
|
||
core_seen = [k for k in CLASSIC_REQUIRED if any(k in u for u in observed)]
|
||
if len(core_seen) != len(CLASSIC_REQUIRED):
|
||
missing = [k for k in CLASSIC_REQUIRED if k not in core_seen]
|
||
FAIL(f"classic network: the REQUIRED v2 core was not observed ({missing}) — the observer "
|
||
f"is not watching a live boot, so a clean allow-list verdict here would be vacuous")
|
||
if vio:
|
||
FAIL(f"CLASSIC FETCH SURFACE WIDENED — {len(vio)} fetch(es) outside the {V2_TAG} baseline: "
|
||
+ '; '.join(f"{u.split('/')[-1]} [{why}]" for u, why in vio[:6])
|
||
+ (f" (+{len(vio)-6} more)" if len(vio) > 6 else '')
|
||
+ ". Allow-list, not deny-list: either the asset belongs in classic and the covenant "
|
||
"needs a ruling + a re-pin, or it must be gated out of ?classic (R36 precedent).")
|
||
else:
|
||
depot_n = len(tiers.get(f'depot asset named in the {V2_TAG} manifest', []))
|
||
v2_n = len(tiers.get(f'same-origin asset present in the {V2_TAG} tree', []))
|
||
code_n = len(tiers.get('code (same-origin module/document)', []))
|
||
exc_n = sum(len(v) for k, v in tiers.items() if k.startswith('CARRIED EXCEPTION'))
|
||
OK(f"classic fetch surface CLEAN against the {V2_TAG} tag ({V2_TAG} = v2's final tagged "
|
||
f"state, Fable's R37 ruling) — {len(observed)} network requests, 0 outside the baseline "
|
||
f"({code_n} code modules · {v2_n} same-origin assets present at {V2_TAG} · {depot_n} depot "
|
||
f"assets named in the {V2_TAG} manifest, incl. the four v2-era street GLBs, which are "
|
||
f"BASELINE and must never be 'fixed' away · {exc_n} carried exception(s), listed below)")
|
||
for tier, urls in sorted(tiers.items()):
|
||
if tier.startswith('CARRIED EXCEPTION'):
|
||
print(f" \033[33m·\033[0m EXCEPTION (NOT {V2_TAG} baseline): "
|
||
f"{sorted({u.split('/')[-1] for u in urls})} — {tier.split('— ', 1)[-1]}")
|
||
|
||
# ── THE CONTROL, demonstrated in-run: two fabricated fetches must both go RED ──
|
||
# (a) the R36 breach replayed exactly — a same-origin ped GLB added after v2.0. An
|
||
# allow-list phrased as "the four depot GLBs + same-origin" would be GREEN on this.
|
||
# (b) an asset name that has never existed — proves a brand-new file cannot slip through
|
||
# on a 404 (the request is still on the wire).
|
||
ctl = list(reqs)
|
||
pg.evaluate("""async () => { for (const u of ['models/peds/woman_dj_01.glb',
|
||
'assets/gen/__gate_control_v8_palette.jpg'])
|
||
{ try { await fetch(u); } catch (e) {} } }""")
|
||
pg.wait_for_timeout(400)
|
||
injected = [u for u in reqs if u not in ctl]
|
||
cvio, _, _ = classic_network_verdict(reqs, HOST)
|
||
names = sorted({u.split('/')[-1] for u, _ in (cvio or [])})
|
||
if cvio is not None and len(cvio) == len(vio) + 2 and \
|
||
'woman_dj_01.glb' in names and '__gate_control_v8_palette.jpg' in names:
|
||
OK(f"CONTROL: the allow-list DISCRIMINATES — injecting the R36 breach itself "
|
||
f"(same-origin models/peds/woman_dj_01.glb) plus a never-existed asset turned a clean "
|
||
f"verdict into exactly 2 violations ({names}); {len(injected)} injected request(s) seen")
|
||
else:
|
||
FAIL(f"CONTROL FAILED: the fabricated fetches did not go red — the allow-list does not "
|
||
f"discriminate (before {len(vio)}, after {len(cvio) if cvio is not None else 'None'}, "
|
||
f"named {names}, injected {injected})")
|
||
|
||
# R7 escape hatch: ?roster=v1 must restore the fixed roster (streamMode:false), render, no errors.
|
||
b2, pg2, errs2 = new_page(p)
|
||
try:
|
||
boot(pg2, 'roster=v1')
|
||
esc = pg2.evaluate("() => ({ streamMode: !!(window.PROCITY.citizens||{}).streamMode, mode: window.DBG.info().mode })")
|
||
if not esc['streamMode'] and esc['mode'] == 'street':
|
||
OK('escape hatch ?roster=v1 → fixed roster (streamMode:false), renders')
|
||
else: FAIL(f"escape hatch ?roster=v1 broken: {esc}")
|
||
if errs2: FAIL(f"?roster=v1: {len(errs2)} console error(s); first: {errs2[0][:140]}")
|
||
finally:
|
||
b2.close()
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def default_boot_gate(p):
|
||
# [R16 — THE FLIP] The enforcement arm of the DEFAULT law: the no-flag boot is now the full experience —
|
||
# all four flags on, the gig layer present, deterministic to the gig golden. This is the number every
|
||
# budget/smoke now means (its inverse is classic_regression above).
|
||
head('DEFAULT-BOOT GATE (the flip → the town shows its best by default)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
state = pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
D.setSegment(5); D.shot('venue_night'); // NIGHT at a venue → the busiest default view (gigs+pools+winmap+tram)
|
||
const i = D.info();
|
||
return { flags: P.flags,
|
||
gigState: !!P.gigs, planGigs: (P.plan.gigs||[]).length, posters: (P.plan.posters||[]).length,
|
||
pub: !!(P.plan.shops||[]).find(s => s.venue), venueGeo: !!P.scene.getObjectByName('venue-presentation'),
|
||
winmapActive: !!(P.winmap && P.winmap.active), tram: !!P.scene.getObjectByName('tram'),
|
||
draws: i.drawCalls, tris: i.tris };
|
||
}""")
|
||
f = state['flags'] or {}
|
||
if f.get('gigs') and f.get('weather') and f.get('winmap') and f.get('tram') and not f.get('classic'):
|
||
OK('default boot flips all four ON (gigs · weather · winmap · tram), classic off')
|
||
else:
|
||
FAIL(f"default boot flag intent wrong: {f}")
|
||
present = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]]
|
||
if len(present) == 5 and state['tram']:
|
||
OK(f"the town is live: gig layer present ({state['planGigs']} gigs, {state['posters']} posters, venue geo) · tram up (winmap is a window shader — its own smoke)")
|
||
else:
|
||
FAIL(f"default boot not fully live: gig={present}, tram={state['tram']}")
|
||
# The default boot's plan IS the gig-augmented plan; selfcheck pins its fingerprint to the gig golden
|
||
# (GIG_GOLDEN) INTERNALLY and is ALL GREEN only if it matches. (selfcheck prints the base fingerprint
|
||
# in its summary but asserts the gig golden silently — so we gate on the green run, not a substring.)
|
||
r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT), capture_output=True, text=True)
|
||
if r.returncode == 0 and 'ALL GREEN' in r.stdout:
|
||
OK(f"default plan pins to the gig golden {hex(GIG_GOLDEN)} (selfcheck ALL GREEN — the gig-layer golden asserts pass)")
|
||
else:
|
||
FAIL(f"selfcheck not green — the gig golden {hex(GIG_GOLDEN)} or determinism broke: {r.stdout[-160:]}")
|
||
# budget on the DEFAULT boot at the busiest venue block (the number the budget law now means)
|
||
if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX:
|
||
OK(f"default-boot street budget @ venue block: {state['draws']} draws ≤ {STREET_DRAWS_MAX}, {state['tris']:,} tris ≤ {STREET_TRIS_MAX:,}")
|
||
else:
|
||
FAIL(f"default-boot street OVER budget @ venue block: {state['draws']} draws / {state['tris']:,} tris (Fable scope call — not silent-trim)")
|
||
# [R31 — THE DIG FLIP GATE] Fable's wave-0 playtest found the alpha-blocker this whole suite
|
||
# missed: every R30 game-loop gate booted `dig=1` EXPLICITLY (belt-and-braces that became a
|
||
# blindfold), so the suite was green while the NAKED boot's E did nothing at a bin — index.html
|
||
# still carried the v2-era opt-in. The gates tested the game; nobody tested that the default boot
|
||
# IS the game. So this gate now proves dig REACHABILITY with zero dig params: enter a record shop,
|
||
# E at an aimed bin, the riffle must open. Un-flip the default again and this line goes red.
|
||
f2 = state['flags'] or {}
|
||
if not f2.get('dig'):
|
||
FAIL(f"dig intent OFF on the naked boot (PROCITY.flags.dig falsy) — the R31 flip regressed: {f2}")
|
||
dug = pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG; D.setSegment(2);
|
||
const s = (P.plan.shops || []).find(x => x.type === 'record' && P.isOpen(x));
|
||
if (!s) return { why: 'no open record shop' };
|
||
D.enterShop(s.id);
|
||
const bins = []; P.interiorMode.current.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bins.push(o); });
|
||
if (!bins.length) { D.exitShop(); return { why: 'no bins' }; }
|
||
const bp = new P.THREE.Vector3(); bins[0].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();
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
|
||
const opened = !!P.interiorMode.digActive;
|
||
const out = document.querySelector('.pcdg-out'); if (out) out.click();
|
||
const closed = !P.interiorMode.digActive;
|
||
D.exitShop();
|
||
return { opened, closed };
|
||
}""")
|
||
if dug.get('opened') and dug.get('closed'):
|
||
OK('dig REACHABLE on the naked boot — E at a bin opened the riffle (and closed clean); the default boot IS the game')
|
||
elif dug.get('opened'):
|
||
WARN(f"naked-boot dig opened but close unconfirmed — {dug}")
|
||
else:
|
||
FAIL(f"THE DIG IS OFF ON A PLAIN BOOT — the R31 alpha-blocker is back: {dug}")
|
||
if errs: FAIL(f"{len(errs)} console error(s) on the default boot; first: {errs[0][:140]}")
|
||
else: OK('0 console errors on the default boot')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke(p, label, query, is_combo=False):
|
||
head(f'SMOKE: {label} (?{query})')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, query)
|
||
# cross ≥2 chunks by teleporting between separated street nodes
|
||
chunks = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG, seen=new Set();
|
||
const ns = P.plan.streets.nodes;
|
||
for (const idx of [0, (ns.length/3|0), (2*ns.length/3|0), ns.length-1]) {
|
||
const n = ns[idx]; D.teleport(n.x, n.z, 0);
|
||
for (let k=0;k<10;k++) (P.citizens&&P.citizens.update&&P.citizens.update(0.05));
|
||
const c = D.info().chunk; if (c!=null) seen.add(String(c));
|
||
}
|
||
return seen.size;
|
||
}""")
|
||
if chunks >= 2: OK(f'crossed {chunks} chunks streaming (flag active, no crash)')
|
||
else: WARN(f'only {chunks} distinct chunks seen (teleport/stream probe)')
|
||
|
||
# enter one interior (open shop), exit
|
||
entered = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG;
|
||
D.setSegment(2);
|
||
const s=(P.plan.shops||[]).find(x=>x.type==='record' && P.isOpen(x)) || (P.plan.shops||[]).find(x=>P.isOpen(x));
|
||
if(!s) return {ok:false};
|
||
D.enterShop(s.id);
|
||
const inMode = D.info().mode==='interior';
|
||
return {ok:inMode, shop:s.name};
|
||
}""")
|
||
if entered.get('ok'): OK(f"entered interior '{entered.get('shop')}' (mode=interior)")
|
||
else: FAIL('could not enter an interior')
|
||
|
||
# dig-specific: open one riffle + close, leak-free
|
||
if 'dig=1' in query:
|
||
dug = pg.evaluate("""() => {
|
||
const P=window.PROCITY, THREE=P.THREE, room=P.interiorMode.current;
|
||
if(!room) return {ok:false, why:'no room'};
|
||
const bins=[]; room.group.traverse(o=>{ if(o.userData&&o.userData.kind==='bin') bins.push(o); });
|
||
if(!bins.length) return {ok:false, why:'no bins'};
|
||
const bp=new THREE.Vector3(); bins[0].getWorldPosition(bp);
|
||
P.camera.position.set(bp.x,1.6,bp.z+1.8); P.camera.lookAt(bp.x,bp.y+0.4,bp.z); P.camera.updateMatrixWorld();
|
||
window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}));
|
||
const opened = P.interiorMode.digActive;
|
||
const btn=document.querySelector('.pcdg-out'); if(btn) btn.click();
|
||
return { ok: opened, closed: !P.interiorMode.digActive };
|
||
}""")
|
||
pg.wait_for_timeout(300)
|
||
if dug.get('ok') and dug.get('closed'): OK('dig: riffle opened (E on bin) + closed (WALK OUT)')
|
||
elif dug.get('ok'): WARN(f"dig: opened but close not confirmed ({dug})")
|
||
else: FAIL(f"dig: riffle did not open ({dug.get('why','?')})")
|
||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
||
|
||
if errs: FAIL(f"{label}: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK(f'{label}: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
def interior_budget(p):
|
||
"""Decision #2: interior draw budget is law — ≤350 draws worst room. Sweep every shop type
|
||
(dig on, so this also confirms batching kept ?dig=1 riffling). C enforces via its sweep; F pins it."""
|
||
head('INTERIOR DRAW BUDGET (≤350 draws/room law — decision #2)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'dig=1')
|
||
pg.evaluate("() => window.DBG.setSegment(2)") # midday → shops open
|
||
types = ['record', 'opshop', 'toy', 'book', 'video', 'pawn', 'milkbar', 'dept', 'stall']
|
||
rows, seen = [], set()
|
||
for t in types:
|
||
picked = pg.evaluate("""(t) => {
|
||
const P=window.PROCITY, D=window.DBG;
|
||
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x));
|
||
if(!s) return null; D.enterShop(s.id); return {id:s.id, name:s.name};
|
||
}""", t)
|
||
if not picked:
|
||
continue
|
||
pg.wait_for_timeout(500) # let async GLB fittings load → settled worst-case draws
|
||
info = pg.evaluate("() => { const i=window.DBG.info(); return {draws:i.drawCalls, tris:i.tris, mode:i.mode}; }")
|
||
rows.append({'type': t, 'shop': picked['name'], **info})
|
||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
||
pg.wait_for_timeout(150)
|
||
over = [r for r in rows if r['draws'] > 350]
|
||
for r in over:
|
||
FAIL(f"interior {r['type']} '{r['shop']}': {r['draws']} draws > 350")
|
||
if rows and not over:
|
||
worst = max(rows, key=lambda r: r['draws'])
|
||
OK(f"all {len(rows)} room types ≤350 draws (worst: {worst['type']} {worst['draws']}, tris {worst['tris']})")
|
||
elif not rows:
|
||
WARN('no interiors sampled (no open shops of the swept types)')
|
||
if errs:
|
||
FAIL(f"interior sweep: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def _enter_type(pg, t, extra=''):
|
||
"""Enter an open shop of type `t`; returns the shop name or {err}."""
|
||
return pg.evaluate("""(t) => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x));
|
||
if(!s) return {err:'no open '+t+' shop'};
|
||
D.enterShop(s.id); return { shop: s.name };
|
||
}""", t)
|
||
|
||
def _pack_ready(pg, t):
|
||
try:
|
||
pg.wait_for_function("async () => { const m = await import('./js/interiors/interiors.js');"
|
||
f" return !!(m.getStockPack && m.getStockPack('{t}')); }}", timeout=12000)
|
||
return True
|
||
except Exception:
|
||
return False
|
||
|
||
def smoke_stock(p):
|
||
"""GRADUATED STRICT (R8): ?stock=real → real GODVERSE sleeves (batched userData.isStock atlas meshes)."""
|
||
head('SMOKE: stock=real (?stock=real — real record sleeves; STRICT r8)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'stock=real&dig=1')
|
||
ready = _pack_ready(pg, 'record')
|
||
r = _enter_type(pg, 'record')
|
||
if r.get('err'): FAIL(f"stock=real: {r['err']}"); return
|
||
n = pg.evaluate("() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.isStock&&o.material&&o.material.map) n++; }); return n; }")
|
||
if not ready: FAIL(f"stock=real: record pack didn't preload in 12s (E ships it) — got {n} isStock meshes")
|
||
elif n > 0: OK(f"stock=real: {n} real atlas sleeve mesh(es) in '{r['shop']}' — non-parody, batched")
|
||
else: FAIL(f"stock=real: pack loaded but 0 isStock atlas meshes in '{r['shop']}'")
|
||
if errs: FAIL(f"stock=real: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_weather(p):
|
||
"""GRADUATED STRICT (R8): ?weather=rain → rain THREE.Points layer + PROCITY.weather.state=='rain'."""
|
||
head('SMOKE: weather (?weather=rain — rain particles + wet ground; STRICT r8)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'weather=rain')
|
||
st = pg.evaluate("() => { const P=window.PROCITY; return { state: P.weather&&P.weather.state, "
|
||
"intensity: P.weather&&P.weather.intensity, rainPts: !!P.scene.getObjectByProperty('type','Points') }; }")
|
||
if st['state'] == 'rain' and st['rainPts']:
|
||
OK(f"weather=rain: rain Points present + PROCITY.weather.state=='rain' (intensity {st['intensity']})")
|
||
else: FAIL(f"weather=rain broken: {st}")
|
||
if errs: FAIL(f"weather=rain: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_buy(p):
|
||
"""NEW (warn): buy-v0 — open dig, pull + BUY a sleeve, assert wallet cash dropped + item banked."""
|
||
head('SMOKE: buy-v0 (dig BUY → session wallet; STRICT r9)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'dig=1')
|
||
setup = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const rec=(P.plan.shops||[]).find(s=>s.type==='record' && P.isOpen(s));
|
||
if(!rec) return {err:'no record shop'};
|
||
D.enterShop(rec.id);
|
||
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);
|
||
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 { cash0: P.wallet.cash(), count0: P.wallet.count() };
|
||
}""")
|
||
if setup.get('err'): FAIL(f"buy-v0: {setup['err']}"); return
|
||
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}))") # open dig
|
||
pg.wait_for_timeout(600)
|
||
# pull the front sleeve (canvas click, no drag → dig.pull) then click its BUY IT button
|
||
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(400)
|
||
res = pg.evaluate("""() => {
|
||
const w=window.PROCITY.wallet, cash0=w.cash(), count0=w.count();
|
||
const btn=document.querySelector('.pcdg-panel button');
|
||
if(!btn) return {err:'no BUY panel (pull failed)'};
|
||
if(btn.disabled) return {err:'BUY disabled (broke?)'};
|
||
btn.click();
|
||
return { cash0, count0, cash1:w.cash(), count1:w.count() };
|
||
}""")
|
||
if res.get('err'): FAIL(f"buy-v0: {res['err']}")
|
||
elif res['cash1'] < res['cash0'] and res['count1'] == res['count0'] + 1:
|
||
OK(f"buy-v0: bought 1 — cash {res['cash0']}→{res['cash1']}, bag {res['count0']}→{res['count1']}")
|
||
else: FAIL(f"buy-v0: unexpected wallet delta {res}")
|
||
if errs: FAIL(f"buy-v0: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_patronage(p):
|
||
"""NEW (warn): peds duck into open shops — assert ≥1 ped parks at a shop door over N seconds."""
|
||
head('SMOKE: patronage (peds visit open shops; STRICT r9)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '') # default: streamed roster + patronage on
|
||
# advance the sim ~12s of ticks at a busy midday node; count peds flagged as inside/at-door
|
||
res = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;}
|
||
let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}}
|
||
D.teleport(best.x,best.z,0); P.citizens.setPopulation(200);
|
||
let everInside=0; const seen=new Set();
|
||
for(let i=0;i<360;i++){ P.citizens.update(0.05);
|
||
const list=P.citizens._activeList||[];
|
||
for(const c of list){ if(c && (c.patronInside||c.inside||c.patronTarget) && !seen.has(c.id)){ seen.add(c.id); everInside++; } }
|
||
}
|
||
return { active:(P.citizens._activeList||[]).length, everVisited: everInside, patronOn: P.citizens.patronageOn };
|
||
}""")
|
||
if not res['patronOn']: FAIL("patronage: off (patronageOn=false) — expected default-on")
|
||
elif res['everVisited'] > 0: OK(f"patronage: {res['everVisited']} ped(s) headed to/into a shop door over ~18s (active {res['active']})")
|
||
else: FAIL(f"patronage: 0 peds visited a shop over ~18s (active {res['active']}) — check door points / timing")
|
||
if errs: FAIL(f"patronage: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_booktoy(p):
|
||
"""NEW (warn): book/toy packs — ?stock=real static shelf stock (real spine/box atlas meshes)."""
|
||
head('SMOKE: book/toy stock (?stock=real — real spines/boxes; STRICT r9)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'stock=real')
|
||
for t in ('book', 'toy'):
|
||
_pack_ready(pg, t)
|
||
r = _enter_type(pg, t)
|
||
if r.get('err'): FAIL(f"book/toy: {r['err']}"); continue
|
||
# R9: real static stock is EITHER an isStock atlas mesh (records/toys) OR a buyMesh shelf
|
||
# (book spines / toy boxes, C2 buy-anywhere). Parody stock has neither.
|
||
n = pg.evaluate("""() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{
|
||
const u=o.userData||{}; if((u.isStock && o.material && o.material.map) || (u.buyMesh && u.buyItems && u.buyItems.length)) n++; }); return n; }""")
|
||
if n > 0: OK(f"{t} stock: {n} real stock mesh(es) in '{r['shop']}'")
|
||
else: FAIL(f"{t} stock: 0 isStock atlas meshes in '{r['shop']}' (pack missing → parody fallback?)")
|
||
pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()")
|
||
pg.wait_for_timeout(150)
|
||
if errs: FAIL(f"book/toy: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_presence(p):
|
||
"""NEW (warn): interior presence — enter an OCCUPIED shop, assert browser rigs == min(occupancy,3),
|
||
dispose clean on exit, 0 errors (the C→D→F seam)."""
|
||
head('SMOKE: interior presence (browsers in occupied shops; new → warn-level)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'stock=real')
|
||
res = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;}
|
||
let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}}
|
||
D.teleport(best.x,best.z,0); P.citizens.setPopulation(200);
|
||
for(let i=0;i<400;i++) P.citizens.update(0.05);
|
||
let shop=null,occ=null;
|
||
for(const s of plan.shops){ const o=P.citizens.occupancyOf(s.id); if(o&&o.count>0){shop=s;occ=o;break;} }
|
||
if(!shop) return {err:'no occupied shop after patronage'};
|
||
D.enterShop(shop.id);
|
||
const kl=P.interiorMode.keepers.keepers||[];
|
||
const total=kl.length, pts=(P.interiorMode.current.browsePoints||[]).length;
|
||
return { shop:shop.name, occ:occ.count, expect:Math.min(occ.count,3), totalKeepers:total, browsePoints:pts };
|
||
}""")
|
||
if res.get('err'): WARN(f"presence: {res['err']}"); return
|
||
# total keepers = 1 counter + browsers; browsers should == min(occ,3)
|
||
browsers = res['totalKeepers'] - 1
|
||
if browsers == res['expect'] and res['browsePoints'] >= 1:
|
||
OK(f"presence: {browsers} browser(s) in '{res['shop']}' == min(occupancy {res['occ']},3); {res['browsePoints']} browse points")
|
||
else:
|
||
WARN(f"presence: browsers {browsers} != expect {res['expect']} (occ {res['occ']}, pts {res['browsePoints']}, keepers {res['totalKeepers']})")
|
||
clean = pg.evaluate("() => { window.DBG.exitShop(); return (window.PROCITY.interiorMode.keepers.keepers||[]).length; }")
|
||
if clean == 0: OK("presence: browsers disposed clean on exit (0 keepers)")
|
||
else: WARN(f"presence: {clean} keeper(s) leaked after exit")
|
||
if errs: WARN(f"presence: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_shelfbuy(p):
|
||
"""NEW (warn): buy-anywhere — E aimed at a book spine / toy box buys over the wallet (Lane C C2)."""
|
||
head('SMOKE: shelf-buy (?stock=real book/toy pull-and-buy; new → warn-level)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'stock=real')
|
||
_pack_ready(pg, 'book')
|
||
res = pg.evaluate("""() => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const bk=(P.plan.shops||[]).find(s=>s.type==='book' && P.isOpen(s));
|
||
if(!bk) return {err:'no book shop'};
|
||
D.enterShop(bk.id);
|
||
const meshes=[]; P.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.buyMesh&&o.userData.buyItems&&o.userData.buyItems.length) meshes.push(o); });
|
||
if(!meshes.length) return {err:'no buy meshes (pack not loaded?)'};
|
||
const m=meshes[0], it=m.userData.buyItems[0], wp=new P.THREE.Vector3();
|
||
m.localToWorld(wp.copy(it.center));
|
||
P.camera.position.set(wp.x,wp.y,wp.z+1.0); P.camera.lookAt(wp.x,wp.y,wp.z); P.camera.updateMatrixWorld();
|
||
const cash0=P.wallet.cash(), count0=P.wallet.count();
|
||
window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}));
|
||
return { shop:bk.name, buyMeshes:meshes.length, cash0, count0, cash1:P.wallet.cash(), count1:P.wallet.count() };
|
||
}""")
|
||
if res.get('err'): WARN(f"shelf-buy: {res['err']}")
|
||
elif res['cash1'] < res['cash0'] and res['count1'] == res['count0'] + 1:
|
||
OK(f"shelf-buy: bought a book spine in '{res['shop']}' — cash {res['cash0']}→{res['cash1']}, bag {res['count0']}→{res['count1']} ({res['buyMeshes']} buy meshes)")
|
||
else: WARN(f"shelf-buy: no purchase — {res}")
|
||
if errs: WARN(f"shelf-buy: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# [R37 item 0.2] the fenced subject. B's R20 per-town fence (tram.js:101-113) refuses the tram on a
|
||
# real-roads graph whose best main chain fronts < 5 shops. R21 measured katoomba/fremantle/bendigo
|
||
# fenced at 3/2/1 shops fronted; katoomba is the absence arm's subject. If it ever stops being fenced
|
||
# this gate FAILS rather than skipping — an absence arm with no subject is the vacuous gate again.
|
||
TRAM_FENCED_TOWN = 'katoomba_real'
|
||
TRAM_LANE = 3.2 # tram.js LANE — offset from the road centreline (drive on the left, AU)
|
||
TRAM_SPEED = 9.0 # tram.js SPEED (m/s)
|
||
TRAM_DWELL = 3.5 # tram.js DWELL (s at each stop)
|
||
|
||
# The behavioural integration, run on an ISOLATED tram (same plan, throwaway scene) so the shell's own
|
||
# rAF tick cannot contaminate the dt sequence. Returns the whole trajectory's verdicts.
|
||
_TRAM_DRIVE_JS = r"""
|
||
async (cfg) => {
|
||
const P = window.PROCITY;
|
||
const m = await import('./js/world/tram.js');
|
||
const scene = new P.THREE.Scene();
|
||
const t = m.createTram({ scene, plan: P.plan, camera: P.camera, lighting: null });
|
||
const info = t.routeInfo || {};
|
||
if (info.fenced || !t.stops) return { fenced: !!info.fenced, stops: t.stops | 0, info };
|
||
// independent geometry reference: the MAIN-edge segments straight off the plan (NOT tram.js's own
|
||
// polyline) — so "the tram is 3.2 m off the road centreline" is checked against Lane A's data.
|
||
const nodes = new Map(P.plan.streets.nodes.map(n => [n.id, n]));
|
||
const segs = P.plan.streets.edges.filter(e => e.kind === 'main')
|
||
.map(e => ({ a: nodes.get(e.a), b: nodes.get(e.b) })).filter(s => s.a && s.b);
|
||
const distToMain = (x, z) => {
|
||
let best = 1e18;
|
||
for (const s of segs) {
|
||
const dx = s.b.x - s.a.x, dz = s.b.z - s.a.z, L2 = dx * dx + dz * dz || 1;
|
||
let u = ((x - s.a.x) * dx + (z - s.a.z) * dz) / L2; u = Math.max(0, Math.min(1, u));
|
||
const px = s.a.x + dx * u, pz = s.a.z + dz * u;
|
||
const d = Math.hypot(x - px, z - pz); if (d < best) best = d;
|
||
}
|
||
return best;
|
||
};
|
||
const dt = cfg.dt, N = Math.round(cfg.seconds / dt);
|
||
const CRUISE = cfg.speed * dt * 1.05; // the most the schedule may move it in one tick, +5%
|
||
let prev = { x: t.group.position.x, z: t.group.position.z };
|
||
let path = 0, maxStep = 0, stills = 0, dwells = 0, run = 0;
|
||
const jumps = []; // steps larger than CRUISE: the end-of-line LANE swap
|
||
const offs = [];
|
||
const runMin = Math.floor((cfg.dwellS * 0.8) / dt);
|
||
for (let i = 0; i < N; i++) {
|
||
t.update(dt);
|
||
const x = t.group.position.x, z = t.group.position.z;
|
||
const step = Math.hypot(x - prev.x, z - prev.z);
|
||
if (step > CRUISE) jumps.push(+step.toFixed(3));
|
||
else { path += step; if (step > maxStep) maxStep = step; }
|
||
if (step < 1e-4) { run++; stills++; }
|
||
else {
|
||
if (run >= runMin) dwells++;
|
||
run = 0;
|
||
offs.push(+distToMain(x, z).toFixed(3));
|
||
}
|
||
prev = { x, z };
|
||
}
|
||
if (run >= runMin) dwells++;
|
||
offs.sort((a, b) => a - b);
|
||
t.dispose();
|
||
// Three kinds of legitimate discontinuity exist in tram.js and all three are bounded by 2·LANE:
|
||
// (a) the END REVERSAL — `dir` flips ⇒ the LANE offset mirrors across the road ⇒ exactly 2·LANE;
|
||
// (b) the STOP SNAP — `s = stopS[nextStop]` when within EPS(2.5 m) of a mark;
|
||
// (c) the BEND KINK — the lane offset is taken off the SEGMENT direction, so at a polyline
|
||
// bend of θ the offset point steps 2·LANE·sin(θ/2) sideways.
|
||
// The gate asserts (a) happened (it reached the end and turned) and that NOTHING exceeds 2·LANE.
|
||
const big = jumps.filter(j => j >= cfg.lane * 2 - 0.5);
|
||
return { fenced: false, stops: t.stops | 0, info, path: +path.toFixed(1), maxStep: +maxStep.toFixed(4),
|
||
dwells, stills, stillS: +(stills * dt).toFixed(2), samples: offs.length,
|
||
jumps: jumps.length, reversals: big.length,
|
||
jumpMin: jumps.length ? Math.min(...jumps) : null,
|
||
jumpMax: jumps.length ? Math.max(...jumps) : null,
|
||
offMed: offs.length ? offs[offs.length >> 1] : null,
|
||
offMin: offs.length ? offs[0] : null, offMax: offs.length ? offs[offs.length - 1] : null };
|
||
}
|
||
"""
|
||
|
||
# Observe the LIVE tram under the shell's own rAF loop (nothing driven by the harness): does the thing
|
||
# the shell built actually move? `getObjectByName('tram')` cannot tell you — the fenced path adds a
|
||
# named EMPTY group, which is exactly why the old one-line check passed on towns with no tram.
|
||
_TRAM_LIVE_JS = r"""
|
||
async (frames) => {
|
||
const P = window.PROCITY;
|
||
const g = P.scene.getObjectByName('tram');
|
||
const seen = [];
|
||
for (let i = 0; i < frames; i++) {
|
||
await new Promise(r => requestAnimationFrame(r));
|
||
seen.push([+P.tram.group.position.x.toFixed(3), +P.tram.group.position.z.toFixed(3)]);
|
||
}
|
||
let moved = 0;
|
||
for (let i = 1; i < seen.length; i++) moved += Math.hypot(seen[i][0] - seen[i - 1][0], seen[i][1] - seen[i - 1][1]);
|
||
const uniq = new Set(seen.map(s => s.join(','))).size;
|
||
return { moved: +moved.toFixed(3), uniq, frames: seen.length,
|
||
namedGroupPresent: !!g, groupChildren: g ? g.children.length : null,
|
||
first: seen[0], last: seen[seen.length - 1] };
|
||
}
|
||
"""
|
||
|
||
|
||
def _tram_state(pg):
|
||
"""The tram's own published verdict + the OLD gate's one-liner, side by side."""
|
||
return pg.evaluate("""() => {
|
||
const P = window.PROCITY, g = P.scene.getObjectByName('tram');
|
||
const ri = P.tram ? (P.tram.routeInfo || {}) : null;
|
||
return { obj: !!P.tram, legacyPresent: !!g, groupChildren: g ? g.children.length : null,
|
||
fenced: ri ? !!ri.fenced : null, stops: P.tram ? (P.tram.stops | 0) : null,
|
||
routeEdges: ri ? (ri.routeEdges | 0) : null, routeMetres: ri ? (ri.routeMetres | 0) : null,
|
||
shopsFronted: ri ? (ri.shopsFronted | 0) : null, reason: ri ? (ri.reason || null) : null,
|
||
townEdges: (P.plan.streets.edges || []).length, town: P.plan.name };
|
||
}""")
|
||
|
||
|
||
# The two predicates the gate is built from. They are FUNCTIONS, not inline asserts, precisely so the
|
||
# falsifiability control can point each one at the other arm's subject and show it go red (house law:
|
||
# every gate ships with its control demonstrated).
|
||
def _tram_runs_pred(st, drive):
|
||
"""True iff this town's tram is a RUNNING tram: route resolved, stops sane, on the road, moving."""
|
||
why = []
|
||
if not st['obj']: why.append('PROCITY.tram is null')
|
||
if st['fenced'] is not False: why.append(f"routeInfo.fenced={st['fenced']}")
|
||
if not st['stops'] or not (1 <= st['stops'] <= 60): why.append(f"stops={st['stops']} outside [1,60]")
|
||
if (st['routeEdges'] or 0) < 2: why.append(f"routeEdges={st['routeEdges']} < 2")
|
||
if (st['routeMetres'] or 0) < 50: why.append(f"routeMetres={st['routeMetres']} < 50")
|
||
if drive is None or drive.get('fenced'): why.append('behavioural drive refused (fenced/no stops)')
|
||
else:
|
||
if drive['path'] < 0.5 * (st['routeMetres'] or 0): why.append(f"travelled {drive['path']}m < half the route")
|
||
if drive['maxStep'] > TRAM_SPEED * 0.05 * 1.05: why.append(f"max cruising tick {drive['maxStep']}m exceeds SPEED·dt")
|
||
if drive['dwells'] < 1: why.append('never dwelled at a stop')
|
||
# dwell ACCOUNTING: total stationary time must equal dwells × DWELL to within a tick. This is
|
||
# what proves the stops are real marks being consumed rather than the body simply being stuck.
|
||
if abs(drive['stillS'] - drive['dwells'] * TRAM_DWELL) > 0.5:
|
||
why.append(f"stationary time {drive['stillS']}s != {drive['dwells']} dwells × {TRAM_DWELL}s")
|
||
# REVERSAL: the only permitted discontinuity is the end-of-line lane swap (dir flips ⇒ the LANE
|
||
# offset mirrors ⇒ exactly 2·LANE across the road). Its presence proves it reached the end and
|
||
# turned; its SIZE proves nothing else is teleporting the tram.
|
||
if not drive['reversals']:
|
||
why.append('never reversed at an end (no 2·LANE lane-swap discontinuity in 400 s)')
|
||
if drive['jumpMax'] is not None and drive['jumpMax'] > 2 * TRAM_LANE + 0.5:
|
||
why.append(f"discontinuity {drive['jumpMax']}m exceeds 2·LANE {2*TRAM_LANE}m — the tram is being "
|
||
f"teleported by something other than the documented end-of-line lane swap")
|
||
if drive['offMed'] is None or not (TRAM_LANE - 0.35 <= drive['offMed'] <= TRAM_LANE + 0.35):
|
||
why.append(f"median offset from the main centreline {drive['offMed']}m != LANE {TRAM_LANE}m")
|
||
return (not why), why
|
||
|
||
|
||
def _tram_absent_pred(st, live):
|
||
"""True iff this town's tram is GENUINELY absent: fenced verdict, no stops, empty group, never moves."""
|
||
why = []
|
||
if not st['obj']: why.append('PROCITY.tram is null (expected the fenced stub object)')
|
||
if st['fenced'] is not True: why.append(f"routeInfo.fenced={st['fenced']} (expected True)")
|
||
if st['stops'] != 0: why.append(f"stops={st['stops']} (expected 0)")
|
||
if st['groupChildren'] != 0: why.append(f"the named 'tram' group has {st['groupChildren']} children (expected 0)")
|
||
if live is not None and live['moved'] > 0.01: why.append(f"the fenced tram MOVED {live['moved']}m")
|
||
return (not why), why
|
||
|
||
|
||
def smoke_tram(p):
|
||
"""[R37 item 0.2 — REPAIRED] the tram gate, rebuilt from a vacuous one-liner.
|
||
|
||
What it was (flags_check.py:617-630, R9→R36): `!!scene.getObjectByName('tram')`, a printed SKIP
|
||
and an early `return` when absent, warn-level. Three faults, all fatal:
|
||
· it PASSED ON THE FENCED PATH — `tram.js:110-114` adds a *named empty group* to the scene
|
||
before returning the no-op stub, so the one assertion is true on every town where the tram
|
||
deliberately does not exist. The gate was green on precisely the towns it was meant to police.
|
||
· SKIP-and-return on absence: a missing subject exited 0. Vacuous-gate law, inside our own suite.
|
||
· it asserted nothing about the tram being a TRAM — no route, no stops, no motion.
|
||
|
||
What it is now — four arms and a demonstrated control:
|
||
1. RUNS (default boot, synthetic): route resolved (edges/metres), stops sane, and — under the
|
||
SHELL'S OWN rAF loop, nothing driven by the harness — the body measurably moves.
|
||
2. BEHAVIOUR (isolated tram, same plan, throwaway scene, fixed dt): it traverses the line,
|
||
reverses at the end, dwells at stops, never exceeds SPEED·dt, and rides exactly LANE=3.2 m
|
||
off the centreline — measured against Lane A's own main-edge segments, not tram.js's polyline.
|
||
3. GENUINELY ABSENT (a fenced real-roads town): the fenced verdict is published, stops == 0, the
|
||
named group is EMPTY, and the thing never moves. The old one-liner's `true` is printed next
|
||
to it as the standing record of what a vacuous gate looks like.
|
||
4. OFF (`?tram=0` and `?classic=1`): `PROCITY.tram` is null and no 'tram' object is in the scene.
|
||
CONTROL, demonstrated every run: the run-predicate is applied to the FENCED town's subject and
|
||
must go RED, and the absence-predicate is applied to the RUNNING tram and must go RED. Each
|
||
predicate is proven to discriminate on this tree, in this run, rather than asserted to.
|
||
|
||
STRICT (was warn-level). A missing subject FAILS."""
|
||
head('SMOKE: tram (R37 0.2 — runs where it runs · genuinely absent where fenced · control both ways)')
|
||
|
||
# ── arm 1+2: the running tram, on the default boot ──────────────────────────────────────────
|
||
b, pg, errs = new_page(p)
|
||
run_st = run_live = run_drive = None
|
||
try:
|
||
boot(pg, '')
|
||
run_st = _tram_state(pg)
|
||
if not run_st['obj']:
|
||
FAIL('tram: PROCITY.tram is NULL on the default boot — the subject is absent, so this gate '
|
||
'FAILS (the pre-R37 gate printed a SKIP here and exited 0)')
|
||
else:
|
||
run_live = pg.evaluate(_TRAM_LIVE_JS, 45)
|
||
run_drive = pg.evaluate(_TRAM_DRIVE_JS,
|
||
{'dt': 0.05, 'seconds': 400, 'dwellS': TRAM_DWELL,
|
||
'speed': TRAM_SPEED, 'lane': TRAM_LANE})
|
||
ok, why = _tram_runs_pred(run_st, run_drive)
|
||
if ok:
|
||
OK(f"tram RUNS on '{run_st['town']}': {run_st['stops']} stops over {run_st['routeMetres']} m "
|
||
f"({run_st['routeEdges']} main edges, {run_st['shopsFronted']} shops fronted)")
|
||
else:
|
||
FAIL(f"tram does NOT run on the default boot: {'; '.join(why)} [{run_st}]")
|
||
# motion under the SHELL's loop — the assertion the old gate never made
|
||
if run_live['moved'] > 0.5 and run_live['uniq'] > 3:
|
||
OK(f"tram MOVES under the shell's own loop — {run_live['moved']} m over {run_live['frames']} "
|
||
f"frames, {run_live['uniq']} distinct positions ({run_live['first']} → {run_live['last']})")
|
||
else:
|
||
FAIL(f"tram did not move under the shell's loop: {run_live}")
|
||
if run_drive and not run_drive.get('fenced'):
|
||
OK(f"tram BEHAVES: travelled {run_drive['path']} m of a {run_st['routeMetres']} m route in 400 s, "
|
||
f"{run_drive['dwells']} stop dwell(s) accounting for exactly {run_drive['stillS']} s of door "
|
||
f"time ({run_drive['dwells']}×{TRAM_DWELL} s), max cruising tick {run_drive['maxStep']} m ≤ "
|
||
f"SPEED·dt {round(TRAM_SPEED*0.05,3)} m, {run_drive['reversals']} end reversal(s) "
|
||
f"(2·LANE lane swap) of {run_drive['jumps']} bounded discontinuities "
|
||
f"({run_drive['jumpMin']}–{run_drive['jumpMax']} m ≤ 2·LANE), offset from A's main "
|
||
f"centreline med {run_drive['offMed']} m [{run_drive['offMin']}–{run_drive['offMax']}] "
|
||
f"vs LANE {TRAM_LANE} m")
|
||
if errs: FAIL(f"tram/default: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: print(' · 0 console errors (default boot)')
|
||
finally:
|
||
b.close()
|
||
|
||
# ── arm 3: the fenced town — genuinely absent, and the old one-liner's verdict on record ─────
|
||
b, pg, errs = new_page(p)
|
||
fen_st = fen_live = None
|
||
try:
|
||
boot(pg, f'plansrc=osm&town={TRAM_FENCED_TOWN}')
|
||
fen_st = _tram_state(pg)
|
||
if not fen_st['obj']:
|
||
FAIL(f"tram/fenced: PROCITY.tram is null on {TRAM_FENCED_TOWN} — the absence arm has no "
|
||
f"subject to measure; re-pick a fenced town")
|
||
elif fen_st['fenced'] is not True:
|
||
FAIL(f"tram/fenced: {TRAM_FENCED_TOWN} is NO LONGER FENCED ({fen_st['shopsFronted']} shops "
|
||
f"fronted, {fen_st['stops']} stops) — the absence arm lost its subject. Re-pick a fenced "
|
||
f"town (R21: katoomba 3 / fremantle 2 / bendigo 1 shops fronted) rather than skipping.")
|
||
else:
|
||
fen_live = pg.evaluate(_TRAM_LIVE_JS, 30)
|
||
ok, why = _tram_absent_pred(fen_st, fen_live)
|
||
if ok:
|
||
OK(f"tram GENUINELY ABSENT on {TRAM_FENCED_TOWN}: fenced=True, 0 stops, the named group is "
|
||
f"EMPTY (0 children), moved {fen_live['moved']} m over {fen_live['frames']} frames "
|
||
f"— reason: {str(fen_st['reason'])[:90]}")
|
||
else:
|
||
FAIL(f"tram/fenced: not genuinely absent — {'; '.join(why)} [{fen_st}]")
|
||
# the standing record of the hole this repair closed
|
||
if fen_st['legacyPresent']:
|
||
print(f" · [record] the pre-R37 assertion `!!scene.getObjectByName('tram')` returns TRUE here "
|
||
f"— a {fen_st['groupChildren']}-child group on a town with no tram. That is the whole bug.")
|
||
else:
|
||
WARN("tram/fenced: the fenced path no longer adds a named empty group — the R37 0.2 "
|
||
"premise moved; re-read tram.js:110-114 before trusting this gate's history")
|
||
if errs: FAIL(f"tram/fenced: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# ── THE CONTROL, demonstrated: each predicate pointed at the other arm's subject must go RED ──
|
||
if run_st and fen_st and run_st.get('obj') and fen_st.get('obj'):
|
||
ok_ctl, why_ctl = _tram_runs_pred(fen_st, None)
|
||
if not ok_ctl:
|
||
OK(f"CONTROL A: the RUN predicate applied to the fenced town goes RED as required "
|
||
f"({why_ctl[0]}) — the run arm cannot pass on a town with no tram")
|
||
else:
|
||
FAIL('CONTROL A FAILED: the RUN predicate PASSES on the fenced town — this gate is vacuous again')
|
||
ok_ctl2, why_ctl2 = _tram_absent_pred(run_st, run_live)
|
||
if not ok_ctl2:
|
||
OK(f"CONTROL B: the ABSENCE predicate applied to the running tram goes RED as required "
|
||
f"({why_ctl2[0]}) — the absence arm cannot pass on a town where the tram runs")
|
||
else:
|
||
FAIL('CONTROL B FAILED: the ABSENCE predicate PASSES on the running tram — absence is unfalsifiable')
|
||
else:
|
||
FAIL('tram: controls unrun — one of the two subjects was missing (see the failures above)')
|
||
|
||
# ── arm 4: the flag is a real off switch (?tram=0 and ?classic=1) ────────────────────────────
|
||
for q, label in (('tram=0', '?tram=0'), ('classic=1', '?classic=1')):
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, q)
|
||
off = pg.evaluate("""() => ({ obj: window.PROCITY.tram,
|
||
named: !!window.PROCITY.scene.getObjectByName('tram') })""")
|
||
if off['obj'] is None and not off['named']:
|
||
OK(f"{label}: no tram object AND no 'tram' in the scene (the flag is a real off switch)")
|
||
else:
|
||
FAIL(f"{label}: tram leaked — {off}")
|
||
if errs: FAIL(f"tram/{label}: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# ── [R37 item 0.1, F half] THE KERB GATE — poster clearance, measured on the PAINT ────────────────
|
||
# John's ruling: the band between kerb and shopfront is FOOTPATH. Lane A corrected its plan-side
|
||
# selfcheck (e4d7762) and pinned the pre-ruling failure at exactly 14 posters on the default boot;
|
||
# Lane B narrowed the road quad and extended the merged footGeos class inward (47db478) and shipped
|
||
# `?verge=0`, which reverts to the pre-ruling geometry permanently.
|
||
#
|
||
# A's gate is PLAN-SIDE: it asks whether a poster's offset is inside `vergeBand(e)[0]`. That is the
|
||
# right question for A's file, but it still never reads a number that came from B — which is the
|
||
# exact failure mode ("the gate and its subject were the same function agreeing with itself") that
|
||
# made the old one vacuous. So F's gate asks the RUNTIME question instead, and asks it of the paint:
|
||
#
|
||
# RAYCAST STRAIGHT DOWN AT EVERY POSTER AND SEE WHAT SURFACE IS ACTUALLY UNDER IT.
|
||
#
|
||
# The topmost ground hit is classified by MATERIAL IDENTITY — `skins.groundMat()` is a cached
|
||
# factory, so the road mesh's material IS the object `groundMat('djsim-road')` returns; no string
|
||
# matching, no mesh names, nothing that drifts when B edits. Topmost (not first-by-edge) is
|
||
# deliberate: B put the carriageway ON TOP at junctions (ROAD_Y 0 → 0.04) so roads run through
|
||
# intersections, and what a player sees under a poster is the top surface.
|
||
_KERB_JS = r"""
|
||
() => {
|
||
const P = window.PROCITY, T = P.THREE, skins = P.skins;
|
||
// the four merged ground classes, identified by the cached material instance itself
|
||
const CLASSES = ['djsim-road', 'djsim-footpath', 'grass', 'brickpave'];
|
||
const matOf = new Map(CLASSES.map(n => [skins.groundMat(n), n]));
|
||
const meshes = [];
|
||
P.scene.traverse(o => { if (o.isMesh && matOf.has(o.material)) meshes.push(o); });
|
||
const classify = (x, z) => {
|
||
const rc = new T.Raycaster(new T.Vector3(x, 40, z), new T.Vector3(0, -1, 0), 0, 200);
|
||
const hits = rc.intersectObjects(meshes, false);
|
||
if (!hits.length) return { cls: 'none', y: null };
|
||
let top = hits[0];
|
||
for (const h of hits) if (h.point.y > top.point.y) top = h;
|
||
return { cls: matOf.get(top.object.material), y: +top.point.y.toFixed(3) };
|
||
};
|
||
// ── the rendered posters: read the real InstancedMesh matrices, not the plan array ──
|
||
const rendered = [];
|
||
const m4 = new T.Matrix4(), v3 = new T.Vector3();
|
||
P.scene.traverse(o => {
|
||
if (o.isInstancedMesh && o.name === 'venue-posters') {
|
||
for (let i = 0; i < o.count; i++) {
|
||
o.getMatrixAt(i, m4); v3.setFromMatrixPosition(m4); o.localToWorld(v3);
|
||
rendered.push([+v3.x.toFixed(3), +v3.z.toFixed(3)]);
|
||
}
|
||
}
|
||
});
|
||
const planned = (P.plan.posters || []).map(q => [+q.x.toFixed(3), +q.z.toFixed(3)]);
|
||
// rendered ↔ planned agreement (licenses using plan ids for the per-poster report)
|
||
let matched = 0, worstDelta = 0;
|
||
for (const r of rendered) {
|
||
let bd = 1e18;
|
||
for (const q of planned) { const d = Math.hypot(r[0] - q[0], r[1] - q[1]); if (d < bd) bd = d; }
|
||
if (bd <= 0.01) matched++;
|
||
if (bd > worstDelta) worstDelta = bd;
|
||
}
|
||
// ── the clearance measurement ──
|
||
const per = rendered.map(([x, z]) => ({ x, z, ...classify(x, z) }));
|
||
const onRoad = per.filter(q => q.cls === 'djsim-road');
|
||
// ── DETECTOR CONTROLS: the classifier must be able to SAY 'road' and to SAY 'footpath' on this
|
||
// boot, or "0 posters on road" is a sentence about nothing. Sample the centreline of the
|
||
// longest main edge (must be road) and a point just outboard of its kerb (must be footpath).
|
||
const nodes = new Map(P.plan.streets.nodes.map(n => [n.id, n]));
|
||
let best = null, bestL = -1;
|
||
for (const e of P.plan.streets.edges) {
|
||
if (e.kind !== 'main') continue;
|
||
const a = nodes.get(e.a), b = nodes.get(e.b); if (!a || !b) continue;
|
||
const L = Math.hypot(b.x - a.x, b.z - a.z);
|
||
if (L > bestL) { bestL = L; best = { e, a, b }; }
|
||
}
|
||
let centre = null, band = null, outboard = null, edgeInfo = null;
|
||
if (best) {
|
||
const { e, a, b } = best;
|
||
const mx = (a.x + b.x) / 2, mz = (a.z + b.z) / 2;
|
||
const dx = (b.x - a.x) / bestL, dz = (b.z - a.z) / bestL;
|
||
const px = -dz, pz = dx; // perpendicular
|
||
centre = { at: [+mx.toFixed(2), +mz.toFixed(2)], ...classify(mx, mz) };
|
||
const inner = (P.plan.__vb || null); // not published; derive from the edge
|
||
const half = e.width / 2;
|
||
// 1 m inboard of the corridor edge — under the RULING this is footpath, pre-ruling it is road
|
||
const ox = mx + px * (half - 1), oz = mz + pz * (half - 1);
|
||
outboard = { at: [+ox.toFixed(2), +oz.toFixed(2)], offset: +(half - 1).toFixed(2), ...classify(ox, oz) };
|
||
edgeInfo = { kind: e.kind, width: e.width, len: +bestL.toFixed(1) };
|
||
}
|
||
const tally = {};
|
||
for (const q of per) tally[q.cls] = (tally[q.cls] || 0) + 1;
|
||
return { posters: rendered.length, planPosters: planned.length, matched, worstDelta: +worstDelta.toFixed(4),
|
||
groundMeshes: meshes.length, tally, onRoad: onRoad.length,
|
||
onRoadSample: onRoad.slice(0, 4).map(q => ({ x: q.x, z: q.z, y: q.y })),
|
||
centre, outboard, edgeInfo };
|
||
}
|
||
"""
|
||
|
||
|
||
def _kerb_measure(p, query, label):
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, query)
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
pg.wait_for_timeout(400)
|
||
r = pg.evaluate(_KERB_JS)
|
||
r['errs'] = errs
|
||
r['label'] = label
|
||
return r
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke_kerb(p):
|
||
"""[R37 item 0.1, Lane F half] THE KERB IS FOOTPATH — the runtime gate, and its control.
|
||
|
||
Measured on the PAINT, not on the plan: raycast straight down at every rendered poster and
|
||
classify the topmost ground hit by the cached `skins.groundMat()` material instance itself.
|
||
Lane A's corrected selfcheck asks the plan-side question and pins the pre-ruling failure at
|
||
exactly 14; this asks whether the player is looking at bitumen under the poster, which is a
|
||
number that comes from Lane B's geometry and from nowhere else.
|
||
|
||
Arms:
|
||
1. SUBJECT — posters exist on the default boot, and the RENDERED InstancedMesh matrices agree
|
||
with `plan.posters` to 1 cm (nobody had ever asserted that the poster you see is the poster
|
||
A placed). Zero posters ⇒ FAIL, never a skip.
|
||
2. DETECTOR CONTROLS — the classifier must be able to return 'road' AND 'footpath' on this
|
||
boot: the centreline of the longest main edge must classify as road, and a point 1 m inboard
|
||
of the corridor edge must classify as footpath. Without this pair, "0 posters on road" could
|
||
be true because the detector never says road.
|
||
3. THE RULING — 0 posters stand on road paint on the default boot.
|
||
4. THE CONTROL — the SAME leg under `?verge=0` must FAIL, at exactly 14 (Lane A's pinned
|
||
pre-ruling count, and B's flag reverts to byte-identical pre-ruling vertices). Keyed to the
|
||
FLAG, not to a line number, exactly as A keyed its own control.
|
||
5. CLASSIC KEEPS ITS PAINT (Fable's mid-round ruling) — under `?classic=1` the same outboard
|
||
sample must still classify as ROAD, i.e. the covenanted town's render did not move."""
|
||
head('SMOKE: the kerb (R37 0.1 — poster clearance measured on the paint · ?verge=0 is the control)')
|
||
|
||
d = _kerb_measure(p, '', 'default')
|
||
v = _kerb_measure(p, 'verge=0', '?verge=0')
|
||
|
||
# ── arm 1: the subject, and rendered == planned ───────────────────────────────────────────
|
||
if not d['posters']:
|
||
FAIL('kerb: ZERO rendered posters on the default boot — the gate has no subject and FAILS '
|
||
'(it does not skip). Expected the gig layer default-on since R16.')
|
||
return
|
||
if d['posters'] == d['planPosters'] and d['matched'] == d['posters']:
|
||
OK(f"subject: {d['posters']} posters rendered == {d['planPosters']} in plan.posters, every one "
|
||
f"within {d['worstDelta']} m of its planned position (the poster you see IS the poster A placed)")
|
||
else:
|
||
FAIL(f"kerb: rendered posters do not match the plan — {d['posters']} rendered / "
|
||
f"{d['planPosters']} planned / {d['matched']} matched, worst delta {d['worstDelta']} m")
|
||
|
||
# ── arm 2: the detector can say both words ────────────────────────────────────────────────
|
||
c, o, ei = d.get('centre'), d.get('outboard'), d.get('edgeInfo')
|
||
if not c or not o:
|
||
FAIL('kerb: no main edge to calibrate the surface detector against')
|
||
else:
|
||
if c['cls'] == 'djsim-road':
|
||
OK(f"detector control A: the centreline of the longest main ({ei['kind']}@{ei['width']} m, "
|
||
f"{ei['len']} m) classifies as ROAD at y={c['y']} — the detector can see bitumen")
|
||
else:
|
||
FAIL(f"detector control A: a main-edge CENTRELINE classifies as '{c['cls']}', not road — "
|
||
f"the surface detector is not measuring what it thinks ({c})")
|
||
if o['cls'] == 'djsim-footpath':
|
||
OK(f"detector control B: {o['offset']} m off that centreline (1 m inboard of the corridor "
|
||
f"edge) classifies as FOOTPATH at y={o['y']} — THE RULING IS IN THE PAINT")
|
||
else:
|
||
FAIL(f"detector control B: {o['offset']} m off the main centreline classifies as "
|
||
f"'{o['cls']}', expected footpath — the kerb ruling is not in the geometry ({o})")
|
||
|
||
# ── arm 3: the ruling ─────────────────────────────────────────────────────────────────────
|
||
if d['onRoad'] == 0:
|
||
OK(f"THE RULING HOLDS: 0 of {d['posters']} posters stand on road paint on the default boot "
|
||
f"(surface tally {d['tally']})")
|
||
else:
|
||
FAIL(f"KERB RULING BREACHED: {d['onRoad']} of {d['posters']} posters stand on road paint on the "
|
||
f"default boot — {d['onRoadSample']} (tally {d['tally']})")
|
||
|
||
# ── arm 4: THE CONTROL — the same leg, under ?verge=0, must go red at exactly 14 ───────────
|
||
if v.get('posters') != d['posters']:
|
||
FAIL(f"kerb control: ?verge=0 changed the POSTER population ({v.get('posters')} vs {d['posters']}) "
|
||
f"— it is a render flag and must not touch the plan; the control is not comparable")
|
||
elif v['onRoad'] == 0:
|
||
FAIL('kerb CONTROL FAILED: ?verge=0 reverts to the pre-ruling street and the clearance leg '
|
||
'still passes there — the leg is blind to where the road is painted, which is the exact '
|
||
'vacuous species this item was opened to retire')
|
||
elif v['onRoad'] == 14:
|
||
OK(f"CONTROL: the same leg FAILS under ?verge=0 at EXACTLY 14 posters on road paint — Lane A's "
|
||
f"pinned pre-ruling count, reproduced at runtime off Lane B's geometry (tally {v['tally']})")
|
||
else:
|
||
WARN(f"kerb control goes red under ?verge=0 as required, but at {v['onRoad']} posters, not the "
|
||
f"pinned 14 (A: selfcheck, plan-side; F: raycast, paint-side). Both are non-zero so the leg "
|
||
f"is falsifiable; the count needs reconciling — sample {v['onRoadSample']}, tally {v['tally']}")
|
||
|
||
# ── arm 5: CLASSIC KEEPS ITS PAINT (Fable's mid-round ruling) ─────────────────────────────
|
||
cl = _kerb_measure(p, 'classic=1', '?classic=1')
|
||
co = cl.get('outboard')
|
||
if not co:
|
||
FAIL('kerb/classic: no main edge to sample')
|
||
elif co['cls'] == 'djsim-road':
|
||
OK(f"CLASSIC KEEPS ITS PAINT: {co['offset']} m off the main centreline is still ROAD under "
|
||
f"?classic=1 (footpath on the default boot at the same point) — the covenanted render did "
|
||
f"not move on a render-side ruling")
|
||
else:
|
||
FAIL(f"?classic=1 ADOPTED the kerb ruling: {co['offset']} m off the centreline classifies as "
|
||
f"'{co['cls']}', expected road. Fable ruled classic is FENCED, not amended ({co})")
|
||
if cl['posters']:
|
||
FAIL(f"?classic=1 rendered {cl['posters']} posters — the gig layer leaked into the covenant")
|
||
|
||
for r in (d, v, cl):
|
||
if r.get('errs'):
|
||
FAIL(f"kerb/{r['label']}: {len(r['errs'])} console error(s); first: {r['errs'][0][:140]}")
|
||
|
||
|
||
def smoke_audio(p):
|
||
"""R11 audio house-law (Lane F): silent-and-happy, nothing plays pre-gesture, ?mute=1 silences,
|
||
?noassets=1 fetches zero audio, and interior beds play + release AudioNodes across enter/exit.
|
||
Each arm uses a fresh Chromium context (standing law — a reused tab's module cache burned us in R10)."""
|
||
head('SMOKE: audio (R11 — silent-happy · pre-gesture silence · mute · noassets · leak)')
|
||
|
||
# 1) boot-with-audio: 0 console errors, and nothing plays before a gesture (ctx suspended → state.ready false)
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
pg.wait_for_function("() => window.PROCITY && window.PROCITY.audio && window.PROCITY.audio.state", timeout=8000)
|
||
st = pg.evaluate("() => { const s=window.PROCITY.audio.state; return {ready:!!s.ready, muted:!!s.muted, mode:s.mode}; }")
|
||
if not st['ready']:
|
||
OK(f"pre-gesture: audio NOT unlocked (state.ready=false, mode={st['mode']}) — silent until first gesture")
|
||
else:
|
||
FAIL(f"audio unlocked before any user gesture (autoplay-policy violation): {st}")
|
||
if errs: FAIL(f"audio boot: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK("audio boot: 0 console errors (silent-and-happy)")
|
||
finally:
|
||
b.close()
|
||
|
||
# 2) ?mute=1 → engine reports muted
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'mute=1')
|
||
pg.wait_for_function("() => window.PROCITY && window.PROCITY.audio", timeout=8000)
|
||
st = pg.evaluate("() => ({muted:!!window.PROCITY.audio.state.muted, mode:window.PROCITY.audio.state.mode})")
|
||
if st['muted']: OK(f"?mute=1: engine muted (mode={st['mode']})")
|
||
else: FAIL(f"?mute=1 not honored: {st}")
|
||
if errs: FAIL(f"?mute=1: {len(errs)} console error(s)")
|
||
finally:
|
||
b.close()
|
||
|
||
# 3) ?noassets=1 → zero audio network fetches (house law: the noassets gate implies no audio)
|
||
b, pg, errs = new_page(p)
|
||
audio_reqs = []
|
||
pg.on('request', lambda r: audio_reqs.append(r.url)
|
||
if ('/assets/audio/' in r.url or r.url.endswith('.ogg') or r.url.endswith('.m4a')) else None)
|
||
try:
|
||
boot(pg, 'noassets=1')
|
||
pg.keyboard.press('Space') # a keyboard gesture unlocks the AudioContext WITHOUT a pointer-lock attempt (headless-safe) # a gesture, so the audio path is genuinely exercised
|
||
pg.evaluate("""() => { const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const s=(P.plan.shops||[]).find(x=>P.isOpen(x)); if(s) D.enterShop(s.id); }""")
|
||
pg.wait_for_timeout(800)
|
||
man = pg.evaluate("() => window.PROCITY.audio.state.manifest")
|
||
if len(audio_reqs) == 0: OK(f"?noassets=1: 0 audio fetches (manifest={man})")
|
||
else: FAIL(f"?noassets=1: {len(audio_reqs)} audio fetch(es) leaked; first {audio_reqs[0]}")
|
||
if errs: FAIL(f"?noassets=1 audio: {len(errs)} console error(s)")
|
||
finally:
|
||
b.close()
|
||
|
||
# 4) interior beds play + enter/exit is AudioNode-leak-free (live BufferSources stay bounded, not O(N))
|
||
b = p.chromium.launch()
|
||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||
errs = []
|
||
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
|
||
pg.on('pageerror', lambda e: errs.append(str(e)))
|
||
pg.add_init_script("""
|
||
window.__live = new Set(); window.__made = 0;
|
||
const AC = window.AudioContext || window.webkitAudioContext;
|
||
if (AC) { const orig = AC.prototype.createBufferSource;
|
||
AC.prototype.createBufferSource = function () { const s = orig.call(this);
|
||
window.__made++; window.__live.add(s);
|
||
const ostop = s.stop.bind(s); s.stop = function (w) { window.__live.delete(s); return ostop(w); };
|
||
s.addEventListener('ended', () => window.__live.delete(s)); return s; }; }
|
||
""")
|
||
try:
|
||
boot(pg, '')
|
||
pg.keyboard.press('Space') # a keyboard gesture unlocks the AudioContext WITHOUT a pointer-lock attempt (headless-safe) # user gesture → unlock the AudioContext
|
||
pg.wait_for_timeout(250)
|
||
played = pg.evaluate("""async () => {
|
||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||
const types=['record','milkbar','video','opshop','book','toy'];
|
||
let entered=0, withMusic=0;
|
||
for (let rep=0; rep<2; rep++) for (const t of types) {
|
||
const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x)); if(!s) continue;
|
||
D.enterShop(s.id); entered++;
|
||
const a = P.interiorMode.current && P.interiorMode.current.audio;
|
||
if (a && a.musicKey) withMusic++;
|
||
await new Promise(r=>setTimeout(r,120));
|
||
D.exitShop && D.exitShop(); await new Promise(r=>setTimeout(r,120));
|
||
}
|
||
return { entered, withMusic, ready:!!P.audio.state.ready };
|
||
}""")
|
||
pg.wait_for_timeout(600)
|
||
live = pg.evaluate("() => window.__live.size")
|
||
made = pg.evaluate("() => window.__made")
|
||
if not played['ready']:
|
||
WARN(f"audio interior: ctx never unlocked in headless — leak check inconclusive ({played}, live {live}/made {made})")
|
||
elif played['entered'] >= 4 and live <= 12:
|
||
OK(f"audio interior: {played['entered']} enter/exit, {played['withMusic']} with a music bed; live AudioNodes {live} bounded (made {made}) — leak-free")
|
||
else:
|
||
FAIL(f"audio interior: {live} live AudioNodes after {played['entered']} enter/exit (made {made}) — possible leak")
|
||
if errs: FAIL(f"audio interior: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK("audio interior: 0 console errors across enter/exit cycles")
|
||
finally:
|
||
b.close()
|
||
|
||
def smoke_gigs(p):
|
||
"""R13 v3.0-beta THE DISTRICT gig layer (Lane F). The ROUND13 §Lane F.6 gates — district determinism ·
|
||
a 4-piece band · per-venue crowd ≤ watchPoints · EVERY venue's gigKey resolves to a real manifest bed
|
||
(debt #1, now across 3 genres) · gig-night interior draw budget · street budget at the busiest venue
|
||
block · no-giants (STATURE) on the stage/crowd rigs · queue ≤ cap and drains by 'done' (pending Lane D)
|
||
· ?noassets=1 still silent-and-fine with ?gigs=1 — plus John's cover ruling, now PER VENUE (paid · free
|
||
· broke-knockback · no re-charge on re-entry · a night at two venues is two covers).
|
||
|
||
Seeds are pinned because `cover` is seeded: 20261990's tonight is a $10 night (Screaming Utes at The
|
||
Thornbury Hotel) and 1234's is free (Dazza & The Dropbears at The Imperial), so BOTH halves of "free
|
||
AND paid" are covered deterministically rather than whichever the default seed happened to roll."""
|
||
head('SMOKE: gigs (R13 v3.0-beta district — determinism · 4-piece · per-venue crowd/cover · gigKeys · budgets · queue · noassets)')
|
||
if not gigs_landed(p):
|
||
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
|
||
return
|
||
|
||
# 1) schedule determinism — two independent boots (fresh contexts) → byte-equal gigs + posters
|
||
sigs = []
|
||
for _ in range(2):
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
sigs.append(pg.evaluate("() => JSON.stringify({g: window.PROCITY.plan.gigs, p: window.PROCITY.plan.posters})"))
|
||
if errs: FAIL(f"gigs boot: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
if sigs[0] == sigs[1]:
|
||
n = len(json.loads(sigs[0])['g'])
|
||
OK(f"gig schedule deterministic: two runs byte-equal ({n} nights + posters)")
|
||
else:
|
||
FAIL('gig schedule NOT deterministic across two runs (byte-diff in plan.gigs/plan.posters)')
|
||
|
||
# 2) the gig night itself: band · crowd · dance mix · budget · the live bed · no giants
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
pg.keyboard.press('Space') # gesture → AudioContext unlock (headless-safe, no pointer-lock)
|
||
night = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3();
|
||
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
|
||
D.setSegment(5); // NIGHT — the gig is on
|
||
const state = P.gigs.stateOf(id);
|
||
D.enterShop(id);
|
||
await new Promise(r => setTimeout(r, 500));
|
||
const room = P.interiorMode.current;
|
||
if (!room) return { entered: false, state };
|
||
const crew = P.interiorMode.crew, info = P.interiorMode.crewInfo;
|
||
const mem = crew ? crew.members : [];
|
||
// STATURE, not world crown: the band stands on Lane C's stage deck (deckY ~0.32 m), so a
|
||
// perfectly human 1.73 m guitarist has a world crown of 2.05 m and a naive [1.4,2.0] world-Y
|
||
// check fails a correct band (it did, on the first R12 verify run). Measure crown MINUS the
|
||
// figure's own base y — that is what "human-sized" actually means — and keep the separate
|
||
// "never clips the ceiling" assert on the world crown, which is the real R10 failure mode.
|
||
const figs = mem.map(m => {
|
||
let crown = 0;
|
||
m.actor.fig.updateWorldMatrix(true, true);
|
||
(m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) {
|
||
o.getWorldPosition(v); crown = Math.max(crown, v.y); } });
|
||
return { part: m.part, kind: m.kind, dance: !!m.dance,
|
||
crown: +crown.toFixed(3), stature: +(crown - (m.base.y || 0)).toFixed(3),
|
||
seated: !!(m.actor.fig.userData && m.actor.fig.userData.procitySeated) }; // [R16 #3]
|
||
});
|
||
// Force one clean interior frame rather than trusting whatever the last rAF left in
|
||
// renderer.info — a throttled/backgrounded tab otherwise reports the last STREET frame
|
||
// (composer + bloom), which reads as a wildly over-budget room. Bit us on the first verify.
|
||
P.renderer.info.reset();
|
||
P.renderer.render(P.interiorMode.scene, P.camera);
|
||
return {
|
||
entered: true, state, H: +room.dims.H.toFixed(2),
|
||
watchPoints: room.watchPoints.length, stage: !!room.stage,
|
||
band: info ? info.band : 0, crowd: info ? info.crowd : 0,
|
||
dancers: mem.filter(m => m.part === 'crowd' && m.dance).length,
|
||
gigKey: room.audio.gigKey || null, musicKey: room.audio.musicKey || null,
|
||
draws: P.renderer.info.render.calls, tris: P.renderer.info.render.triangles, figs,
|
||
};
|
||
}""")
|
||
if not night.get('entered'):
|
||
FAIL(f"gig night: could not enter the venue at NIGHT ({night})")
|
||
else:
|
||
if night['state'] == 'on': OK(f"gig state machine: NIGHT → 'on' (venue {night['H']}m room)")
|
||
else: FAIL(f"gig state machine: NIGHT should be 'on', got '{night['state']}'")
|
||
# band is a 4-piece (R13 debt #3: front-line trio + a drummer on C's riser)
|
||
if night['band'] == 4: OK('band: 4-piece on the stage (front-line trio + drummer)')
|
||
else: FAIL(f"band: expected a 4-piece (trio + drummer), got {night['band']}")
|
||
# crowd cap ≤ watchPoints
|
||
if night['crowd'] <= night['watchPoints'] and night['crowd'] > 0:
|
||
OK(f"crowd: {night['crowd']} at {night['watchPoints']} watch points (≤ cap)")
|
||
else:
|
||
FAIL(f"crowd {night['crowd']} vs {night['watchPoints']} watch points (must be >0 and ≤ cap)")
|
||
# John's "a bit of both": the crowd must not be all-standing or all-dancing
|
||
d, c = night['dancers'], night['crowd']
|
||
if c and 0 < d < c: OK(f"crowd mix: {d} dancing / {c - d} standing — 'a bit of both'")
|
||
else: FAIL(f"crowd is not mixed: {d} dancing of {c} (John's ruling: some of both)")
|
||
# the live bed actually resolves (the C↔E gigKey seam F bridges — see LANE_F_NOTES §12)
|
||
if night['gigKey']: OK(f"live bed: room.audio.gigKey = {night['gigKey']} (F resolves → manifest key)")
|
||
else: FAIL('live bed: no room.audio.gigKey on a gig-on venue (Lane C opts.gig seam)')
|
||
# gig-night interior draw budget (ROUND12: ≤350 incl. the crowd)
|
||
if night['draws'] <= GIG_DRAWS_MAX:
|
||
OK(f"gig-night interior budget: {night['draws']} draws ≤ {GIG_DRAWS_MAX} (tris {night['tris']:,})")
|
||
else:
|
||
FAIL(f"gig-night interior budget: {night['draws']} draws > {GIG_DRAWS_MAX} (tris {night['tris']:,})")
|
||
# no giants on the stage/crowd rigs (R10 gate, inherited by the gig path)
|
||
rigs = [f for f in night['figs'] if f['kind'] == 'rig']
|
||
bad = [f for f in rigs if not stature_ok(f) or f['crown'] >= night['H']] # [R16 #3] seated exemption
|
||
if not rigs:
|
||
WARN(f"no-giants: crew spawned as placeholders (no fleet) — rig check n/a ({len(night['figs'])} figs)")
|
||
elif bad:
|
||
FAIL(f"no-giants: {len(bad)} gig rig(s) out of [1.4,2.0]m stature or through the {night['H']}m ceiling: {bad[:3]}")
|
||
else:
|
||
lo = min(f['stature'] for f in rigs); hi = max(f['stature'] for f in rigs)
|
||
hc = max(f['crown'] for f in rigs)
|
||
OK(f"no-giants: {len(rigs)} band+crowd rigs human-sized ({lo:.2f}–{hi:.2f} m stature; "
|
||
f"tallest crown {hc:.2f} m under the {night['H']} m ceiling)")
|
||
if errs: FAIL(f"gig night: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('gig night: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
# 2b) THE DISTRICT (R13): every playing venue fires — its live bed resolves to a REAL manifest bar for
|
||
# its genre (debt #1, now across pub/band_room/rsl), its crowd is capped at its OWN watch points, and it
|
||
# is a 4-piece. Plus the queue ≤ cap / drains-by-done probe (WARN-skips until Lane D wires the seam).
|
||
music_keys = set()
|
||
try:
|
||
mani = json.loads((ROOT / 'web' / 'assets' / 'manifest.json').read_text())
|
||
music_keys = set((mani.get('audio', {}).get('music', {}) or {}).keys())
|
||
except Exception as e:
|
||
WARN(f"district: could not read manifest music keys ({e}) — gigKey-resolves check is JS-only")
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
pg.keyboard.press('Space')
|
||
district = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3();
|
||
D.setSegment(5); // NIGHT — every playing venue is 'on'
|
||
const ids = P.gigs.venueShopIds || [];
|
||
const out = [];
|
||
for (const id of ids) {
|
||
const shop = (P.plan.shops || []).find(s => s.id === id);
|
||
if (!P.gigs.onOf(id)) { out.push({ id, kind: shop && shop.venueKind, dark: true }); continue; }
|
||
D.enterShop(id);
|
||
await new Promise(r => setTimeout(r, 450));
|
||
const room = P.interiorMode.current, info = P.interiorMode.crewInfo;
|
||
const mem = P.interiorMode.crew ? P.interiorMode.crew.members : [];
|
||
const figs = mem.map(m => {
|
||
let crown = 0; m.actor.fig.updateWorldMatrix(true, true);
|
||
(m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) {
|
||
o.getWorldPosition(v); crown = Math.max(crown, v.y); } });
|
||
return { part: m.part, kind: m.kind, stature: +(crown - (m.base.y || 0)).toFixed(3),
|
||
seated: !!(m.actor.fig.userData && m.actor.fig.userData.procitySeated) }; // [R16 #3]
|
||
});
|
||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||
out.push({ id, kind: shop && shop.venueKind, genreKey: shop && shop.genreKey,
|
||
gigKey: (room && room.audio && room.audio.gigKey) || null,
|
||
band: info ? info.band : 0, crowd: info ? info.crowd : 0,
|
||
watchPoints: room ? room.watchPoints.length : 0, figs });
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 200));
|
||
}
|
||
return { ids, out };
|
||
}""")
|
||
playing = [d for d in district['out'] if not d.get('dark')]
|
||
OK(f"district: {len(district['ids'])} venue(s) — {len(playing)} playing tonight, "
|
||
f"{len(district['out']) - len(playing)} dark (reads true to the seeded week)")
|
||
genres_seen = set()
|
||
for d in playing:
|
||
tag = f"{d['kind']} #{d['id']}"
|
||
want = f"gig-{d['genreKey']}"
|
||
if d['gigKey'] == want and (not music_keys or want in music_keys):
|
||
OK(f" {tag}: live bed '{d['gigKey']}' resolves to a real manifest bar (genre {d['genreKey']})")
|
||
genres_seen.add(d['genreKey'])
|
||
else:
|
||
FAIL(f" {tag}: gigKey '{d['gigKey']}' != '{want}' or not in manifest.music {sorted(music_keys)}")
|
||
if 0 < d['crowd'] <= d['watchPoints']:
|
||
OK(f" {tag}: crowd {d['crowd']} ≤ {d['watchPoints']} watch points")
|
||
else:
|
||
FAIL(f" {tag}: crowd {d['crowd']} vs {d['watchPoints']} watch points (must be >0 and ≤ cap)")
|
||
if d['band'] == 4: OK(f" {tag}: 4-piece band")
|
||
else: FAIL(f" {tag}: expected a 4-piece, got {d['band']}")
|
||
rigs = [f for f in d['figs'] if f['kind'] == 'rig']
|
||
bad = [f for f in rigs if not stature_ok(f)] # [R16 #3] seated exemption
|
||
if rigs and bad: FAIL(f" {tag}: {len(bad)} giant rig(s) by stature: {bad[:2]}")
|
||
if len(genres_seen) >= 2:
|
||
OK(f"district: {len(genres_seen)} distinct genres verified live ({', '.join(sorted(genres_seen))})")
|
||
|
||
# queue ≤ cap and drains by 'done' — Lane D's outdoor line (charter item). D shipped the VenueQueue
|
||
# class and left the street-side lifecycle to F, which wires it in the shell (spawn at doors, drive
|
||
# each frame, disposeAll at done); read via P.queueCountOf(id). The line is a seeded short 2–6.
|
||
q = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
if (typeof P.queueCountOf !== 'function') return { wired: false };
|
||
const id = P.gigs.venueShopIds[0];
|
||
D.setSegment(4); await new Promise(r => setTimeout(r, 700)); // DUSK — doors open, the line forms
|
||
const atDoors = P.queueCountOf(id) | 0;
|
||
D.setSegment(0); await new Promise(r => setTimeout(r, 700)); // DAWN — closing time ('done'), line gone
|
||
const atDone = P.queueCountOf(id) | 0;
|
||
return { wired: true, atDoors, atDone };
|
||
}""")
|
||
QUEUE_CAP = 6
|
||
if not q.get('wired'):
|
||
FAIL("queue: P.queueCountOf(id) not exposed — F's shell wiring missing")
|
||
elif 0 < q['atDoors'] <= QUEUE_CAP and q['atDone'] == 0:
|
||
OK(f"queue: {q['atDoors']} punters in line at doors (≤ cap {QUEUE_CAP}), drained to 0 by 'done'")
|
||
else:
|
||
FAIL(f"queue: at doors {q['atDoors']} (want 1..{QUEUE_CAP}), at done {q['atDone']} (want 0)")
|
||
|
||
if errs: FAIL(f"district: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('district: 0 console errors across all venues')
|
||
finally:
|
||
b.close()
|
||
|
||
# 2c) STREET BUDGET at the busiest venue block (R13 law: ≤300 draws / ≤200k tris with the lit frontage,
|
||
# town-wide posters and the queue zone all in). Park on the footpath in front of the primary venue at
|
||
# NIGHT with the gig on, let the block stream in, and sample the settled composer frame.
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
st = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
D.setSegment(5); // NIGHT — frontage lit, posters up
|
||
const id = P.gigs.venueShopIds[0], shop = (P.plan.shops || []).find(s => s.id === id);
|
||
const lot = shop && (P.plan.lots || []).find(l => l.id === shop.lot);
|
||
if (lot) {
|
||
const fx = Math.sin(lot.ry || 0), fz = Math.cos(lot.ry || 0);
|
||
D.teleport(lot.x + fx * 10, lot.z + fz * 10, Math.atan2(-fx, -fz)); // 10 m out, facing the venue
|
||
}
|
||
await new Promise(r => setTimeout(r, 700)); // chunks/frontage/posters/pools settle
|
||
let draws = Infinity, tris = 0; // take the settled floor (ignore stream spikes)
|
||
for (let i = 0; i < 12; i++) {
|
||
await new Promise(r => requestAnimationFrame(r));
|
||
if (P.renderer.info.render.calls < draws) { draws = P.renderer.info.render.calls; tris = P.renderer.info.render.triangles; }
|
||
}
|
||
return { draws, tris, venue: shop && shop.name };
|
||
}""")
|
||
if st['draws'] <= STREET_DRAWS_MAX and st['tris'] <= STREET_TRIS_MAX:
|
||
OK(f"street budget @ venue block ({st['venue']}): {st['draws']} draws ≤ {STREET_DRAWS_MAX}, {st['tris']:,} tris ≤ {STREET_TRIS_MAX:,}")
|
||
else:
|
||
FAIL(f"street budget @ venue block ({st['venue']}): {st['draws']} draws / {st['tris']:,} tris over ({STREET_DRAWS_MAX} / {STREET_TRIS_MAX:,})")
|
||
if errs: FAIL(f"street budget: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# 3) cover ruling, PAID night (seed 20261990 → $10): debit once · no re-charge on re-entry · broke = knockback
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
paid = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
|
||
D.setSegment(5);
|
||
const cover = P.gigs.coverOf(id);
|
||
if (!cover) return { skip: true, cover };
|
||
const before = P.wallet.cash();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
|
||
const inRoom1 = !!P.interiorMode.current, afterPay = P.wallet.cash();
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
const mid = P.wallet.cash();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
|
||
const inRoom2 = !!P.interiorMode.current, afterReentry = P.wallet.cash();
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
// now go broke and try again on the SAME night (stamp already paid → still free), then roll the
|
||
// night over (MORNING clears the stamp) and try broke → must be knocked back at the door.
|
||
P.wallet.buy({ title: 'qa: drain', price: P.wallet.cash() });
|
||
D.setSegment(1); D.setSegment(5); // NIGHT → MORNING → NIGHT = a new night, cover due
|
||
const brokeCash = P.wallet.cash();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
|
||
const inRoomBroke = !!P.interiorMode.current;
|
||
return { cover, before, afterPay, inRoom1, mid, afterReentry, inRoom2, brokeCash, inRoomBroke,
|
||
inv: P.wallet.inventory().map(i => i.title) };
|
||
}""")
|
||
if paid.get('skip'):
|
||
WARN(f"cover: seed {SEED} tonight is free (cover 0) — paid path not exercised here")
|
||
else:
|
||
if paid['inRoom1'] and paid['afterPay'] == paid['before'] - paid['cover']:
|
||
OK(f"cover: ${paid['cover']} debited once at the door (${paid['before']} → ${paid['afterPay']}), entry granted")
|
||
else:
|
||
FAIL(f"cover: wrong debit/entry on a paid night: {paid}")
|
||
if paid['inRoom2'] and paid['afterReentry'] == paid['mid']:
|
||
OK(f"cover: re-entry the same night is FREE (no re-charge, still ${paid['afterReentry']})")
|
||
else:
|
||
FAIL(f"cover: re-entry re-charged or was refused: {paid}")
|
||
if not paid['inRoomBroke'] and paid['brokeCash'] < paid['cover']:
|
||
OK(f"cover: broke (${paid['brokeCash']}) on a NEW night → polite knockback, no entry")
|
||
else:
|
||
FAIL(f"cover: broke player got in (or the night never rolled): {paid}")
|
||
if errs: FAIL(f"cover: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# 4) cover ruling, FREE night (seed 1234 → cover 0): walk straight in, wallet untouched
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
pg.goto(f'{HOST}/index.html?seed={FREE_GIG_SEED}&dbg=1')
|
||
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
|
||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||
free = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
|
||
D.setSegment(5);
|
||
const cover = P.gigs.coverOf(id), before = P.wallet.cash();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 350));
|
||
return { cover, before, after: P.wallet.cash(), inRoom: !!P.interiorMode.current, band: P.gigs.bandNameOf(id) };
|
||
}""")
|
||
if free['cover'] == 0 and free['inRoom'] and free['after'] == free['before']:
|
||
OK(f"cover: free night (seed {FREE_GIG_SEED}, {free['band']}) → walked straight in, wallet untouched (${free['after']})")
|
||
else:
|
||
FAIL(f"cover: free-night entry wrong: {free}")
|
||
if errs: FAIL(f"free night: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# 5) ?noassets=1 + ?gigs=1 — still silent-and-fine: zero audio/band-file fetches, gig still happens
|
||
b, pg, errs = new_page(p)
|
||
reqs = []
|
||
pg.on('request', lambda r: reqs.append(r.url)
|
||
if ('/assets/audio/' in r.url or r.url.endswith('.ogg') or r.url.endswith('.m4a')
|
||
or 'custom_bands' in r.url or r.url.endswith('.glb')) else None)
|
||
try:
|
||
boot(pg, 'noassets=1')
|
||
pg.keyboard.press('Space')
|
||
na = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id
|
||
D.setSegment(5);
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 450));
|
||
const info = P.interiorMode.crewInfo;
|
||
return { state: P.gigs.stateOf(id), inRoom: !!P.interiorMode.current, band: info && info.band,
|
||
crowd: info && info.crowd, gigs: (P.plan.gigs || []).length,
|
||
bandName: P.gigs.bandNameOf(id), audio: P.audio && P.audio.state.manifest };
|
||
}""")
|
||
if na['inRoom'] and na['band'] == 4 and na['crowd'] > 0:
|
||
OK(f"?noassets=1&gigs=1: the gig still happens (4-piece band, crowd {na['crowd']}, placeholders)")
|
||
else:
|
||
FAIL(f"?noassets=1&gigs=1: no gig without assets: {na}")
|
||
if len(reqs) == 0:
|
||
OK(f"?noassets=1&gigs=1: 0 audio/GLB/band-file fetches (manifest={na['audio']}, bands from the generator)")
|
||
else:
|
||
FAIL(f"?noassets=1&gigs=1: {len(reqs)} asset fetch(es) leaked; first {reqs[0]}")
|
||
if errs: FAIL(f"?noassets=1&gigs=1: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('?noassets=1&gigs=1: 0 console errors (silent-and-happy)')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
GODVERSE_TOWN = 'redhill_godverse'
|
||
GODVERSE_SHOP = 3962749 # Monster Robot Party, 147 Musgrave Rd, Red Hill QLD — the one REAL crate
|
||
GODVERSE_BASE = f'assets/stock_godverse/{GODVERSE_SHOP}/'
|
||
STOCK_MANIFEST = 'assets/stock_godverse/index.json'
|
||
|
||
|
||
def smoke_real_crate(p):
|
||
"""R26 — THE BETA GATE: every crate different, sourcing visible, and NOT ONE 404.
|
||
|
||
The lineage of this gate is the epoch's whole argument. R23's #7a would have gone green on "real
|
||
covers render" while the room held the generic v2 pack. R24's brief then said assert id equality —
|
||
which also couldn't discriminate, because both packs numbered items positionally and every atlas id
|
||
was a generic-pack id too. What survives is what a wrong pack cannot fake: TITLES only this crate
|
||
has, and the TEXTURE URL the GPU actually samples.
|
||
|
||
R26 adds the two beta claims. DISTINCTNESS: 15 keyed crates, pairwise title-disjoint — asserted
|
||
across the indexes of record, so it cannot pass by entering one lucky shop. SOURCING (C's §7.2b,
|
||
which correctly overruled the brief's `tier`): every index declares real|mint, a mint crate can
|
||
never report as real, and the gate PRINTS it.
|
||
|
||
And the 404s are gone for the right reason. R24 probed every keyed shop and ate a 404 for each one
|
||
with no atlas; F's smoke could only tolerate that by naming it — a tolerance is a confession. G's
|
||
manifest now says which (shop, type) pairs exist, so the shell asks for nothing else. ZERO console
|
||
errors, NO attribution exceptions. Subject absent ⇒ SKIP with the reason, never a quiet pass."""
|
||
head(f'SMOKE: the beta crate gate (R26 — 15 crates distinct · sourcing · zero 404s · {GODVERSE_TOWN})')
|
||
b, pg, errs = new_page(p)
|
||
four04 = []
|
||
pg.on('response', lambda r: four04.append(r.url) if r.status == 404 else None)
|
||
try:
|
||
pg.goto(f'{HOST}/{STOCK_MANIFEST}')
|
||
if 'godverseShopId' not in pg.content():
|
||
print('· SKIP: no atlas manifest — subject absent, nothing asserted (Lane G)'); return
|
||
boot(pg, f'plansrc=osm&town={GODVERSE_TOWN}&stock=real&dig=1')
|
||
res = pg.evaluate("""async ({manifest, real, base}) => {
|
||
const D = window.DBG, P = window.PROCITY;
|
||
const man = await (await fetch(manifest)).json();
|
||
|
||
// ── DISTINCTNESS across the indexes of record (not just the shop we walk into) ──
|
||
const crates = [];
|
||
for (const sh of (man.shops || [])) for (const t of (sh.types || [])) {
|
||
const u = `assets/stock_godverse/${sh.godverseShopId}/stock_${t}_index.json`;
|
||
const r = await fetch(u);
|
||
if (!r.ok) return { fail: `manifest lists ${u} but it 404s` };
|
||
const idx = await r.json();
|
||
crates.push({ id: sh.godverseShopId, type: t, manSourcing: sh.sourcing,
|
||
sourcing: idx.sourcing || null,
|
||
titles: (idx.items || []).map(i => i.title) });
|
||
}
|
||
const gen = {};
|
||
for (const t of ['record', 'book', 'toy'])
|
||
gen[t] = new Set((((await (await fetch(`assets/models/stock_${t}_index.json`)).json()).items) || [])
|
||
.map(i => i.title));
|
||
let shared = 0, vsGeneric = 0, noSourcing = 0, manMismatch = 0;
|
||
for (let i = 0; i < crates.length; i++) {
|
||
if (!crates[i].sourcing) noSourcing++;
|
||
if (crates[i].sourcing !== crates[i].manSourcing) manMismatch++;
|
||
if (crates[i].titles.some(t => gen[crates[i].type] && gen[crates[i].type].has(t))) vsGeneric++;
|
||
for (let j = i + 1; j < crates.length; j++) {
|
||
const a = new Set(crates[i].titles);
|
||
if (crates[j].titles.some(t => a.has(t))) shared++;
|
||
}
|
||
}
|
||
|
||
// ── the ROOM binds to the right crate: one REAL, one MINT, one soft fall ──
|
||
const byId = new Map((man.shops || []).map(s => [s.godverseShopId, s]));
|
||
const stocked = (P.plan.shops || []).filter(s => { const e = byId.get(s.godverseShopId);
|
||
return e && (e.types || []).includes(s.type); });
|
||
const unfetch = (P.plan.shops || []).find(s => { if (!s.godverseShopId) return false;
|
||
const e = byId.get(s.godverseShopId);
|
||
return !e || !(e.types || []).includes(s.type); });
|
||
const visit = async (shop) => {
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
D.enterShop('g:' + shop.godverseShopId); await new Promise(r => setTimeout(r, 1200));
|
||
const si = D.stockInfo();
|
||
const c = crates.find(c => c.id === shop.godverseShopId && c.type === shop.type);
|
||
const own = c ? new Set(c.titles) : new Set();
|
||
return { name: si.name, gid: si.godverseShopId, want: shop.godverseShopId, type: si.type,
|
||
base: si.base, packItems: si.packItems,
|
||
sourcing: c ? c.sourcing : null,
|
||
inOwn: si.packTitles.filter(t => own.has(t)).length,
|
||
fromOwnTex: si.texUrls.filter(u => u.includes(`stock_godverse/${shop.godverseShopId}/`)).length,
|
||
fromGenericTex: si.texUrls.filter(u => u.includes('assets/models/stock_')).length };
|
||
};
|
||
const realShop = stocked.find(s => s.godverseShopId === real);
|
||
const mintShop = stocked.find(s => (byId.get(s.godverseShopId) || {}).sourcing === 'mint');
|
||
const out = { crates: crates.length, shared, vsGeneric, noSourcing, manMismatch,
|
||
stocked: stocked.length,
|
||
realRoom: realShop ? await visit(realShop) : null,
|
||
mintRoom: mintShop ? await visit(mintShop) : null };
|
||
if (unfetch) { D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
D.enterShop('g:' + unfetch.godverseShopId); await new Promise(r => setTimeout(r, 900));
|
||
const o = D.stockInfo();
|
||
out.softFall = { name: o.name, gid: o.godverseShopId, want: unfetch.godverseShopId,
|
||
packItems: o.packItems }; }
|
||
return out;
|
||
}""", {'manifest': STOCK_MANIFEST, 'real': GODVERSE_SHOP, 'base': GODVERSE_BASE})
|
||
if res.get('fail'):
|
||
FAIL(f"beta crate gate: {res['fail']}"); return
|
||
|
||
# ── distinctness (independently measured; G reported 15/15) ──
|
||
pairs = res['crates'] * (res['crates'] - 1) // 2
|
||
OK(f"DISTINCTNESS: {res['crates']} keyed crates, {pairs} pairs, {res['shared']} sharing a title") \
|
||
if res['shared'] == 0 else FAIL(f"DISTINCTNESS: {res['shared']} crate pair(s) share titles")
|
||
OK(f"DISTINCTNESS: 0/{res['crates']} crates overlap the generic v2 pack") \
|
||
if res['vsGeneric'] == 0 else FAIL(f"{res['vsGeneric']} crate(s) overlap the generic pack")
|
||
# ── sourcing (C §7.2b — an index with no `sourcing` FAILS; the gate PRINTS it) ──
|
||
OK(f"SOURCING: {res['crates']}/{res['crates']} indexes declare sourcing") \
|
||
if res['noSourcing'] == 0 else FAIL(f"SOURCING: {res['noSourcing']} index(es) declare none")
|
||
OK('SOURCING: every index agrees with the manifest') \
|
||
if res['manMismatch'] == 0 else FAIL(f"SOURCING: {res['manMismatch']} index/manifest disagreement(s)")
|
||
|
||
for label, want in (('realRoom', 'real'), ('mintRoom', 'mint')):
|
||
r = res.get(label)
|
||
if not r:
|
||
FAIL(f"{label}: no {want} crate reachable in {GODVERSE_TOWN}"); continue
|
||
if r['gid'] != r['want']:
|
||
FAIL(f"{label}: entered the WRONG shop — wanted {r['want']}, got {r['gid']}"); continue
|
||
print(f" room: {r['name']} (godverse {r['gid']}, {r['type']}) → {r['base']} [sourcing: {r['sourcing']}]")
|
||
OK(f" {want}: sourcing reports '{r['sourcing']}'") if r['sourcing'] == want \
|
||
else FAIL(f" {want} crate reports sourcing '{r['sourcing']}' — a {want} crate must never report otherwise")
|
||
OK(f" {want}: TITLE equality {r['inOwn']}/{r['packItems']} from its OWN crate") \
|
||
if (r['packItems'] > 0 and r['inOwn'] == r['packItems']) \
|
||
else FAIL(f" {want}: only {r['inOwn']}/{r['packItems']} titles are its own")
|
||
OK(f" {want}: TEXTURE provenance {r['fromOwnTex']} own / {r['fromGenericTex']} generic") \
|
||
if (r['fromOwnTex'] > 0 and r['fromGenericTex'] == 0) \
|
||
else FAIL(f" {want}: textures {r['fromOwnTex']} own / {r['fromGenericTex']} generic — wrong file")
|
||
|
||
sf = res.get('softFall')
|
||
if sf:
|
||
if sf['gid'] != sf['want']: FAIL(f"soft fall: entered the wrong shop ({sf['gid']} != {sf['want']})")
|
||
elif sf['packItems'] == 0: OK(f"soft fall: keyed, unfetchable type \"{sf['name']}\" → parody, silently")
|
||
else: FAIL(f"soft fall BROKEN: \"{sf['name']}\" inherited {sf['packItems']} items")
|
||
|
||
# ── ZERO 404s. No exceptions. The manifest is what makes this honest. ──
|
||
OK('ZERO 404s — the manifest means we never ask for an atlas that is not there') \
|
||
if not four04 else FAIL(f"{len(four04)} unexpected 404(s) — the manifest should have prevented every one; first: {four04[0]}")
|
||
OK('0 console errors (no attribution exceptions)') if not errs \
|
||
else FAIL(f'the crate: {len(errs)} console error(s); first: {errs[0][:140]}')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
# ── TIER 2 (v5.0) — the kill-the-server gate's own lifecycle ───────────────────────────────────────
|
||
GV = 'pipeline/godverse_server.py'
|
||
LIVE_BASE = 'http://127.0.0.1:8778'
|
||
LIVE_SHOP = 3962749
|
||
FIXTURE_DB = 'recordgod_fixture'
|
||
REAL_DB = 'recordgod'
|
||
|
||
|
||
def _gv(args, db=None):
|
||
env = dict(os.environ)
|
||
if db: env['GODVERSE_POS_DB'] = db
|
||
return subprocess.run(['python3', GV] + args, cwd=str(ROOT), capture_output=True, text=True, env=env)
|
||
|
||
|
||
def _gv_kill():
|
||
subprocess.run("lsof -ti :8778 | xargs kill -9", shell=True, capture_output=True)
|
||
time.sleep(0.8)
|
||
|
||
|
||
def _gv_start(db):
|
||
_gv_kill()
|
||
env = dict(os.environ); env['GODVERSE_POS_DB'] = db
|
||
subprocess.Popen(['python3', GV, 'start'], cwd=str(ROOT), env=env,
|
||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
for _ in range(20):
|
||
time.sleep(0.3)
|
||
try:
|
||
urllib.request.urlopen(f'{LIVE_BASE}/godverse/v1/shop/{LIVE_SHOP}/stock', timeout=1).read()
|
||
return True
|
||
except Exception:
|
||
pass
|
||
return False
|
||
|
||
|
||
def smoke_tier2(p):
|
||
"""R27 — THE KILL-THE-SERVER GATE. Charter law #1 made falsifiable: the server enriches, never gates.
|
||
|
||
This gate owns its own subject and its own weather. Two lanes made that necessary and both said so
|
||
before it could go wrong:
|
||
· G: production `gone` is EMPTY — 0 of crate 550's 120 records have sold since 2026-07-01 — so a
|
||
sold-means-gone assert on live data would pass VACUOUSLY FOREVER. G built `make-fixture`, and the
|
||
gate MUST use it. "A fixture is not a lie; a green assert over an absent subject is."
|
||
· C: pick-then-omit and filter-the-array BOTH hide the sold record, so "the gone item never renders"
|
||
cannot tell them apart — it is vacuous and it goes green. The discriminating assert is COLLATERAL:
|
||
the survivors' identity AND ORDER unchanged across a `gone` delta. One sale must remove one
|
||
record, not re-stock the shop (C measured the naive wiring changing 21 other records).
|
||
|
||
Tier 2 is non-deterministic BY DESIGN (charter law #2), so this gate never asserts dig determinism
|
||
with the server up. What it asserts is the LADDER: live enriches, dead degrades, and the player never
|
||
waits on either. Subject absent (no psql / no POS db) ⇒ SKIP with the reason printed, never a pass."""
|
||
head('SMOKE: the kill-the-server gate (R27 — charter law #1: the server enriches, never gates)')
|
||
if subprocess.run(['which', 'psql'], capture_output=True).returncode != 0:
|
||
print('· SKIP: no psql — the POS fixture cannot be built, nothing asserted (needs the GODVERSE box)')
|
||
return
|
||
if subprocess.run(['psql', '-lqt'], capture_output=True, text=True).stdout.find(REAL_DB) < 0:
|
||
print(f'· SKIP: no `{REAL_DB}` POS database here — subject absent, nothing asserted (Lane G)')
|
||
return
|
||
|
||
fx = _gv(['make-fixture', '--shop', str(LIVE_SHOP), '--sold', '3'])
|
||
sold = [l.strip() for l in fx.stdout.splitlines() if l.strip().startswith('sku_')]
|
||
if len(sold) != 3:
|
||
FAIL(f'the fixture built no subject (want 3 sold, got {len(sold)}): {fx.stderr[:120]}'); return
|
||
print(f' subject: {len(sold)} record(s) marked SOLD in a fixture POS — production `gone` is empty, '
|
||
f'so this is the only honest way to have one')
|
||
|
||
try:
|
||
# ── ARM 1: server UP → live fields land, and the crate omits WITHOUT reshuffling ──
|
||
if not _gv_start(FIXTURE_DB):
|
||
FAIL('the fixture server would not come up'); return
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, f'plansrc=osm&town=redhill_godverse&stock=real&dig=1&live=1')
|
||
r1 = pg.evaluate("""async (shop) => {
|
||
const D = window.DBG, m = await import('./js/interiors/stockpack.js');
|
||
const BASE = `assets/stock_godverse/${shop}/`;
|
||
D.enterShop('g:' + shop); await new Promise(r => setTimeout(r, 200));
|
||
const pack = m.getStockPack('record', BASE);
|
||
const before = pack.items.map(i => i.id);
|
||
await new Promise(r => setTimeout(r, 1800));
|
||
const gone = pack.gone ? [...pack.gone] : [];
|
||
const after = pack.items.map(i => i.id);
|
||
const survB = before.filter(i => !gone.includes(i));
|
||
const survA = after.filter(i => !gone.includes(i));
|
||
return { gone, itemsBefore: before.length, itemsAfter: after.length,
|
||
orderIntact: before.length === after.length && before.every((x, i) => x === after[i]),
|
||
survivors: survA.length,
|
||
collateral: survB.filter((x, i) => survA[i] !== x).length };
|
||
}""", LIVE_SHOP)
|
||
OK(f"ARM 1 server-up: live read consumed — gone[{len(r1['gone'])}], {r1['survivors']} survivors") \
|
||
if len(r1['gone']) == 3 else FAIL(f"ARM 1: gone[{len(r1['gone'])}], expected 3 from the fixture")
|
||
OK(f"ARM 1: items[] untouched — {r1['itemsAfter']} ids, identity AND ORDER intact") \
|
||
if r1['orderIntact'] and r1['itemsAfter'] == 120 \
|
||
else FAIL(f"ARM 1: items[] MUTATED ({r1['itemsBefore']}→{r1['itemsAfter']}, order intact={r1['orderIntact']})")
|
||
OK('ARM 1: COLLATERAL 0 — one sale removed one record, it did not re-stock the shop') \
|
||
if r1['collateral'] == 0 \
|
||
else FAIL(f"ARM 1: COLLATERAL {r1['collateral']} — the crate RESHUFFLED (filter-the-array, not pick-then-omit)")
|
||
if set(r1['gone']) != set(sold):
|
||
FAIL(f"ARM 1: gone {r1['gone']} != the fixture's {sold} — the subject is not what the gate thinks")
|
||
else:
|
||
OK('ARM 1: `gone` carries the atlas id form verbatim — zero transformation (R26 fence)')
|
||
|
||
# ── ARM 2: KILL IT MID-SESSION ──
|
||
_gv_kill()
|
||
errs.clear()
|
||
r2 = pg.evaluate("""async (shop) => {
|
||
const D = window.DBG, m = await import('./js/interiors/stockpack.js');
|
||
const pack = m.getStockPack('record', `assets/stock_godverse/${shop}/`);
|
||
const frames = []; let last = performance.now();
|
||
const iv = setInterval(() => { const n = performance.now(); frames.push(n - last); last = n; }, 0);
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 300));
|
||
const t0 = performance.now();
|
||
D.enterShop('g:' + shop); // fires a read at a DEAD server
|
||
const enterMs = performance.now() - t0;
|
||
await new Promise(r => setTimeout(r, 2000)); // past the 1200ms abort
|
||
clearInterval(iv);
|
||
const si = D.stockInfo();
|
||
frames.sort((a, b) => a - b);
|
||
return { enterMs, worst: frames[frames.length - 1], packItems: si.packItems,
|
||
ownTex: si.texUrls.filter(u => u.includes(`stock_godverse/${shop}/`)).length };
|
||
}""", LIVE_SHOP)
|
||
# THE CONTROL (R21: a number without a control is not a measurement). Entering a shop
|
||
# REBUILDS an interior — that costs the same with or without a server, so an absolute
|
||
# threshold measures Lane C's room build, not the network failure. Walk the same shop with
|
||
# tier 2 OFF and compare: the question is "did the DEATH cost anything", not "is entering fast".
|
||
ctrl = pg.evaluate("""async (shop) => {
|
||
const D = window.DBG;
|
||
const frames = []; let last = performance.now();
|
||
const iv = setInterval(() => { const n = performance.now(); frames.push(n - last); last = n; }, 0);
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 300));
|
||
const t0 = performance.now();
|
||
D.enterShop('g:' + shop);
|
||
const enterMs = performance.now() - t0;
|
||
await new Promise(r => setTimeout(r, 2000));
|
||
clearInterval(iv); frames.sort((a, b) => a - b);
|
||
return { enterMs, worst: frames[frames.length - 1] };
|
||
}""", LIVE_SHOP) # the breaker has tripped by now → this walk touches no network at all
|
||
dEnter = r2['enterMs'] - ctrl['enterMs']
|
||
dWorst = r2['worst'] - ctrl['worst']
|
||
print(f" control (same walk, breaker tripped, no network): enter {ctrl['enterMs']:.0f}ms, worst frame {ctrl['worst']:.0f}ms")
|
||
OK(f"ARM 2 kill mid-session: enter {r2['enterMs']:.0f}ms vs control {ctrl['enterMs']:.0f}ms (Δ{dEnter:+.0f}ms) — "
|
||
f"worst frame {r2['worst']:.0f}ms vs {ctrl['worst']:.0f}ms (Δ{dWorst:+.0f}ms): the death cost nothing the room didn't") \
|
||
if (dEnter < 100 and dWorst < 100) \
|
||
else FAIL(f"ARM 2: the DEATH itself stuttered — enter Δ{dEnter:+.0f}ms, worst frame Δ{dWorst:+.0f}ms over control")
|
||
OK(f"ARM 2: the room is still complete from files — {r2['packItems']} items, {r2['ownTex']} own atlas tex") \
|
||
if (r2['packItems'] == 120 and r2['ownTex'] > 0) \
|
||
else FAIL(f"ARM 2: the room degraded WRONG — {r2['packItems']} items, {r2['ownTex']} own tex")
|
||
# THE BRIEF SAID ZERO. ZERO IS UNACHIEVABLE, AND F MEASURED WHY — see LANE_F_NOTES §27.
|
||
# A fetch to a dead host logs `net::ERR_CONNECTION_REFUSED` at the BROWSER level; no
|
||
# `.catch()` suppresses it (same species as R26's 404s, one layer up). To log zero the
|
||
# reader would have to know the server is dead WITHOUT ASKING, which it cannot. So one
|
||
# refusal is the irreducible cost of discovering death — and asserting EXACTLY ONE is
|
||
# strictly stronger than asserting zero: it proves the circuit breaker. Without the
|
||
# breaker this is one error per shop entry, forever, and "zero" could never distinguish
|
||
# a working breaker from a broken one because both fail it.
|
||
game_errs = [e for e in errs if 'ERR_CONNECTION_REFUSED' not in e]
|
||
net_errs = [e for e in errs if 'ERR_CONNECTION_REFUSED' in e]
|
||
OK('ARM 2: ZERO errors from game code — nothing threw, nothing warned') \
|
||
if not game_errs else FAIL(f"ARM 2: {len(game_errs)} GAME error(s) on server death; first: {game_errs[0][:120]}")
|
||
OK(f'ARM 2: EXACTLY 1 refused connection — the circuit breaker tripped and never asked again') \
|
||
if len(net_errs) == 1 \
|
||
else FAIL(f"ARM 2: {len(net_errs)} refused connections — the breaker did NOT trip "
|
||
f"(1 = discovery, >1 = it keeps asking a dead server)")
|
||
finally:
|
||
b.close()
|
||
|
||
# ── ARM 3: boot with the server DOWN → pure tier 1, no live state at all ──
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'plansrc=osm&town=redhill_godverse&stock=real&dig=1&live=1')
|
||
r3 = pg.evaluate("""async (shop) => {
|
||
const D = window.DBG, m = await import('./js/interiors/stockpack.js');
|
||
D.enterShop('g:' + shop); await new Promise(r => setTimeout(r, 2000));
|
||
const pack = m.getStockPack('record', `assets/stock_godverse/${shop}/`);
|
||
const si = D.stockInfo();
|
||
return { items: si.packItems, goneSize: pack.gone ? pack.gone.size : 0,
|
||
ownTex: si.texUrls.filter(u => u.includes(`stock_godverse/${shop}/`)).length };
|
||
}""", LIVE_SHOP)
|
||
OK(f"ARM 3 server-down boot: pure tier 1 — {r3['items']} items, gone[{r3['goneSize']}], {r3['ownTex']} own tex") \
|
||
if (r3['items'] == 120 and r3['goneSize'] == 0 and r3['ownTex'] > 0) \
|
||
else FAIL(f"ARM 3: not pure tier 1 — {r3['items']} items, gone[{r3['goneSize']}]")
|
||
g3 = [e for e in errs if 'ERR_CONNECTION_REFUSED' not in e]
|
||
n3 = [e for e in errs if 'ERR_CONNECTION_REFUSED' in e]
|
||
OK('ARM 3: ZERO errors from game code booting against a dead server') \
|
||
if not g3 else FAIL(f"ARM 3: {len(g3)} game error(s); first: {g3[0][:120]}")
|
||
OK('ARM 3: EXACTLY 1 refused connection for the whole boot — asked once, learned, stopped') \
|
||
if len(n3) == 1 else FAIL(f"ARM 3: {len(n3)} refused connections (want exactly 1: the breaker)")
|
||
finally:
|
||
b.close()
|
||
|
||
# ── THE SANDBOX, BOTH DIRECTIONS (#4) ──
|
||
# real→game proven above (a real sale removed a record from the game crate).
|
||
# game→real: prove the ABSENCE of the verb. G: 501 is a stronger proof than 404 — a 404 means a
|
||
# handler ran and declined; 501 means there is NO write handler in the process at all.
|
||
_gv_start(FIXTURE_DB)
|
||
codes = {}
|
||
for verb in ('POST', 'PUT', 'PATCH', 'DELETE'):
|
||
rq = urllib.request.Request(f'{LIVE_BASE}/godverse/v1/shop/{LIVE_SHOP}/stock', method=verb, data=b'{}')
|
||
try:
|
||
urllib.request.urlopen(rq, timeout=3); codes[verb] = 200
|
||
except urllib.error.HTTPError as e:
|
||
codes[verb] = e.code
|
||
except Exception as e:
|
||
codes[verb] = str(e)[:20]
|
||
OK(f'SANDBOX game→real: every write verb 501 — there is no write handler in the process {codes}') \
|
||
if all(c == 501 for c in codes.values()) \
|
||
else FAIL(f'SANDBOX game→real: a write verb did NOT answer 501 — {codes}')
|
||
try:
|
||
urllib.request.urlopen(f'{LIVE_BASE}/godverse/v1/shop/158/stock', timeout=3)
|
||
FAIL('SANDBOX: a MINT shop served live stock — the metadata fence leaks')
|
||
except urllib.error.HTTPError as e:
|
||
OK('SANDBOX: a mint shop 404s — only the one real shop is live, which is the point') \
|
||
if e.code == 404 else FAIL(f'SANDBOX: mint shop answered {e.code}, expected 404')
|
||
finally:
|
||
_gv_start(REAL_DB) # leave the box as we found it: the REAL POS serving
|
||
|
||
|
||
def smoke_glance(p):
|
||
"""R29 Spike 1 — THE POSE GATE, un-SKIPPED. v6's first thread, and the first clip to reach the game.
|
||
|
||
R28 wrote this gate and it had NO SUBJECT: `look.glb` was converted, verified, published, and
|
||
sha1-proven by fetching the bytes back off the depot — and nothing bound it. Every check E ran was
|
||
real and passed; none could see that no consumer existed (a gate on the artifact cannot see the wire
|
||
— the R17 pattern's fifth outing). It SKIPPED with its reason printed rather than report green over
|
||
nothing. D bound it in R29 wave 0, so the gate binds too.
|
||
|
||
Asserts the two things that matter, and they pull in opposite directions:
|
||
· the glance REACHES THE GAME — peds actually enter the state, on the default boot;
|
||
· `?classic=1` never fetches the clip and never glances — while STILL DRAWING THE ROLL, because
|
||
D takes from a dedicated rng stream always and only flips the flag when a clip exists. That is
|
||
what keeps the covenant byte-identical while the behaviour is gated: the determinism is in the
|
||
draw, not in the outcome. Asserting "0 glancing" alone would pass on a fleet that failed to load
|
||
— so we assert the clip is ABSENT under classic and PRESENT on the default boot."""
|
||
head('SMOKE: the glance (R29 Spike 1 — v6\'s first clip in the game · classic still zero-fetch)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
# Check the subject on DISK, not via pg.goto — navigating to a .glb starts a download and
|
||
# Playwright aborts the navigation, which crashes the gate instead of skipping it. A gate that
|
||
# dies looking for its subject is worse than one that skips: it takes the suite with it.
|
||
if not os.path.exists(os.path.join(str(ROOT), 'web', 'models', 'peds', 'look.glb')):
|
||
print('· SKIP: look.glb not on disk — subject absent, nothing asserted (Lane E)'); return
|
||
boot(pg, 'plansrc=osm&town=redhill_godverse')
|
||
r = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, c = P.citizens;
|
||
let peak = 0;
|
||
for (let i = 0; i < 9000; i++) { c.update(1/60);
|
||
if (i % 30 === 0) { const p = (c._activeList||[]).concat(c.roster||[]);
|
||
const g = p.filter(x => x && x.glance === true).length; if (g > peak) peak = g; } }
|
||
const p = (c._activeList||[]).concat(c.roster||[]);
|
||
return { bound: !!P.fleet.lookClip, dur: P.fleet.lookClip ? P.fleet.lookClip.duration : 0,
|
||
tracks: P.fleet.lookClip ? P.fleet.lookClip.tracks.length : 0,
|
||
peak, pop: p.length, rng: p.some(x => x && !!x.glanceRng) };
|
||
}""")
|
||
OK(f"default boot: look.glb BOUND — {r['dur']:.1f}s, {r['tracks']} tracks") \
|
||
if r['bound'] else FAIL('default boot: look.glb is NOT bound — the R28 gap is back')
|
||
OK(f"the glance REACHES THE GAME — peak {r['peak']} of {r['pop']} peds glancing") \
|
||
if r['peak'] > 0 else FAIL(f"no ped ever glanced across 9000 ticks ({r['pop']} peds) — clip bound, behaviour absent")
|
||
if errs: FAIL(f'glance: {len(errs)} console error(s); first: {errs[0][:120]}')
|
||
else: print(' · 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'classic=1')
|
||
r = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, c = P.citizens;
|
||
let peak = 0;
|
||
for (let i = 0; i < 6000; i++) { c.update(1/60);
|
||
if (i % 40 === 0) { const p = (c._activeList||[]).concat(c.roster||[]);
|
||
const g = p.filter(x => x && x.glance === true).length; if (g > peak) peak = g; } }
|
||
const p = (c._activeList||[]).concat(c.roster||[]);
|
||
return { bound: !!P.fleet.lookClip, peak, pop: p.length, rng: p.some(x => x && !!x.glanceRng) };
|
||
}""")
|
||
# NOT bound is the assertion: ?classic must not fetch the clip at all (zero-fetch-delta covenant)
|
||
OK('?classic=1: look.glb NEVER FETCHED — the zero-fetch-delta covenant holds') \
|
||
if not r['bound'] else FAIL('?classic=1 FETCHED look.glb — the covenant is breached')
|
||
OK(f"?classic=1: glance inert at the source — 0 of {r['pop']} peds, and the rng stream still draws "
|
||
f"({r['rng']}) so determinism is in the DRAW, not the outcome") \
|
||
if (r['peak'] == 0 and r['rng']) \
|
||
else FAIL(f"?classic=1: peak {r['peak']} glancing / rng stream present={r['rng']}")
|
||
if errs: FAIL(f'glance/classic: {len(errs)} console error(s); first: {errs[0][:120]}')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke_djdance(p):
|
||
"""R36 wave 2 — the 2 a.m. dance clips (046668e) through the gate, per E's wave-1 flags.
|
||
|
||
Four Rokoko free-mocap clips (dance_party/medium/drink/sway — Rokoko's own Mixamo-skeleton
|
||
export, NOT Mixamo; new licence zone, LANE_E_NOTES R36) joined `loadPedFleet({dance:!CLASSIC})`
|
||
outside the round system with only "page boots" verified. This smoke proves the commit's claims:
|
||
|
||
· the clips BIND (E flag (b)): gate on binds + NaN, never node counts — the Rokoko shape is
|
||
legitimately 68-node/52-joint, not the sit/look 66/65 template. Runtime census: post
|
||
_canon/_rotOnly each clip must bind all tracks except Rokoko's root `mixamorigReference`
|
||
(64/65 — matches E's node-side census and 90sDJsim's in-game measurement).
|
||
· the clips REACH THE GAME: a gig-night dancer's live action IS a dance clip (identity by
|
||
duration — the four durations 30.5–52.4 s are unambiguous vs the ~s idle), the mixer is
|
||
provably advancing, and fig.position.y stays FLAT (the procedural fallback is not running).
|
||
· `?classic=1` fetches ZERO dance clips (the zero-fetch-delta covenant; positive control =
|
||
the default boot fetched all 4, so the detector discriminates).
|
||
· `?noassets=1` falls back to PROCEDURAL sway: zero GLB fetches, dancers exist as
|
||
placeholders, and fig.position.y measurably bounces (>2 cm) — the exact inverse of the
|
||
clip-driven amplitude, so the pair proves which branch moved the body.
|
||
|
||
[R36 HOLD — not this gate's to fix] the 5 trellis DJ BODIES (b339402) fetch under ?classic
|
||
too: they are unconditional PED_NAMES.normal roster members (rigs.js:22, Lane D's file), which
|
||
also widened classic's seeded crowd pool 17→22 on July 20. Suite stayed green (plan fp is
|
||
plan-side; draws in tolerance) but the classic fetch surface + crowd identity moved without a
|
||
ruling. WARNED here so the record is in the machine, held for Fable per the five-holds law."""
|
||
head('SMOKE: dance clips (R36 — Rokoko binds · dancer driven · classic zero-fetch · noassets sway)')
|
||
if not os.path.exists(os.path.join(str(ROOT), 'web', 'models', 'peds', 'dance_party.glb')):
|
||
print('· SKIP: dance_party.glb not on disk — subject absent, nothing asserted (Lane E)'); return
|
||
DANCE_FILES = ['dance_party.glb', 'dance_medium.glb', 'dance_drink.glb', 'dance_sway.glb']
|
||
DJ_FILES = ['woman_dj_01.glb', 'woman_dj_02.glb', 'woman_dj_03.glb', 'dj_techno_01.glb', 'dj_phrtt_01.glb']
|
||
|
||
# 1+2) default boot: binds + the driven dancer (with fetch capture as the positive control)
|
||
b, pg, errs = new_page(p)
|
||
reqs = []
|
||
pg.on('request', lambda r: reqs.append(r.url))
|
||
try:
|
||
boot(pg, '')
|
||
r = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG, fleet = P.fleet;
|
||
const clips = fleet.danceClips || [];
|
||
// runtime bind census vs a real rig's node set (post _canon/_rotOnly, exactly as bound)
|
||
const nodes = new Set(); fleet.all[0].scene.traverse(o => { if (o.name) nodes.add(o.name); });
|
||
const census = clips.map(c => {
|
||
const un = [...new Set(c.tracks.filter(t => !nodes.has(t.name.split('.')[0]))
|
||
.map(t => t.name.split('.')[0]))];
|
||
return { dur: +c.duration.toFixed(2), total: c.tracks.length,
|
||
bind: c.tracks.length - c.tracks.filter(t => !nodes.has(t.name.split('.')[0])).length,
|
||
unbound: un };
|
||
});
|
||
// gig night: the dancer's live action
|
||
D.setSegment(5);
|
||
D.enterShop(P.gigs.venueShopIds[0]);
|
||
await new Promise(res => setTimeout(res, 600));
|
||
const crew = P.interiorMode.crew;
|
||
const dancers = crew ? crew.members.filter(m => m.part === 'crowd' && m.dance) : [];
|
||
const durs = new Set(clips.map(c => +c.duration.toFixed(2)));
|
||
const before = dancers.map(m => {
|
||
const acts = m.actor.mixer ? m.actor.mixer._actions : [];
|
||
return { kind: m.kind, clipDur: acts.length ? +acts[0]._clip.duration.toFixed(2) : null,
|
||
t: acts.length ? acts[0].time : null, y: m.actor.fig.position.y };
|
||
});
|
||
let minY = dancers.map(m => m.actor.fig.position.y), maxY = minY.slice();
|
||
for (let k = 0; k < 14; k++) {
|
||
await new Promise(res => setTimeout(res, 50));
|
||
dancers.forEach((m, i) => { const y = m.actor.fig.position.y;
|
||
minY[i] = Math.min(minY[i], y); maxY[i] = Math.max(maxY[i], y); });
|
||
}
|
||
const after = dancers.map(m => {
|
||
const acts = m.actor.mixer ? m.actor.mixer._actions : [];
|
||
return acts.length ? acts[0].time : null;
|
||
});
|
||
D.exitShop();
|
||
return { nClips: clips.length, census,
|
||
dancers: before.map((d, i) => ({ ...d, tAfter: after[i],
|
||
isDance: d.clipDur != null && durs.has(d.clipDur),
|
||
amp: +(maxY[i] - minY[i]).toFixed(4) })) };
|
||
}""")
|
||
if r['nClips'] == 4: OK('default boot: 4 dance clips loaded and canonicalised (party/medium/drink/sway)')
|
||
else: FAIL(f"default boot: expected 4 dance clips, fleet has {r['nClips']}")
|
||
badc = [c for c in r['census'] if c['bind'] != c['total'] - 1 or c['unbound'] != ['mixamorigReference']]
|
||
if not badc:
|
||
OK(f"Rokoko binds: every clip binds {r['census'][0]['bind']}/{r['census'][0]['total']} tracks, "
|
||
f"sole unbound = mixamorigReference (the Rokoko root — E's census, reproduced at runtime)")
|
||
else:
|
||
FAIL(f"dance clip bind census off: {badc[:2]}")
|
||
dn = r['dancers']
|
||
if not dn:
|
||
FAIL('gig night: no dancers in the crowd — the subject is absent')
|
||
else:
|
||
not_dance = [d for d in dn if not d['isDance'] or d['kind'] != 'rig']
|
||
stalled = [d for d in dn if d['tAfter'] is not None and d['t'] is not None and d['tAfter'] <= d['t']]
|
||
bouncing = [d for d in dn if d['amp'] > 0.02]
|
||
if not_dance: FAIL(f"gig night: dancer NOT driven by a dance clip: {not_dance[:2]}")
|
||
elif stalled: FAIL(f"gig night: dancer mixer not advancing (page stalled?) — amplitude read is vacuous: {stalled[:2]}")
|
||
elif bouncing: FAIL(f"gig night: dancer position.y bouncing {bouncing[0]['amp']}m — the PROCEDURAL fallback is running despite loaded clips")
|
||
else:
|
||
OK(f"gig night: {len(dn)} dancer(s) driven by real dance clips (durations {sorted(set(d['clipDur'] for d in dn))}s, "
|
||
f"mixers advancing, position.y flat — the clip moves the body, not the fallback)")
|
||
dance_fetches = [u for u in reqs if any(f in u for f in DANCE_FILES)]
|
||
if len(dance_fetches) == 4: OK('positive control: the default boot fetched all 4 dance clips (the detector can see the fetch)')
|
||
else: WARN(f"positive control: expected 4 dance-clip fetches on the default boot, saw {len(dance_fetches)}")
|
||
if errs: FAIL(f"dance/default: {len(errs)} console error(s); first: {errs[0][:120]}")
|
||
else: print(' · 0 console errors (default boot)')
|
||
finally:
|
||
b.close()
|
||
|
||
# 3) ?classic=1 — zero dance-clip fetches; the DJ-body roster fetch is WARNED (held, see docstring)
|
||
b, pg, errs = new_page(p)
|
||
reqs = []
|
||
pg.on('request', lambda r: reqs.append(r.url))
|
||
try:
|
||
boot(pg, 'classic=1')
|
||
pg.wait_for_timeout(1500)
|
||
dance_fetches = [u.split('/')[-1] for u in reqs if any(f in u for f in DANCE_FILES)]
|
||
dj_fetches = sorted(u.split('/')[-1] for u in reqs if any(f in u for f in DJ_FILES))
|
||
if not dance_fetches: OK('?classic=1: ZERO dance-clip fetches — the zero-fetch-delta covenant holds for the clips')
|
||
else: FAIL(f"?classic=1 FETCHED dance clips: {dance_fetches}")
|
||
if dj_fetches:
|
||
WARN(f"?classic=1 fetches the 5 trellis DJ BODIES ({len(dj_fetches)}) — they are unconditional roster "
|
||
f"members (rigs.js PED_NAMES, b339402), which also widened classic's crowd pool 17→22. "
|
||
f"HELD for Fable: bless the roster change or direct D to gate them out of ?classic")
|
||
else:
|
||
OK('?classic=1: zero DJ-body fetches')
|
||
if errs: FAIL(f"dance/classic: {len(errs)} console error(s); first: {errs[0][:120]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# 4) ?noassets=1 — the procedural sway fallback, alive and measured
|
||
b, pg, errs = new_page(p)
|
||
reqs = []
|
||
pg.on('request', lambda r: reqs.append(r.url))
|
||
try:
|
||
boot(pg, 'noassets=1')
|
||
r = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
D.setSegment(5);
|
||
D.enterShop(P.gigs.venueShopIds[0]);
|
||
await new Promise(res => setTimeout(res, 600));
|
||
const crew = P.interiorMode.crew;
|
||
const dancers = crew ? crew.members.filter(m => m.part === 'crowd' && m.dance) : [];
|
||
let minY = dancers.map(m => m.actor.fig.position.y), maxY = minY.slice();
|
||
for (let k = 0; k < 14; k++) {
|
||
await new Promise(res => setTimeout(res, 50));
|
||
dancers.forEach((m, i) => { const y = m.actor.fig.position.y;
|
||
minY[i] = Math.min(minY[i], y); maxY[i] = Math.max(maxY[i], y); });
|
||
}
|
||
D.exitShop();
|
||
return { fleet: !!P.fleet, dancers: dancers.map((m, i) => ({ kind: m.kind,
|
||
amp: +(maxY[i] - minY[i]).toFixed(4) })) };
|
||
}""")
|
||
glb_fetches = [u for u in reqs if '.glb' in u]
|
||
if not glb_fetches: OK('?noassets=1: zero GLB fetches (dance clips included)')
|
||
else: FAIL(f"?noassets=1: {len(glb_fetches)} GLB fetch(es) leaked; first {glb_fetches[0]}")
|
||
dn = r['dancers']
|
||
swaying = [d for d in dn if d['amp'] > 0.02]
|
||
if not dn:
|
||
FAIL('?noassets=1: no dancers — fallback subject absent')
|
||
elif r['fleet']:
|
||
FAIL('?noassets=1: fleet exists — the boot is not asset-free')
|
||
elif len(swaying) == len(dn):
|
||
OK(f"?noassets=1: procedural sway ALIVE — {len(dn)} placeholder dancer(s) bouncing "
|
||
f"(amp {min(d['amp'] for d in dn)}–{max(d['amp'] for d in dn)}m vs the clip-driven 0.00 — the inverse pair proves the branch)")
|
||
else:
|
||
FAIL(f"?noassets=1: dancer(s) NOT swaying procedurally: {dn}")
|
||
if errs: FAIL(f"dance/noassets: {len(errs)} console error(s); first: {errs[0][:120]}")
|
||
else: print(' · 0 console errors (?noassets)')
|
||
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 →
|
||
tonightRoster(id) → GigCrew.spawn({roster}) → the front crowd slots carry those identities. Asserts the
|
||
seam on identities (crewInfo.fromRoster + member.pedIndex ∈ tonightRoster), the invariant crowd ⊇
|
||
roster ∩ cap, and that an EMPTY roster injects nobody (D's byte-identical law, proven here on the shell
|
||
side; D's sim soak proves the sim side). Also ledger #5 — reports SETTLED tris (after the async
|
||
instrument GLBs land), not the pre-load number C caught the R13 smoke reporting."""
|
||
head('SMOKE: continuity (R15 — the ped you followed is in the crowd · empty-roster clean · settled tris)')
|
||
if not gigs_landed(p):
|
||
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
|
||
return
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
pg.keyboard.press('Space')
|
||
res = pg.evaluate("""async () => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const id = P.gigs.venueShopIds[0];
|
||
const rosterOf = () => (P.citizens.tonightRoster(id) || []).map(e => e.pedIndex);
|
||
// 1) EMPTY-ROSTER PATH — enter before any admit; crew must inject only what's in the roster
|
||
// (usually 0). This is the shell side of D's "empty roster = byte-identical to R13" law.
|
||
D.setSegment(5); // NIGHT — the gig is on
|
||
const rosterAtEmpty = rosterOf();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 550));
|
||
const ei = P.interiorMode.crewInfo;
|
||
const emptyFromRoster = ei ? ei.fromRoster : -1, emptyCrowd = ei ? ei.crowd : 0;
|
||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||
// 2) POPULATED PATH — let the street loop spawn the queue, then admit a few EXPLICITLY (rAF
|
||
// auto-drain is throttled in automation, per B's note). Each admit fires onAdmit → recordVenueEntry.
|
||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
|
||
const q = P.venueQueues && P.venueQueues.get(id);
|
||
const admitted = [];
|
||
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) { const e = q.admitOne(); if (e) admitted.push(e.pedIndex); }
|
||
const roster = rosterOf();
|
||
D.enterShop(id); await new Promise(r => setTimeout(r, 700)); // build + crew spawn
|
||
const info = P.interiorMode.crewInfo, room = P.interiorMode.current;
|
||
const crowd = P.interiorMode.crew ? P.interiorMode.crew.members.filter(m => m.part === 'crowd') : [];
|
||
const rosterCrowd = crowd.filter(m => m.fromRoster).map(m => m.pedIndex);
|
||
// 3) SETTLED TRIS (ledger #5) — immediately after build vs after the instrument GLBs + rig atlas land
|
||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||
const trisImmediate = P.renderer.info.render.triangles, draws = P.renderer.info.render.calls;
|
||
await new Promise(r => setTimeout(r, 1600));
|
||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||
const trisSettled = P.renderer.info.render.triangles, drawsSettled = P.renderer.info.render.calls;
|
||
return { rosterAtEmpty, emptyFromRoster, emptyCrowd, admitted, roster, rosterCrowd,
|
||
fromRoster: info ? info.fromRoster : -1, crowd: info ? info.crowd : 0,
|
||
watchPoints: room ? room.watchPoints.length : 0,
|
||
trisImmediate, draws, trisSettled, drawsSettled };
|
||
}""")
|
||
# empty-roster path: crew injects exactly what's in the roster at that moment (0 unless surge beat us)
|
||
want_empty = min(len(res['rosterAtEmpty']), res['emptyCrowd'])
|
||
if res['emptyFromRoster'] == want_empty:
|
||
OK(f"empty roster: crew injected {res['emptyFromRoster']} from a {len(res['rosterAtEmpty'])}-entry roster "
|
||
f"(crowd {res['emptyCrowd']} — no phantom continuity, R13-identical path)")
|
||
else:
|
||
FAIL(f"empty roster: fromRoster {res['emptyFromRoster']} != roster∩cap {want_empty} (roster leaked)")
|
||
# the queue admits were recorded into tonightRoster (the onAdmit → recordVenueEntry relay)
|
||
roster, admitted, rosterCrowd = res['roster'], res['admitted'], res['rosterCrowd']
|
||
if admitted and all(a in roster for a in admitted):
|
||
OK(f"relay: {len(admitted)} queue admits → tonightRoster ({admitted} ⊆ roster of {len(roster)})")
|
||
else:
|
||
FAIL(f"relay: queue admits {admitted} not all in tonightRoster {roster} (onAdmit→recordVenueEntry broke)")
|
||
# THE SEAM — crowd ⊇ roster ∩ cap, and every roster-crowd member's pedIndex is genuinely in the roster
|
||
cap = res['crowd']
|
||
want = min(len(roster), cap)
|
||
subset_ok = set(rosterCrowd).issubset(set(roster))
|
||
if res['fromRoster'] == want and want > 0 and subset_ok and set(roster[:cap]).issubset(set(rosterCrowd)):
|
||
OK(f"continuity seam: {res['fromRoster']} of {cap} crowd came from the roster "
|
||
f"(crowd ⊇ roster∩cap ✓, all pedIndex ∈ tonightRoster ✓) — the ped you followed is in the crowd")
|
||
else:
|
||
FAIL(f"continuity seam: fromRoster {res['fromRoster']} vs roster∩cap {want}; rosterCrowd {rosterCrowd} "
|
||
f"vs roster {roster} (subset={subset_ok})")
|
||
# ledger #5 — settled-tris honesty (report both; interior gate is draws, which stays low)
|
||
OK(f"settled tris: {res['trisImmediate']:,} pre-load → {res['trisSettled']:,} settled "
|
||
f"(instrument GLBs; draws {res['draws']}→{res['drawsSettled']} ≤ {GIG_DRAWS_MAX} gate)")
|
||
if errs: FAIL(f"continuity: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('continuity: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
# ═══ R30 v7.0-alpha — THE GATES (ledger #5). Every gate below names its subject and proves it
|
||
# touched it (the vacuous-gate law): storage counts come from a prototype instrument with a POSITIVE
|
||
# control, the rotation fp hashes VERTEX DATA (the wave-3 binding — F measured the transform fp
|
||
# vacuous over batched parody stock), and the no-pump run drives the REAL dig/sell UI, not the API. ═══
|
||
|
||
GAME_DOM_IDS = ['pc-game', 'pc-gamebar', 'pc-crate', 'pc-crate-btn', 'pc-sleep-btn', 'pc-day', 'pc-cash']
|
||
|
||
# Installed BEFORE any page script (add_init_script) — counts every Storage-prototype method call.
|
||
# "Measure, don't trust": save.js promises zero module-scope storage access; this sees through it.
|
||
STORAGE_INSTRUMENT = """(() => {
|
||
window.__storageCalls = [];
|
||
for (const m of ['getItem', 'setItem', 'removeItem', 'clear', 'key']) {
|
||
const orig = Storage.prototype[m];
|
||
Storage.prototype[m] = function (...a) {
|
||
window.__storageCalls.push(m + ':' + String(a.length ? a[0] : ''));
|
||
return orig.apply(this, a);
|
||
};
|
||
}
|
||
})();"""
|
||
|
||
# In-page FNV-1a over the serialized plan — the world fingerprint for the delta-law asserts.
|
||
JS_PLAN_FP = """() => { const s = JSON.stringify(window.PROCITY.plan); let h = 0x811c9dc5;
|
||
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); }
|
||
return h >>> 0; }"""
|
||
|
||
# The stock fingerprint: FNV-1a over the position + uv ARRAYS of every isStock/buyMesh mesh in the
|
||
# open room. NOT transforms — batchRoom merges parody stock at identity transform, so a transform
|
||
# hash reads constants and reports "no rotation" over stock that rotates (F's own R30 harness bug).
|
||
# The day-salted picks/jitters live in the vertex data; bytes/meshes come back so the caller can
|
||
# prove the subject was present (a hash over zero bytes is a green light over nothing).
|
||
JS_VERTEX_FP = """() => {
|
||
const P = window.PROCITY; let h = 0x811c9dc5 >>> 0, bytes = 0, meshes = 0;
|
||
const mix = (arr) => { const u = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
||
for (let i = 0; i < u.length; i++) { h = (h ^ u[i]) >>> 0; h = Math.imul(h, 0x01000193) >>> 0; }
|
||
bytes += u.length; };
|
||
P.interiorMode.current.group.traverse((o) => {
|
||
const u = o.userData || {};
|
||
if (!(u.isStock || u.buyMesh) || !o.geometry) return;
|
||
meshes++;
|
||
const pos = o.geometry.getAttribute('position'), uv = o.geometry.getAttribute('uv');
|
||
if (pos) mix(pos.array); if (uv) mix(uv.array);
|
||
});
|
||
return { fp: h >>> 0, bytes, meshes };
|
||
}"""
|
||
|
||
|
||
def _game_state(pg):
|
||
"""The byte-comparable game state + a savedAt-normalized export (both stringified IN PAGE so key
|
||
order is identical on every read — python re-serialization would reorder and fake a diff)."""
|
||
return pg.evaluate("""() => { const g = window.PROCITY.game;
|
||
const ex = JSON.parse(g.export()); delete ex.savedAt;
|
||
return { state: JSON.stringify({ day: g.day, cash: g.cash, town: g.townKey, collection: g.collection }),
|
||
exNorm: JSON.stringify(ex), day: g.day, cash: g.cash, n: g.collection.length }; }""")
|
||
|
||
|
||
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' };
|
||
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, bins: bins.length };
|
||
}""", idx)
|
||
|
||
|
||
def _dig_buy(pg):
|
||
"""One REAL buy: E at the aimed bin → riffle → pull the front sleeve → click BUY → WALK OUT.
|
||
Returns {cash0,n0,disabled,cash1,n1} read off the live wallet + collection."""
|
||
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
|
||
pg.wait_for_timeout(500)
|
||
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(350)
|
||
r = pg.evaluate("""() => {
|
||
const P = window.PROCITY, g = P.game, w = P.wallet;
|
||
const btn = document.querySelector('.pcdg-panel button');
|
||
if (!btn) return { err: 'no pull panel (pull failed)' };
|
||
const cash0 = w.cash(), n0 = g.collection.length, disabled = btn.disabled;
|
||
btn.click();
|
||
const out = document.querySelector('.pcdg-out'); if (out) out.click();
|
||
return { cash0, n0, disabled, cash1: w.cash(), n1: g.collection.length };
|
||
}""")
|
||
pg.wait_for_timeout(250)
|
||
return r
|
||
|
||
|
||
def _counter_sell(pg, max_sales=10):
|
||
"""Walk to the counter, press E — THE REAL ROUTING opens C's sell card — then click SELL up to
|
||
max_sales times (card closes itself when nothing sellable remains). Camera looks UP so the
|
||
bin-aim fallback can't win the E (aimed bin → dig is the routing's first branch, by contract)."""
|
||
r = pg.evaluate("""() => {
|
||
const P = window.PROCITY, room = P.interiorMode.current;
|
||
const c = room.counter && room.counter.pose;
|
||
if (!c) return { err: 'no counter pose' };
|
||
P.camera.position.set(c.x, 1.6, c.z + 1.2);
|
||
P.camera.lookAt(c.x, 4.0, c.z); P.camera.updateMatrixWorld(); // aim above the room — no bin under aim
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
|
||
const panel = document.querySelector('.pcsl-panel');
|
||
return { opened: !!(panel && panel.style.display === 'block'), sellActive: P.interiorMode.sellActive };
|
||
}""")
|
||
if r.get('err') or not r.get('opened'):
|
||
return r
|
||
sold = 0
|
||
for _ in range(max_sales):
|
||
s = pg.evaluate("""() => {
|
||
const p = document.querySelector('.pcsl-panel');
|
||
if (!p || p.style.display !== 'block') return { open: false };
|
||
const b = p.querySelector('button');
|
||
if (!b || b.disabled) return { open: true, stuck: true };
|
||
b.click(); return { open: true, sold: true };
|
||
}""")
|
||
if not s.get('open') or s.get('stuck'):
|
||
break
|
||
sold += 1
|
||
pg.wait_for_timeout(60)
|
||
pg.evaluate("""() => { const p = document.querySelector('.pcsl-panel');
|
||
if (p && p.style.display === 'block') { const x = p.querySelector('.x'); if (x) x.click(); } }""")
|
||
pg.wait_for_timeout(100)
|
||
r['sold'] = sold
|
||
return r
|
||
|
||
|
||
def _enter_record_shop(pg):
|
||
"""Midday, first OPEN record shop whose counter sits > 2.6 m from every bin (so counter-E can
|
||
never fall into binUnderAim's 2.5 m nearest-bin fallback — the routing's dig branch would win).
|
||
Returns the shop id it settled in, or an err."""
|
||
return pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG; D.setSegment(2);
|
||
const recs = (P.plan.shops || []).filter(s => s.type === 'record' && P.isOpen(s));
|
||
if (!recs.length) return { err: 'no open record shop' };
|
||
for (const s of recs.slice(0, 6)) {
|
||
D.enterShop(s.id);
|
||
const room = P.interiorMode.current;
|
||
const c = room.counter && room.counter.pose;
|
||
const bins = []; room.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bins.push(o); });
|
||
if (c && bins.length) {
|
||
const wp = new P.THREE.Vector3();
|
||
const near = bins.some(b => { b.getWorldPosition(wp); return Math.hypot(wp.x - c.x, wp.z - c.z) < 2.6; });
|
||
if (!near) return { shop: s.id, name: s.name, bins: bins.length };
|
||
}
|
||
D.exitShop();
|
||
}
|
||
return { err: 'no record shop with counter clear of bins in the first 6' };
|
||
}""")
|
||
|
||
|
||
def smoke_sell_routing(p):
|
||
"""R30 §9.4 — THE HELD JOIN, verified live: dig → buy → walk to the counter → E → C's sell card
|
||
→ SELL → wallet credits, collection shrinks via game.removeFind, B's HUD follows. Also the two
|
||
negative routing legs: E at the counter with NOTHING sellable opens no card; ?game=0 never routes."""
|
||
head('GATE R30·routing: the E-key sell routing (dig → counter → sell card → credit + removeFind + HUD)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'dig=1')
|
||
# HUD present (B's game bar — the gate reads it after the sale)
|
||
try:
|
||
pg.wait_for_function("() => !!document.getElementById('pc-cash')", timeout=8000)
|
||
except Exception:
|
||
pass
|
||
r0 = _enter_record_shop(pg)
|
||
if r0.get('err'): FAIL(f"routing: {r0['err']}"); return
|
||
# negative leg FIRST: empty collection ⇒ E at the counter opens NOTHING (open() false, no DOM)
|
||
r_neg = _counter_sell(pg, max_sales=0)
|
||
if r_neg.get('opened'): FAIL('routing: sell card opened over an EMPTY collection (open() must be false)')
|
||
else: OK('routing: E at the counter with nothing sellable → no card, no DOM (fail-closed)')
|
||
# buy one through the real dig
|
||
_at_bin(pg)
|
||
rb = _dig_buy(pg)
|
||
if rb.get('err') or rb.get('disabled'): FAIL(f"routing: dig buy failed — {rb}"); return
|
||
paid = rb['cash0'] - rb['cash1']
|
||
if rb['n1'] == rb['n0'] + 1 and paid > 0:
|
||
OK(f"routing: dig buy — ${paid} debited, collection {rb['n0']}→{rb['n1']}")
|
||
else:
|
||
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), 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:
|
||
FAIL(f"routing: entry shape wrong — {ent}")
|
||
# the sale, through the card
|
||
cash_before = pg.evaluate("() => window.PROCITY.wallet.cash()")
|
||
n_before = pg.evaluate("() => window.PROCITY.game.collection.length")
|
||
rs = _counter_sell(pg, max_sales=1)
|
||
if rs.get('err') or not rs.get('opened'):
|
||
FAIL(f"routing: E at the counter did not open the sell card — {rs}"); return
|
||
# HUD refreshes in STREET mode only (it is hidden inside a shop by design — setVisible(m==='street')),
|
||
# so "the HUD follows" is measured where the player can see it: back on the street, frames run.
|
||
pg.evaluate("() => window.DBG.exitShop()")
|
||
pg.wait_for_timeout(500)
|
||
after = pg.evaluate("""() => { const P = window.PROCITY;
|
||
const cashEl = document.getElementById('pc-cash'), crateBtn = document.getElementById('pc-crate-btn');
|
||
return { cash: P.wallet.cash(), n: P.game.collection.length,
|
||
hudCash: cashEl ? cashEl.textContent : null,
|
||
hudCrate: crateBtn ? crateBtn.textContent : null }; }""")
|
||
credited = after['cash'] - cash_before
|
||
# [R32 amendment] the no-pump law's beta form: the offer is EXACTLY min(basis−1, 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(basis−1, 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} 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']}")
|
||
else:
|
||
FAIL(f"routing: HUD stale — hudCash {after['hudCash']} vs {after['cash']}, {after['hudCrate']} vs CRATE {after['n']}")
|
||
else:
|
||
WARN('routing: game bar not built in this context — HUD leg unobserved (B verified it R30w2)')
|
||
if errs: FAIL(f"routing: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('routing: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
# ?game=0: the routing must be inert — E at the counter does nothing, zero sell DOM, zero errors
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'dig=1&game=0')
|
||
r = pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG; D.setSegment(2);
|
||
const rec = (P.plan.shops || []).find(s => s.type === 'record' && P.isOpen(s));
|
||
D.enterShop(rec.id);
|
||
const c = P.interiorMode.current.counter.pose;
|
||
P.camera.position.set(c.x, 1.6, c.z + 1.2); P.camera.lookAt(c.x, 4.0, c.z); P.camera.updateMatrixWorld();
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
|
||
return { game: !!P.game, panels: document.querySelectorAll('.pcsl-panel').length,
|
||
sellActive: P.interiorMode.sellActive };
|
||
}""")
|
||
if not r['game'] and r['panels'] == 0 and not r['sellActive']:
|
||
OK('routing: ?game=0 — no game, E at the counter is a no-op, zero sell DOM')
|
||
else:
|
||
FAIL(f"routing: ?game=0 leaked — {r}")
|
||
if errs: FAIL(f"routing/game=0: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke_save_determinism(p):
|
||
"""R30 gate (a) — scripted session (buy 2, sell 1, sleep 2×) → export → FRESH boot → import →
|
||
byte-equal game state AND world fingerprint unchanged (the delta law's falsifiable form)."""
|
||
head('GATE R30a: save/load determinism (buy 2 · sell 1 · sleep 2× → export → fresh boot → import)')
|
||
b, pg, errs = new_page(p)
|
||
exported = None; snap0 = None; fp0 = None
|
||
try:
|
||
boot(pg, 'dig=1')
|
||
r0 = _enter_record_shop(pg)
|
||
if r0.get('err'): FAIL(f"save-det: {r0['err']}"); return
|
||
for i in (1, 2):
|
||
_at_bin(pg)
|
||
rb = _dig_buy(pg)
|
||
if rb.get('err') or rb.get('disabled') or rb['n1'] != rb['n0'] + 1:
|
||
FAIL(f"save-det: scripted buy {i} failed — {rb}"); return
|
||
rs = _counter_sell(pg, max_sales=1)
|
||
if rs.get('err') or not rs.get('opened') or rs.get('sold') != 1:
|
||
FAIL(f"save-det: scripted sell failed — {rs}"); return
|
||
pg.evaluate("() => window.DBG.exitShop()")
|
||
pg.wait_for_timeout(200)
|
||
try: # B's game bar builds lazily on street frames — prefer the REAL surface
|
||
pg.wait_for_function("() => !!document.getElementById('pc-sleep-btn')", timeout=4000)
|
||
except Exception:
|
||
pass
|
||
slept = pg.evaluate("""() => { const g = window.PROCITY.game;
|
||
const btn = document.getElementById('pc-sleep-btn');
|
||
if (btn) { btn.click(); btn.click(); } else { g.sleep(); g.sleep(); }
|
||
return { day: g.day, viaBtn: !!btn }; }""")
|
||
if slept['day'] != 3: FAIL(f"save-det: 2 sleeps → day {slept['day']} (want 3)"); return
|
||
via = "via B's SLEEP button" if slept['viaBtn'] else "via game.sleep() — HUD bar absent in this context"
|
||
OK(f"save-det: scripted session done — bought 2, sold 1, slept 2× → day 3 ({via})")
|
||
snap0 = _game_state(pg)
|
||
exported = pg.evaluate("() => window.PROCITY.game.export()")
|
||
fp0 = pg.evaluate(JS_PLAN_FP)
|
||
print(f" subject: day {snap0['day']} · ${snap0['cash']} · {snap0['n']} item(s) · plan fp {fp0:#010x}")
|
||
if errs: FAIL(f"save-det/session: {len(errs)} console error(s); first: {errs[0][:140]}"); return
|
||
finally:
|
||
b.close()
|
||
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'dig=1') # fresh browser = fresh localStorage = a day-1 game
|
||
fp1 = pg.evaluate(JS_PLAN_FP)
|
||
if fp1 != fp0: FAIL(f"save-det: fresh boot plan fp {fp1:#010x} != {fp0:#010x} (seeded world drifted?)"); return
|
||
r = pg.evaluate("""(saved) => { const g = window.PROCITY.game;
|
||
const fresh = { day: g.day, n: g.collection.length };
|
||
const ok = g.import(saved);
|
||
return { ok, fresh, week: window.PROCITY.gigs ? window.PROCITY.gigs.weekNight : null }; }""", exported)
|
||
if not (r['ok'] and r['fresh']['day'] == 1 and r['fresh']['n'] == 0):
|
||
FAIL(f"save-det: import on a fresh boot — {r}"); return
|
||
snap1 = _game_state(pg)
|
||
fp2 = pg.evaluate(JS_PLAN_FP)
|
||
if snap1['state'] == snap0['state'] and snap1['exNorm'] == snap0['exNorm']:
|
||
OK(f"save-det: BYTE-EQUAL after import — state ({len(snap1['state'])} bytes) and savedAt-normalized "
|
||
f"export ({len(snap1['exNorm'])} bytes) identical to the exporting session")
|
||
else:
|
||
FAIL(f"save-det: state diverged — day {snap1['day']} vs {snap0['day']}, cash {snap1['cash']} vs "
|
||
f"{snap0['cash']}, n {snap1['n']} vs {snap0['n']}")
|
||
if fp2 == fp0:
|
||
OK(f"save-det: world fingerprint UNCHANGED across export→import ({fp2:#010x}) — the delta law holds")
|
||
else:
|
||
FAIL(f"save-det: import MOVED THE WORLD — plan fp {fp2:#010x} != {fp0:#010x}")
|
||
if r['week'] == 2:
|
||
OK('save-det: day-derived state applied on import — weekNight 2 == (3−1)%7')
|
||
else:
|
||
FAIL(f"save-det: weekNight {r['week']} after importing day 3 (want 2)")
|
||
if errs: FAIL(f"save-det/import: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('save-det: 0 console errors both contexts')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke_nopump(p):
|
||
"""R30 gate (b) — buy-then-immediately-sell ×5 through the REAL dig + sell-card paths → strictly
|
||
negative per trip. PLUS C's negative control (the wave-3 binding): with cash 0, the real buy paths
|
||
invoke wallet.buy, it returns false, and NO entry enters the collection — the pump C found is dead."""
|
||
head('GATE R30b: the no-pump gate v0 (5 real round trips → monotonic loss · the debit-gate negative control)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'dig=1&stock=real')
|
||
_pack_ready(pg, 'book') # the negative control's shelf needs the book pack
|
||
r0 = _enter_record_shop(pg)
|
||
if r0.get('err'): FAIL(f"no-pump: {r0['err']}"); return
|
||
deltas = []
|
||
for i in range(5):
|
||
_at_bin(pg)
|
||
cash0 = pg.evaluate("() => window.PROCITY.wallet.cash()")
|
||
rb = _dig_buy(pg)
|
||
if rb.get('err') or rb.get('disabled') or rb['n1'] != rb['n0'] + 1:
|
||
FAIL(f"no-pump: trip {i + 1} buy failed — {rb}"); return
|
||
rs = _counter_sell(pg) # sell EVERYTHING so no held item distorts the trip
|
||
if rs.get('err') or not rs.get('opened') or rs.get('sold', 0) < 1:
|
||
FAIL(f"no-pump: trip {i + 1} sell failed — {rs}"); return
|
||
cash1 = pg.evaluate("() => window.PROCITY.wallet.cash()")
|
||
deltas.append(cash1 - cash0)
|
||
if all(d <= -1 for d in deltas):
|
||
OK(f"no-pump: 5/5 round trips strictly negative — deltas {deltas} (Σ {sum(deltas)}) — "
|
||
f"the spread is the house's, by the sellOffer clamp")
|
||
else:
|
||
FAIL(f"no-pump: a round trip did NOT lose money — deltas {deltas}")
|
||
|
||
# ── C's negative control: no debit ⇒ no entry, on the REAL paths ──
|
||
broke = pg.evaluate("""() => { const g = window.PROCITY.game;
|
||
const ex = JSON.parse(g.export()); ex.cash = 0; ex.savedAt = Date.now();
|
||
const ok = g.import(JSON.stringify(ex));
|
||
return { ok, cash: g.cash, n: g.collection.length }; }""")
|
||
if not (broke['ok'] and broke['cash'] == 0):
|
||
FAIL(f"no-pump: could not set up the broke player — {broke}"); return
|
||
# arm 1: the dig — pull panel's BUY must be disabled; clicking it moves nothing
|
||
_at_bin(pg)
|
||
rd = _dig_buy(pg)
|
||
if rd.get('err'):
|
||
FAIL(f"no-pump/control: dig arm — {rd}")
|
||
elif rd['disabled'] and rd['n1'] == rd['n0'] and rd['cash1'] == 0:
|
||
OK('no-pump/control dig: BUY disabled at $0 — clicked anyway, zero debit, zero entry')
|
||
else:
|
||
FAIL(f"no-pump/control dig: {rd} — an unpaid entry got in or cash moved")
|
||
# arm 2: the shelf — wallet.buy IS invoked (counted, behavior untouched), returns false,
|
||
# and recordFind stays unreachable. This is the exact pump C built by accident, proven dead.
|
||
pg.evaluate("() => window.DBG.exitShop()")
|
||
pg.wait_for_timeout(200)
|
||
ra = pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG; D.setSegment(2);
|
||
const bk = (P.plan.shops || []).find(s => s.type === 'book' && P.isOpen(s));
|
||
if (!bk) return { err: 'no open book shop' };
|
||
D.enterShop(bk.id);
|
||
const meshes = []; P.interiorMode.current.group.traverse(o => {
|
||
if (o.userData && o.userData.buyMesh && o.userData.buyItems && o.userData.buyItems.length) meshes.push(o); });
|
||
if (!meshes.length) return { err: 'no buy meshes (book pack absent)' };
|
||
const m = meshes[0], it = m.userData.buyItems[0], wp = new P.THREE.Vector3();
|
||
m.localToWorld(wp.copy(it.center));
|
||
P.camera.position.set(wp.x, wp.y, wp.z + 1.0); P.camera.lookAt(wp.x, wp.y, wp.z); P.camera.updateMatrixWorld();
|
||
const g = P.game, w = P.wallet, n0 = g.collection.length;
|
||
const orig = w.buy; const calls = []; // instrument the REAL facade — count, don't mock
|
||
w.buy = function (o) { const r = orig.call(this, o); calls.push(r); return r; };
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
|
||
w.buy = orig;
|
||
return { calls, n0, n1: g.collection.length, cash: w.cash() };
|
||
}""")
|
||
if ra.get('err'):
|
||
FAIL(f"no-pump/control shelf: {ra['err']}")
|
||
elif ra['calls'] == [False] and ra['n1'] == ra['n0'] and ra['cash'] == 0:
|
||
OK("no-pump/control shelf: wallet.buy INVOKED once, returned false, recordFind unreachable — "
|
||
"zero entry, cash still $0 (C's pump is dead)")
|
||
else:
|
||
FAIL(f"no-pump/control shelf: {ra} — buy not invoked, or an entry minted without a debit")
|
||
# positive arm (the control's control): with cash restored the SAME aim buys — the gate discriminates
|
||
rp = pg.evaluate("""() => { const g = window.PROCITY.game, w = window.PROCITY.wallet;
|
||
const ex = JSON.parse(g.export()); ex.cash = 100; ex.savedAt = Date.now();
|
||
g.import(JSON.stringify(ex));
|
||
const n0 = g.collection.length, cash0 = w.cash();
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
|
||
const n1 = g.collection.length, cash1 = w.cash();
|
||
const e = g.collection[g.collection.length - 1] || {};
|
||
return { n0, n1, cash0, cash1, paidMatches: n1 === n0 + 1 && e.pricePaid === cash0 - cash1 }; }""")
|
||
if rp['n1'] == rp['n0'] + 1 and rp['cash1'] < rp['cash0'] and rp['paidMatches']:
|
||
OK(f"no-pump/control positive: same aim with cash — debit ${rp['cash0'] - rp['cash1']} == entry.pricePaid, "
|
||
f"1 entry in (the control can tell success from failure)")
|
||
else:
|
||
FAIL(f"no-pump/control positive: {rp} — the negative control was measuring a dead path")
|
||
if errs: FAIL(f"no-pump: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('no-pump: 0 console errors')
|
||
finally:
|
||
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(low−1,
|
||
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_wantlist_travel(p):
|
||
"""R33 gate — THE WANTLIST + THE RUMOR + TRAVEL COSTS A DAY (v7.0-beta wave 2). Legs:
|
||
(a) ☆ WANT on the pull card (real path) → the save carries the want; (b) a keeper rumor
|
||
naming the hunted artist surfaces in some shop's banner (seeded per shop×day — scan);
|
||
(c) buying the wanted item auto-clears the want; (d) booting ANOTHER town on the same save
|
||
costs exactly one day (cash/collection intact), (e) re-booting the SAME town costs nothing
|
||
(the control discriminates), (f) the return trip costs a day again; (g) ?game=0 builds ZERO
|
||
want DOM and mutters no rumor (construction-gating holds)."""
|
||
head('GATE R33: wantlist · rumor · travel-costs-a-day (+ same-town and ?game=0 controls)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
r0 = _enter_record_shop(pg)
|
||
if r0.get('err'): FAIL(f"wantlist: {r0['err']}"); return
|
||
# (a) pull the front sleeve, click ☆ WANT through the real card
|
||
_at_bin(pg)
|
||
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
|
||
pg.wait_for_timeout(400)
|
||
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)
|
||
# [R35 amendment] a FRESH game already hunts ONE seeded want (the first-five system) — this
|
||
# gate's fresh-boot baseline is that want, and the card adds a SECOND on top of it.
|
||
base = pg.evaluate("() => window.PROCITY.game.wants.length")
|
||
rw = pg.evaluate("""() => {
|
||
const w = document.querySelector('.pcdg-panel .want');
|
||
if (!w) return { err: 'no WANT line on the card' };
|
||
const before = w.textContent; w.click();
|
||
const g = window.PROCITY.game, ex = JSON.parse(g.export());
|
||
return { before, after: w.textContent, wants: ex.wants || [], live: g.wants.length }; }""")
|
||
if rw.get('err'): FAIL(f"wantlist: {rw['err']}"); return
|
||
if 'WANT IT' in rw['before'] and 'ON THE WANTLIST' in rw['after'] and len(rw['wants']) == base + 1 and rw['live'] == base + 1:
|
||
OK(f"wantlist: ☆→★ through the real card — the save carries {len(rw['wants'])} want(s) "
|
||
f"(the seeded hunt + {rw['wants'][-1]})")
|
||
else:
|
||
FAIL(f"wantlist: want did not take — base {base}, {rw}")
|
||
# the rumor may pick ANY current want (seeded index over the live list)
|
||
artists = [str(w.get('artist') or w.get('title') or '').upper() for w in rw['wants']]
|
||
artist = artists[-1]
|
||
pg.evaluate("""() => { const p = document.querySelector('.pcdg-panel');
|
||
const x = p && p.querySelector('.x'); if (x) x.click();
|
||
const o = document.querySelector('.pcdg-out'); if (o) o.click(); }""")
|
||
pg.wait_for_timeout(200)
|
||
pg.evaluate("() => window.DBG.exitShop()")
|
||
pg.wait_for_timeout(300)
|
||
# (b) the rumor: seeded ~45% per (shop, day) — walk the town's open shops until a keeper mutters
|
||
rr = pg.evaluate("""() => {
|
||
const P = window.PROCITY, D = window.DBG;
|
||
const open = (P.plan.shops || []).filter(s => P.isOpen(s)).slice(0, 10);
|
||
const seen = [];
|
||
for (const s of open) {
|
||
D.enterShop(s.id);
|
||
const t = [...document.querySelectorAll('div')].map(e => e.textContent)
|
||
.find(t => t && t.includes('🚪') && t.includes('🗣'));
|
||
D.exitShop();
|
||
if (t) return { rumor: t.slice(t.indexOf('🗣')), tried: seen.length + 1 };
|
||
seen.push(s.id);
|
||
}
|
||
return { rumor: null, tried: seen.length }; }""")
|
||
if rr.get('rumor') and any(a and a in rr['rumor'] for a in artists):
|
||
OK(f"rumor: a keeper muttered after {rr['tried']} shop(s) — {rr['rumor'][:80]}")
|
||
else:
|
||
FAIL(f"rumor: no keeper named any of {artists} in {rr.get('tried')} open shops — {rr}")
|
||
# (c) buy the wanted sleeve → the want clears itself
|
||
r1 = _enter_record_shop(pg)
|
||
if r1.get('err'): FAIL(f"wantlist/buy: {r1['err']}"); return
|
||
_at_bin(pg)
|
||
rb = _dig_buy(pg)
|
||
rc = pg.evaluate("() => ({ wants: window.PROCITY.game.wants.length, n: window.PROCITY.game.collection.length })")
|
||
if not rb.get('err') and not rb.get('disabled') and rc['wants'] == base and rc['n'] >= 1:
|
||
OK('wantlist: bought the hunted sleeve — ITS want cleared itself (the seeded hunt stays), '
|
||
'the find is in the crate')
|
||
else:
|
||
FAIL(f"wantlist/buy: {rb} · after {rc} (want base {base})")
|
||
base = pg.evaluate("() => ({ day: window.PROCITY.game.day, cash: window.PROCITY.game.cash, n: window.PROCITY.game.collection.length })")
|
||
# (d) travel: another town on the SAME save — the ride costs exactly one day
|
||
boot(pg, 'plansrc=osm&town=melbourne')
|
||
t1 = pg.evaluate("() => ({ day: window.PROCITY.game.day, cash: window.PROCITY.game.cash, n: window.PROCITY.game.collection.length, trav: window.PROCITY.game.traveled })")
|
||
if t1['day'] == base['day'] + 1 and t1['trav'] and t1['cash'] == base['cash'] and t1['n'] == base['n']:
|
||
OK(f"travel: synthetic → melbourne cost exactly one day (day {base['day']}→{t1['day']}), "
|
||
f"cash ${t1['cash']} and {t1['n']} find(s) rode along")
|
||
else:
|
||
FAIL(f"travel: {base} → {t1} (want day+1, traveled, cash/collection intact)")
|
||
# (e) same-town control: re-boot melbourne — no day moves, traveled stays false
|
||
boot(pg, 'plansrc=osm&town=melbourne')
|
||
t2 = pg.evaluate("() => ({ day: window.PROCITY.game.day, trav: window.PROCITY.game.traveled })")
|
||
if t2['day'] == t1['day'] and not t2['trav']:
|
||
OK(f"travel/control: re-booting the SAME town is free (day {t2['day']}, traveled false)")
|
||
else:
|
||
FAIL(f"travel/control: same-town reboot moved the day — {t2}")
|
||
# (f) the return trip costs a day again
|
||
boot(pg, '')
|
||
t3 = pg.evaluate("() => ({ day: window.PROCITY.game.day, trav: window.PROCITY.game.traveled })")
|
||
if t3['day'] == t1['day'] + 1 and t3['trav']:
|
||
OK(f"travel: the ride home cost a day too (day {t3['day']})")
|
||
else:
|
||
FAIL(f"travel: return trip wrong — {t3}")
|
||
# (g) ?game=0: zero want DOM on the card, zero rumor in any banner
|
||
boot(pg, 'game=0')
|
||
r2 = _enter_record_shop(pg)
|
||
if r2.get('err'): FAIL(f"wantlist/g0: {r2['err']}")
|
||
else:
|
||
_at_bin(pg)
|
||
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
|
||
pg.wait_for_timeout(350)
|
||
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)
|
||
g0 = pg.evaluate("""() => ({
|
||
want: !!document.querySelector('.pcdg-panel .want'),
|
||
panel: !!document.querySelector('.pcdg-panel'),
|
||
rumor: [...document.querySelectorAll('div')].some(e => e.textContent && e.textContent.includes('🗣')) })""")
|
||
if g0['panel'] and not g0['want'] and not g0['rumor']:
|
||
OK('wantlist/control: ?game=0 card has ZERO want DOM and no keeper mutters (construction-gated)')
|
||
else:
|
||
FAIL(f"wantlist/control: game=0 leaked want/rumor DOM — {g0}")
|
||
if errs: FAIL(f"wantlist: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('wantlist: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke_godbay(p):
|
||
"""R34 gate — GODBAY, the offline seeded auction. Legs: (a) ⚖ LIST through the real crate DOM →
|
||
the entry leaves the collection into save-carried listings; (b) sleep → resolved: listings empty,
|
||
cash+net (bounded [1, 2×high×0.88 + slack]) XOR the item came home, the wake toast carries the
|
||
morning paper; (c) THE ONE-HAMMER DETERMINISM: a pre-sleep export imported and slept TWICE lands
|
||
byte-identical cash/collection both times; (d) travel resolves too (list → ride → hammered on
|
||
arrival); (e) ?game=0 builds no crate at all (nothing to list from)."""
|
||
head('GATE R34: GODBAY — the offline auction (one hammer · bounded · morning paper · controls)')
|
||
BANDHI = {'record': {'bargain': 7, 'standard': 24, 'collector': 59, 'grail': 120},
|
||
'book': {'bargain': 4, 'standard': 14, 'collector': 39, 'grail': 80},
|
||
'toy': {'bargain': 5, 'standard': 19, 'collector': 49, 'grail': 100}}
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
r0 = _enter_record_shop(pg)
|
||
if r0.get('err'): FAIL(f"godbay: {r0['err']}"); return
|
||
rich = pg.evaluate("""() => { const g = window.PROCITY.game;
|
||
const ex = JSON.parse(g.export()); ex.cash = 5000; ex.savedAt = Date.now();
|
||
return g.import(JSON.stringify(ex)); }""")
|
||
if not rich: FAIL('godbay: bankroll import failed'); return
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
for i in range(2): # two finds: one to list now, one for the travel leg
|
||
_at_bin(pg)
|
||
rb = _dig_buy(pg)
|
||
if rb.get('err') or rb.get('disabled'): FAIL(f"godbay: buy {i + 1} failed — {rb}"); return
|
||
# (a) ⚖ LIST through the real crate DOM
|
||
rl = pg.evaluate("""() => {
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||
const g = window.PROCITY.game, n0 = g.collection.length;
|
||
const lb = document.querySelector('#pc-crate .pc-crate-row div[title*="GodBay"]');
|
||
if (!lb) return { err: 'no ⚖ LIST control in the crate' };
|
||
lb.click();
|
||
const ex = JSON.parse(g.export());
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||
return { n0, n1: g.collection.length, listings: (ex.listings || []).length,
|
||
entry: (ex.listings || [])[0] || null }; }""")
|
||
if rl.get('err'): FAIL(f"godbay: {rl['err']}"); return
|
||
if rl['n1'] == rl['n0'] - 1 and rl['listings'] == 1 and rl['entry'] and rl['entry'].get('listDay'):
|
||
OK(f"godbay: consigned through the crate — collection {rl['n0']}→{rl['n1']}, "
|
||
f"the save carries the listing (listDay {rl['entry']['listDay']})")
|
||
else:
|
||
FAIL(f"godbay: listing did not take — {rl}"); return
|
||
# (c) THE ONE-HAMMER DETERMINISM: freeze pre-sleep, sleep twice from the same frozen state
|
||
pre = pg.evaluate("() => window.PROCITY.game.export()")
|
||
outcomes = []
|
||
for _ in range(2):
|
||
pg.evaluate("(ex) => window.PROCITY.game.import(ex)", pre)
|
||
pg.evaluate("() => { document.getElementById('pc-sleep-btn').click(); }")
|
||
pg.wait_for_timeout(250)
|
||
toast = pg.evaluate("() => (document.getElementById('pc-toast') || {}).textContent || ''")
|
||
st = pg.evaluate("""() => { const g = window.PROCITY.game;
|
||
const ex = JSON.parse(g.export()); delete ex.savedAt;
|
||
return { cash: g.cash, n: g.collection.length, nl: g.listings.length, ex: JSON.stringify(ex) }; }""")
|
||
st['toast'] = toast
|
||
outcomes.append(st)
|
||
a, b2 = outcomes
|
||
if a['ex'] == b2['ex'] and a['cash'] == b2['cash']:
|
||
OK('godbay: ONE HAMMER — the same frozen state slept twice lands byte-identical both times')
|
||
else:
|
||
FAIL(f"godbay: hammer not deterministic — {a['cash']} vs {b2['cash']}")
|
||
# (b) resolution honesty + bounds + the morning paper
|
||
pre_cash = json.loads(pre)['cash']; pre_n = len(json.loads(pre)['collection'])
|
||
ent = rl['entry']; hi = BANDHI.get(ent.get('type'), BANDHI['record']).get(ent.get('band'), 120)
|
||
sold = a['cash'] > pre_cash; came_home = a['n'] == pre_n + 1
|
||
net = a['cash'] - pre_cash
|
||
if a['nl'] == 0 and (sold != came_home) and (not sold or 1 <= net <= int(2.0 * hi * 0.88) + 2):
|
||
OK(f"godbay: resolved — {'SOLD net $' + str(net) + f' (≤ 2×${hi}×0.88 bound)' if sold else 'passed in, came home'}"
|
||
f" · listings drained")
|
||
else:
|
||
FAIL(f"godbay: resolution dishonest — nl={a['nl']} sold={sold} home={came_home} net={net} hi={hi}")
|
||
if '⚖' in a['toast']:
|
||
OK(f"godbay: the wake toast carried the morning paper — {a['toast'][:90]}")
|
||
else:
|
||
FAIL(f"godbay: no ⚖ in the wake toast — {a['toast'][:90]!r}")
|
||
# (d) travel resolves too: list the second find, ride to melbourne
|
||
r2 = pg.evaluate("""() => {
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||
const g = window.PROCITY.game;
|
||
const lb = document.querySelector('#pc-crate .pc-crate-row div[title*="GodBay"]');
|
||
if (!lb) return { err: 'no ⚖ for the travel leg (collection empty?)' };
|
||
lb.click();
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||
return { cash: g.cash, nl: g.listings.length, n: g.collection.length }; }""")
|
||
if r2.get('err'): FAIL(f"godbay/travel: {r2['err']}")
|
||
else:
|
||
boot(pg, 'plansrc=osm&town=melbourne')
|
||
t = pg.evaluate("""() => { const g = window.PROCITY.game;
|
||
return { nl: g.listings.length, cash: g.cash, n: g.collection.length, trav: g.traveled }; }""")
|
||
if t['trav'] and t['nl'] == 0 and (t['cash'] > r2['cash'] or t['n'] == r2['n'] + 1):
|
||
OK(f"godbay/travel: the ride to melbourne hammered the listing (cash ${r2['cash']}→${t['cash']}, "
|
||
f"collection {r2['n']}→{t['n']})")
|
||
else:
|
||
FAIL(f"godbay/travel: {r2} → {t}")
|
||
# (e) ?game=0 — no crate, nothing to list from
|
||
boot(pg, 'game=0')
|
||
g0 = pg.evaluate("() => ({ crate: !!document.getElementById('pc-crate'), game: !!window.PROCITY.game })")
|
||
if not g0['crate'] and not g0['game']: OK('godbay/control: ?game=0 — no game, no crate, no auction house')
|
||
else: FAIL(f"godbay/control: game=0 leaked — {g0}")
|
||
if errs: FAIL(f"godbay: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('godbay: 0 console errors')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke_first_five(p):
|
||
"""R35 gate — THE FIRST FIVE MINUTES (charter #7). Legs: (a) a FRESH game arrives with a seeded
|
||
first want (parody roster) and the splash names the hunt + the pocket money; (b) the want is
|
||
the SAME artist in a second fresh browser (seeded, not random); (c) the beats walk the hustle:
|
||
E at a crate → '②', a buy → '③', a ⚖ LIST → the send-off; (d) a RETURNING save gets no
|
||
tutorial (splash line hidden, no '②' on dig); (e) ?game=0 shows no hunt line at all."""
|
||
head('GATE R35: THE FIRST FIVE MINUTES — seeded want · splash · beats (+ returning/?game=0 controls)')
|
||
ROSTER = {'THE VERANDAHS', 'DEZ & THE UTES', 'MERINO', 'HILLS HOIST', 'THE ESKIES', 'GOON BAG',
|
||
'STOBIE POLES', 'THE FIBROS', 'WARATAH', 'THONGS', 'THE CLOTHESLINE', 'GALAH'}
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
fa = pg.evaluate("""() => { const g = window.PROCITY.game, h = document.getElementById('pc-hunt');
|
||
return { wants: g.wants.map(w => w.artist), cash: g.cash,
|
||
hunt: h ? { shown: h.style.display !== 'none', text: h.textContent } : null }; }""")
|
||
if (len(fa['wants']) == 1 and fa['wants'][0] in ROSTER and fa['hunt'] and fa['hunt']['shown']
|
||
and fa['wants'][0] in fa['hunt']['text'] and f"${fa['cash']}" in fa['hunt']['text']):
|
||
OK(f"first-five: fresh game hunts {fa['wants'][0]} with ${fa['cash']} — the splash says so")
|
||
else:
|
||
FAIL(f"first-five: seeded want/splash wrong — {fa}")
|
||
# (c) the beats, through the real paths
|
||
r0 = _enter_record_shop(pg)
|
||
if r0.get('err'): FAIL(f"first-five: {r0['err']}"); return
|
||
pg.evaluate("""() => { const g = window.PROCITY.game;
|
||
const ex = JSON.parse(g.export()); ex.cash = 5000; ex.savedAt = Date.now(); g.import(JSON.stringify(ex)); }""")
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
_at_bin(pg)
|
||
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
|
||
pg.wait_for_timeout(350)
|
||
t2 = pg.evaluate("() => (document.getElementById('pc-toast') || {}).textContent || ''")
|
||
pg.evaluate("() => { const o = document.querySelector('.pcdg-out'); if (o) o.click(); }")
|
||
pg.wait_for_timeout(200)
|
||
_at_bin(pg)
|
||
rb = _dig_buy(pg)
|
||
t3 = pg.evaluate("() => (document.getElementById('pc-toast') || {}).textContent || ''")
|
||
rl = pg.evaluate("""() => {
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||
const lb = document.querySelector('#pc-crate .pc-crate-row div[title*="GodBay"]');
|
||
if (lb) lb.click();
|
||
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyC' }));
|
||
return (document.getElementById('pc-toast') || {}).textContent || ''; }""")
|
||
if '②' in t2 and '③' in t3 and 'whole hustle' in rl:
|
||
OK('first-five: the beats walked the hustle — ② at the crate, ③ on the buy, the send-off on the ⚖')
|
||
else:
|
||
FAIL(f"first-five: beats wrong — ②:{('②' in t2)} ③:{('③' in t3)} send-off:{('whole hustle' in rl)}")
|
||
# (d) a returning save gets no tutorial
|
||
boot(pg, '')
|
||
rd = pg.evaluate("""() => { const h = document.getElementById('pc-hunt');
|
||
return { shown: h && h.style.display !== 'none', n: window.PROCITY.game.collection.length,
|
||
nl: window.PROCITY.game.listings.length }; }""")
|
||
r1 = _enter_record_shop(pg)
|
||
if r1.get('err'): FAIL(f"first-five/return: {r1['err']}")
|
||
else:
|
||
_at_bin(pg)
|
||
pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }))")
|
||
pg.wait_for_timeout(350)
|
||
tr = pg.evaluate("""() => { const t = document.getElementById('pc-toast');
|
||
return { text: t ? t.textContent : '', vis: t ? t.style.opacity : '0' }; }""")
|
||
if not rd['shown'] and not ('②' in tr['text'] and tr['vis'] == '1'):
|
||
OK(f"first-five/return: a save with {rd['n']} find(s)/{rd['nl']} listing(s) gets no tutorial")
|
||
else:
|
||
FAIL(f"first-five/return: tutorial leaked to a returning save — {rd} {tr}")
|
||
# (b) determinism: a second FRESH browser hunts the same artist
|
||
b2, pg2, _ = new_page(p)
|
||
try:
|
||
boot(pg2, '')
|
||
w2 = pg2.evaluate("() => window.PROCITY.game.wants.map(w => w.artist)")
|
||
if w2 == fa['wants']: OK(f"first-five: a second fresh browser hunts the same {w2[0]} — seeded, not random")
|
||
else: FAIL(f"first-five: fresh want differs across browsers — {fa['wants']} vs {w2}")
|
||
finally:
|
||
b2.close()
|
||
# (e) ?game=0 — no hunt line
|
||
boot(pg, 'game=0')
|
||
g0 = pg.evaluate("""() => { const h = document.getElementById('pc-hunt');
|
||
return h ? h.style.display !== 'none' : false; }""")
|
||
if not g0: OK('first-five/control: ?game=0 — no hunt, no tutorial')
|
||
else: FAIL('first-five/control: game=0 shows the hunt line')
|
||
if errs: FAIL(f"first-five: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
else: OK('first-five: 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
|
||
instrument must SEE calls, and the game DOM must exist) — without it a broken instrument passes
|
||
forever. Plus the plan delta: game layer on vs off moves ZERO plan bytes."""
|
||
head('GATE R30c: classic purity (instrumented Storage calls · zero game DOM · plan fp game-on == game-off)')
|
||
res = {}
|
||
for q, name in (('classic=1', 'classic'), ('game=0', 'game0'), ('', 'default')):
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
pg.add_init_script(STORAGE_INSTRUMENT)
|
||
boot(pg, q)
|
||
if name == 'default':
|
||
try: pg.wait_for_function("() => !!document.getElementById('pc-game')", timeout=8000)
|
||
except Exception: pass
|
||
r = pg.evaluate("""(ids) => ({
|
||
count: window.__storageCalls.length, first: window.__storageCalls.slice(0, 6),
|
||
saveTouched: window.__storageCalls.some(c => c.includes('procity-save')),
|
||
game: !!(window.PROCITY && window.PROCITY.game),
|
||
dom: ids.filter(id => !!document.getElementById(id)),
|
||
})""", GAME_DOM_IDS)
|
||
r['fp'] = pg.evaluate(JS_PLAN_FP)
|
||
r['errs'] = list(errs)
|
||
res[name] = r
|
||
finally:
|
||
b.close()
|
||
for name in ('classic', 'game0'):
|
||
r = res[name]
|
||
if r['count'] == 0 and not r['game'] and not r['dom']:
|
||
OK(f"?{'classic=1' if name == 'classic' else 'game=0'}: ZERO Storage calls · game null · zero game DOM")
|
||
else:
|
||
FAIL(f"?{name}: storage calls {r['count']} (first {r['first']}), game={r['game']}, DOM leak {r['dom']}")
|
||
if r['errs']: FAIL(f"?{name}: {len(r['errs'])} console error(s); first: {r['errs'][0][:140]}")
|
||
d = res['default']
|
||
if d['count'] > 0 and d['saveTouched'] and d['game']:
|
||
OK(f"positive control: default boot — instrument SAW {d['count']} Storage call(s) incl. procity-save, game live")
|
||
else:
|
||
FAIL(f"positive control BROKEN: default boot count {d['count']}, saveTouched {d['saveTouched']}, game {d['game']}"
|
||
" — the zero readings above are unproven")
|
||
if d['dom'] == GAME_DOM_IDS:
|
||
OK('positive control: all 7 game DOM ids present on the default boot')
|
||
else:
|
||
WARN(f"positive control: game DOM partial on default boot — {d['dom']} (headless rAF timing?)")
|
||
if res['game0']['fp'] == d['fp']:
|
||
OK(f"plan fp game-on == game-off ({d['fp']:#010x}) — the game layer moves ZERO plan bytes")
|
||
else:
|
||
FAIL(f"plan fp DIFFERS with the game layer: game0 {res['game0']['fp']:#010x} vs default {d['fp']:#010x}")
|
||
print(f" (?classic plan fp {res['classic']['fp']:#010x} differs by design — gig-less plan; its golden is classic_regression's)")
|
||
|
||
|
||
def smoke_rotation(p):
|
||
"""R30 gate (d) — rotation determinism over VERTEX DATA (the wave-3 binding): same (seed, day) →
|
||
byte-identical stock fp (re-entry AND across a reboot); day 1→2→3 each actually different;
|
||
?game=0 == day 1 (the day-1 convention). Subject proven present: bytes and mesh count printed."""
|
||
head('GATE R30d: rotation determinism (vertex-data fp · same day byte-identical · day N ≠ N+1 · cross-boot)')
|
||
b, pg, errs = new_page(p)
|
||
shop_id = None; fps = {}
|
||
try:
|
||
boot(pg, '')
|
||
r0 = _enter_record_shop(pg)
|
||
if r0.get('err'): FAIL(f"rotation: {r0['err']}"); return
|
||
shop_id = r0['shop']
|
||
|
||
def fp_of(day_label):
|
||
r = pg.evaluate(JS_VERTEX_FP)
|
||
if r['bytes'] == 0 or r['meshes'] == 0:
|
||
FAIL(f"rotation: VACUOUS — {day_label}: 0 bytes / 0 stock meshes hashed (no subject)"); return None
|
||
return r
|
||
|
||
def reenter():
|
||
pg.evaluate("(id) => { window.DBG.exitShop(); }", shop_id); pg.wait_for_timeout(150)
|
||
pg.evaluate("(id) => { window.DBG.setSegment(2); window.DBG.enterShop(id); }", shop_id)
|
||
pg.wait_for_timeout(150)
|
||
|
||
a1 = fp_of('day1'); reenter(); a2 = fp_of('day1 re-entry')
|
||
if not a1 or not a2: return
|
||
if a1['fp'] == a2['fp']: OK(f"day 1: re-entry byte-identical (fp {a1['fp']:#010x}, {a1['bytes']:,} bytes / {a1['meshes']} meshes)")
|
||
else: FAIL(f"day 1: re-entry fp moved {a1['fp']:#010x} → {a2['fp']:#010x} — same (seed, day) not deterministic")
|
||
pg.evaluate("() => { window.DBG.exitShop(); window.PROCITY.game.sleep(); }")
|
||
reenter(); b1 = fp_of('day2'); reenter(); b2 = fp_of('day2 re-entry')
|
||
if not b1 or not b2: return
|
||
if b1['fp'] != a1['fp']: OK(f"day 2 ≠ day 1: stock ROTATED ({a1['fp']:#010x} → {b1['fp']:#010x})")
|
||
else: FAIL(f"day 2 == day 1: fp {b1['fp']:#010x} — rotation is not happening (or the fp is vacuous)")
|
||
if b1['fp'] == b2['fp']: OK(f"day 2: re-entry byte-identical ({b1['fp']:#010x})")
|
||
else: FAIL(f"day 2: re-entry fp moved — not deterministic within the day")
|
||
pg.evaluate("() => { window.DBG.exitShop(); window.PROCITY.game.sleep(); }")
|
||
reenter(); c1 = fp_of('day3')
|
||
if not c1: return
|
||
if c1['fp'] != b1['fp'] and c1['fp'] != a1['fp']:
|
||
OK(f"day 3 ≠ day 2 ≠ day 1 ({c1['fp']:#010x}) — each day is its own crate")
|
||
else:
|
||
FAIL(f"day 3 fp {c1['fp']:#010x} collides (day1 {a1['fp']:#010x}, day2 {b1['fp']:#010x})")
|
||
pg.evaluate("() => window.PROCITY.game.save()")
|
||
fps = { 'day1': a1['fp'], 'day3': c1['fp'] }
|
||
# cross-boot: reload the SAME browser context (localStorage carries day 3) — same (seed, day) again
|
||
boot(pg, '')
|
||
day = pg.evaluate("() => window.PROCITY.game.day")
|
||
if day != 3: FAIL(f"rotation: reboot loaded day {day} (want 3 — the save didn't carry)"); return
|
||
pg.evaluate("(id) => { window.DBG.setSegment(2); window.DBG.enterShop(id); }", shop_id)
|
||
pg.wait_for_timeout(150)
|
||
r = pg.evaluate(JS_VERTEX_FP)
|
||
if r['fp'] == fps['day3']: OK(f"cross-boot: day-3 fp byte-identical after a full reboot ({r['fp']:#010x})")
|
||
else: FAIL(f"cross-boot: day-3 fp {r['fp']:#010x} != pre-reboot {fps['day3']:#010x} — rotation not seed-stable")
|
||
if errs: FAIL(f"rotation: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
if not fps: return
|
||
# the day-1 convention: a ?game=0 boot's stock is byte-identical to a fresh game's day 1
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'game=0')
|
||
pg.evaluate("(id) => { window.DBG.setSegment(2); window.DBG.enterShop(id); }", shop_id)
|
||
pg.wait_for_timeout(150)
|
||
r = pg.evaluate(JS_VERTEX_FP)
|
||
if r['fp'] == fps['day1']: OK(f"?game=0 == day 1 ({r['fp']:#010x}) — the pre-v7 town IS day 1, measured")
|
||
else: FAIL(f"?game=0 fp {r['fp']:#010x} != day-1 fp {fps['day1']:#010x} — the day-1 convention broke")
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════════════════════════════════
|
||
# [R38 — v8 WAVE 1, INHABITATION] THE FOUR GATES (Lane F)
|
||
#
|
||
# Four content systems landed in one round (Lane B `4514cb9`, Lane E `1568c9b`). Each gate below
|
||
# ships its FALSIFIABILITY CONTROL demonstrated in the same run, because this house has now twice
|
||
# shipped a mechanism that produced a constant and gated green on it:
|
||
# · R37 killed two vacuous gates (`smoke_tram`'s single assertion that was TRUE on the fenced path;
|
||
# the classic covenant's total absence of a network assertion);
|
||
# · R38's own §1.3 season mechanism was keyed on `citySeed` alone, made **24 of 24 towns a constant
|
||
# 214 days from swooping season**, and would have gated green because `?magpie=1` forces the
|
||
# state. Lane B caught it in its own sweep. Every arm here therefore either (a) flips a subject
|
||
# between two boots that differ in exactly one thing, or (b) asserts a NEGATIVE that a broken
|
||
# build would satisfy — never "the object exists".
|
||
#
|
||
# The house rule these four obey, in one line: **a gate that cannot fail is not a gate.**
|
||
# ══════════════════════════════════════════════════════════════════════════════════════════════════
|
||
|
||
R38_AMB_KEYS = ['dog-bark-fence', 'mower-distant', 'radio-through-window', 'sprinkler',
|
||
'roller-door', 'magpie-carol']
|
||
R38_V1_GROUND = {'road': 'djsim-road', 'foot': 'djsim-footpath', 'base': 'grass', 'plaza': 'brickpave'}
|
||
R38_V1_KERB = 0x9a948c
|
||
R38_ALT_PORT = PORT + 41 # the patched-manifest control server (its own port, its own root)
|
||
|
||
|
||
def _r38_patched_root(mutate):
|
||
"""A second web root that is `web/` symlink-for-symlink EXCEPT `assets/manifest.json`, which is a
|
||
patched copy. Everything else — index.html, every module, every byte of every asset — is the same
|
||
inode, so the only difference between the two servers is the one manifest key `mutate` changes.
|
||
That is what makes the control's verdict attributable."""
|
||
import tempfile
|
||
d = pathlib.Path(tempfile.mkdtemp(prefix='procity-r38-ctl-'))
|
||
src = ROOT / 'web'
|
||
for e in src.iterdir():
|
||
if e.name != 'assets':
|
||
(d / e.name).symlink_to(e)
|
||
a = d / 'assets'; a.mkdir()
|
||
for e in (src / 'assets').iterdir():
|
||
if e.name != 'manifest.json':
|
||
(a / e.name).symlink_to(e)
|
||
man = json.loads((src / 'assets' / 'manifest.json').read_text())
|
||
mutate(man)
|
||
(a / 'manifest.json').write_text(json.dumps(man, indent=1))
|
||
return d
|
||
|
||
|
||
def _r38_serve(root, port):
|
||
proc = subprocess.Popen(['python3', '-m', 'http.server', str(port)], cwd=str(root),
|
||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||
for _ in range(60):
|
||
if port_up(port): return proc
|
||
time.sleep(0.1)
|
||
proc.terminate()
|
||
return None
|
||
|
||
|
||
def _r38_boot_at(pg, host, query):
|
||
pg.goto(f'{host}/index.html?seed={SEED}&dbg=1' + (('&' + query) if query else ''))
|
||
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=25000)
|
||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||
try:
|
||
pg.wait_for_function("() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)", timeout=12000)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
# The five ground meshes, hashed over VERTEX + UV bytes (FNV-1a, the JS_VERTEX_FP shape) plus the
|
||
# material identity. §1.2's whole claim is "+0 draws / +0 tris and classic is byte-identical BY
|
||
# CONSTRUCTION" — a hash of the geometry and a read of the shared material is exactly that claim.
|
||
JS_R38_GROUND = """() => {
|
||
const P = window.PROCITY, g = P.scene.getObjectByName('ground');
|
||
const c = P.character || null;
|
||
const fnv = (arr) => { let h = 0x811c9dc5 >>> 0;
|
||
const u = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
||
for (let i = 0; i < u.length; i++) { h = (h ^ u[i]) >>> 0; h = Math.imul(h, 0x01000193) >>> 0; }
|
||
return h >>> 0; };
|
||
const meshes = [];
|
||
if (g) g.traverse((o) => {
|
||
if (!o.isMesh || !o.geometry) return;
|
||
const pos = o.geometry.getAttribute('position'), uv = o.geometry.getAttribute('uv');
|
||
let h = 0x811c9dc5 >>> 0;
|
||
if (pos) h = (h ^ fnv(pos.array)) >>> 0;
|
||
if (uv) h = (h ^ fnv(uv.array)) >>> 0;
|
||
const m = o.material;
|
||
const src = (m && m.map && m.map.image && (m.map.image.src || m.map.image.currentSrc)) || null;
|
||
meshes.push({ fp: h >>> 0, color: m && m.color ? '#' + m.color.getHexString() : null,
|
||
map: src ? String(src).split('/').pop() : null,
|
||
tris: pos ? (o.geometry.index ? o.geometry.index.count : pos.count) / 3 : 0 });
|
||
});
|
||
return {
|
||
townKey: P.townKey || null,
|
||
character: c ? { key: c.key, label: c.label, matched: !!c.matched,
|
||
ground: Object.assign({}, c.ground), tint: Object.assign({}, c.tint),
|
||
kerb: c.kerb, sky: c.sky } : null,
|
||
meshes, meshCount: meshes.length,
|
||
};
|
||
}"""
|
||
|
||
JS_R38_AMB = """() => {
|
||
const A = window.PROCITY.audio;
|
||
const a = A && A.state && A.state.ambient;
|
||
if (!a) return null;
|
||
const out = { panning: !!a.panning, off: !!a.off, ready: !!(A.state && A.state.ready), sources: {} };
|
||
for (const k of Object.keys(a.sources || {})) {
|
||
const s = a.sources[k];
|
||
out.sources[k] = { anchors: s.anchors, status: s.status, dist: s.dist, gain: s.gain,
|
||
pan: s.pan, plays: s.plays };
|
||
}
|
||
return out;
|
||
}"""
|
||
|
||
|
||
def _r38_amb(pg):
|
||
return pg.evaluate(JS_R38_AMB)
|
||
|
||
|
||
def _r38_home_lots(pg, n=34):
|
||
"""A deterministic spread of house/yard lot centres — the anchor pool §1.1 and §1.4 both derive
|
||
from. Returned in plan order, evenly sampled, so both the treatment and the control walk the
|
||
SAME list."""
|
||
return pg.evaluate("""(n) => {
|
||
const L = (window.PROCITY.plan.lots || []).filter((l) => l.use === 'house' || l.use === 'yard');
|
||
if (!L.length) return [];
|
||
const out = []; const step = Math.max(1, Math.floor(L.length / n));
|
||
for (let i = 0; i < L.length && out.length < n; i += step) out.push([L[i].x, L[i].z]);
|
||
return out;
|
||
}""", n)
|
||
|
||
|
||
def _r38_probe_poses(pg, poses):
|
||
"""Walk the pose list once, recording the closest approach to every §1.1 source. One pass (the
|
||
engine re-scans nearest-anchor every 0.33 s) that serves the whole gate, so the per-source
|
||
assertions below all stand at a position where the subject is genuinely in range."""
|
||
best = {k: (1e9, None) for k in R38_AMB_KEYS}
|
||
for (x, z) in poses:
|
||
pg.evaluate("([x, z]) => window.DBG.teleport(x, z, 0)", [x, z])
|
||
pg.wait_for_timeout(420)
|
||
st = _r38_amb(pg)
|
||
if not st: continue
|
||
for k, s in st['sources'].items():
|
||
d = s.get('dist')
|
||
if d is not None and d < best.get(k, (1e9, None))[0]:
|
||
best[k] = (d, (x, z))
|
||
return best
|
||
|
||
|
||
def _r38_wait_status(pg, key, want, timeout_s=22, poll=0.5):
|
||
"""Poll one source's status until it is in `want`. Returns the final reading."""
|
||
t0 = time.time()
|
||
last = None
|
||
while time.time() - t0 < timeout_s:
|
||
st = _r38_amb(pg)
|
||
last = st['sources'].get(key) if st else None
|
||
if last and last['status'] in want:
|
||
return last
|
||
pg.wait_for_timeout(int(poll * 1000))
|
||
return last
|
||
|
||
|
||
def smoke_ambient(p):
|
||
"""[R38 §1.1] THE AUDIO INHABITATION LAYER — six new mixer layers, zero draws.
|
||
|
||
Arms, each with its control:
|
||
1. six sources resolve against the SHIPPED manifest (not a table of hopes) and all six anchor
|
||
on the default boot;
|
||
2. THE FALSIFIABILITY CONTROL, kept verbatim from Lane B: a second server whose ONLY difference
|
||
is `audio.sfx['dog-bark-fence']` renamed → that source must report `no-entry`, 0 plays, and
|
||
must NEVER reach `audible`, while the other five read exactly what they read unpatched;
|
||
3. HOURS is a real gate — the sprinkler ([6,10)) flips audible→idle between seg 0 (06:30) and
|
||
seg 2 (12:30) at the SAME position;
|
||
4. DAY-OF-WEEK is a real gate — the mower (weekend-only) flips idle→audible between day 1
|
||
(Monday) and day 6 (Saturday) at the same position and the same segment;
|
||
5. PANNING is a real bearing, not a constant — the pan SIGN FLIPS when the camera turns 180°;
|
||
6. ZERO DRAWS is structural — `?ambient=0` and the default boot read the same draw count at a
|
||
fixed pose with the citizen sim quiet.
|
||
|
||
WHAT THIS GATE CANNOT DO, said plainly: it cannot confirm that a human would HEAR anything.
|
||
The autoplay policy needs a trusted gesture and headless output goes nowhere; everything
|
||
downstream of the unlock (manifest resolve → fetch → decode → layer gain → pan → one-shot
|
||
scheduling → play counters) is measured, and the acoustics are not. Nobody in this loop has ears.
|
||
"""
|
||
head('GATE R38 §1.1: the audio inhabitation layer (6 point sources · hours · day · pan · the patched-manifest control)')
|
||
b, pg, errs = new_page(p)
|
||
ref = None; poses = None; best = None
|
||
try:
|
||
boot(pg, '')
|
||
# A real Input-domain KEY press: the autoplay unlock (audio.js listens for pointerdown OR
|
||
# keydown). Deliberately not a click — a click on the canvas also requests Pointer Lock, which
|
||
# the automation browser refuses, and the resulting console error is an artifact of the probe
|
||
# rather than of the build (R30/R37 recorded the same four).
|
||
pg.keyboard.press('Shift')
|
||
pg.wait_for_timeout(500)
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
st = _r38_amb(pg)
|
||
if not st:
|
||
FAIL('§1.1: no PROCITY.audio.state.ambient on the default boot — the layer did not construct')
|
||
return
|
||
if st.get('off'):
|
||
FAIL('§1.1: the ambient layer reports off on the DEFAULT boot (it is default-on)')
|
||
return
|
||
got = sorted(st['sources'].keys())
|
||
if got == sorted(R38_AMB_KEYS):
|
||
OK(f"six point sources published: {' '.join(R38_AMB_KEYS)}")
|
||
else:
|
||
FAIL(f"§1.1: source table is {got}, want {sorted(R38_AMB_KEYS)}")
|
||
return
|
||
OK(f"StereoPannerNode available: panning={st['panning']} (mono fail-soft says so in the same field)")
|
||
|
||
# ── arm 1: every key resolves against the manifest that actually shipped ──
|
||
man = json.loads((ROOT / 'web' / 'assets' / 'manifest.json').read_text())
|
||
banks = man.get('audio', {})
|
||
missing = [k for k in R38_AMB_KEYS
|
||
if not any(k in banks.get(bk, {}) for bk in ('sfx', 'ambience', 'music'))]
|
||
if missing: FAIL(f"§1.1: manifest has no entry for {missing} — the table names sounds nobody shipped")
|
||
else: OK('all six keys resolve in the shipped manifest (Lane E `1568c9b`)')
|
||
noent = [k for k, s in st['sources'].items() if s['status'] == 'no-entry']
|
||
if noent: FAIL(f"§1.1: {noent} report no-entry on the UNPATCHED boot")
|
||
else: OK('no source reports no-entry unpatched — the control below therefore means something')
|
||
zero = [k for k, s in st['sources'].items() if not s['anchors']]
|
||
if zero: FAIL(f"§1.1: {zero} anchored NOWHERE on the default boot (the magpie-carol running-counter bug's shape)")
|
||
else: OK('all six anchored on the default boot: '
|
||
+ ' · '.join(f"{k} {st['sources'][k]['anchors']}" for k in R38_AMB_KEYS))
|
||
|
||
# ── position once, use everywhere ──
|
||
poses = _r38_home_lots(pg)
|
||
if len(poses) < 8:
|
||
FAIL(f"§1.1: only {len(poses)} house/yard lots on the default boot — the pose search is vacuous")
|
||
return
|
||
best = _r38_probe_poses(pg, poses)
|
||
OK('closest approach per source over %d residential poses: %s' % (
|
||
len(poses), ' · '.join(f"{k} {best[k][0]:.0f}m" for k in R38_AMB_KEYS)))
|
||
|
||
# ── arm 2 (positive control for the whole gate): ONE source reaches `audible` ──
|
||
# For a one-shot, `audible` is only published once `plays > 0` (audio.js), so this single
|
||
# assertion covers fetch, decode, gain, scheduling and the play counter in one.
|
||
dpos = best['dog-bark-fence'][1]
|
||
if not dpos:
|
||
FAIL('§1.1: no pose ever came within range of a dog anchor')
|
||
else:
|
||
pg.evaluate("([x, z]) => window.DBG.teleport(x, z, 0)", list(dpos))
|
||
r = _r38_wait_status(pg, 'dog-bark-fence', {'audible'}, timeout_s=26)
|
||
if r and r['status'] == 'audible' and r['plays'] > 0:
|
||
OK(f"positive control: dog-bark-fence AUDIBLE at {r['dist']} m — gain {r['gain']}, "
|
||
f"pan {r['pan']}, {r['plays']} play(s) scheduled")
|
||
ref = _r38_amb(pg)
|
||
else:
|
||
FAIL(f"§1.1: dog-bark-fence never reached audible in 26 s at {best['dog-bark-fence'][0]:.0f} m "
|
||
f"(status {r and r['status']}, plays {r and r['plays']}) — the whole layer is unproven")
|
||
|
||
# ── arm 5: the pan is a BEARING, not a constant ──
|
||
if dpos:
|
||
# FOUR bearings, not two. An anchor that happens to sit on the forward axis reads ~0 at
|
||
# yaw 0 AND at yaw 180 — a two-bearing arm would call a working panner inconclusive (it
|
||
# did, on the first run of this gate). Turning through all four quadrants means the
|
||
# anchor is hard right at one of them and hard left at another, whatever its bearing.
|
||
pans = []
|
||
for k in range(4):
|
||
pg.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [dpos[0], dpos[1], k * 1.5707963])
|
||
pg.wait_for_timeout(600)
|
||
pans.append(_r38_amb(pg)['sources']['dog-bark-fence']['pan'])
|
||
if max(pans) > 0.25 and min(pans) < -0.25:
|
||
OK('pan is a real bearing, not a constant: yaw 0/90/180/270° → '
|
||
+ ' '.join(f'{v:+.2f}' for v in pans)
|
||
+ ' (hard right at one bearing, hard left at another)')
|
||
else:
|
||
FAIL('pan does not track the camera: yaw 0/90/180/270° → '
|
||
+ ' '.join(f'{v:+.2f}' for v in pans)
|
||
+ ' — the StereoPanner is not driven by the bearing-to-anchor')
|
||
|
||
# ── arm 3: HOURS is a real gate (sprinkler [6,10)) ──
|
||
spos = best['sprinkler'][1]
|
||
if spos and best['sprinkler'][0] < 15:
|
||
pg.evaluate("([x, z]) => { window.DBG.setSegment(0); window.DBG.teleport(x, z, 0); }", list(spos))
|
||
on = _r38_wait_status(pg, 'sprinkler', {'audible', 'loading'}, timeout_s=10)
|
||
pg.evaluate("([x, z]) => { window.DBG.setSegment(2); window.DBG.teleport(x, z, 0); }", list(spos))
|
||
off = _r38_wait_status(pg, 'sprinkler', {'idle'}, timeout_s=10)
|
||
if on and on['status'] in ('audible', 'loading') and off and off['status'] == 'idle':
|
||
OK(f"HOURS arm: sprinkler {on['status']} at 06:30 → idle at 12:30, same pose ({on['dist']} m) "
|
||
'— the [6,10) window is enforced, not decorative')
|
||
else:
|
||
FAIL(f"§1.1 hours arm: sprinkler 06:30 {on and on['status']} / 12:30 {off and off['status']} "
|
||
'— the hours rule did not change the state')
|
||
else:
|
||
FAIL(f"§1.1 hours arm: no pose within the sprinkler's 15 m radius "
|
||
f"(closest {best['sprinkler'][0]:.0f} m) — arm SKIPPED, and a skipped arm is not a pass")
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
|
||
# ── arm 4: DAY OF WEEK is a real gate (mower = weekend only; dow = (day-1)%7) ──
|
||
mpos = best['mower-distant'][1]
|
||
if mpos and best['mower-distant'][0] < 95:
|
||
pg.evaluate("([x, z]) => window.DBG.teleport(x, z, 0)", list(mpos))
|
||
wk = _r38_wait_status(pg, 'mower-distant', {'idle'}, timeout_s=8)
|
||
day = pg.evaluate("() => window.PROCITY.game && window.PROCITY.game.day")
|
||
for _ in range(8):
|
||
d = pg.evaluate("() => { window.PROCITY.game.sleep(); return window.PROCITY.game.day; }")
|
||
pg.wait_for_timeout(120)
|
||
if (d - 1) % 7 == 5: break # Saturday
|
||
d2 = pg.evaluate("() => window.PROCITY.game.day")
|
||
pg.evaluate("([x, z]) => { window.DBG.setSegment(2); window.DBG.teleport(x, z, 0); }", list(mpos))
|
||
we = _r38_wait_status(pg, 'mower-distant', {'audible', 'loading'}, timeout_s=14)
|
||
if wk and wk['status'] == 'idle' and we and we['status'] in ('audible', 'loading'):
|
||
OK(f"DAY arm: mower idle on day {day} (dow {(day-1)%7}, a weekday) → {we['status']} on day {d2} "
|
||
f"(dow {(d2-1)%7}, Saturday) at the same pose — the local dowOf() rule is enforced")
|
||
else:
|
||
FAIL(f"§1.1 day arm: mower day {day} {wk and wk['status']} → day {d2} {we and we['status']} "
|
||
'— weekend-only did not change the state')
|
||
else:
|
||
FAIL(f"§1.1 day arm: no pose within the mower's 95 m radius (closest {best['mower-distant'][0]:.0f} m) — arm SKIPPED")
|
||
|
||
real = [e for e in errs if 'PointerLock' not in e and 'Pointer Lock' not in e]
|
||
if real: FAIL(f"§1.1: {len(real)} console error(s) on the default boot; first: {real[0][:140]}")
|
||
else: OK('0 console errors on the default boot (Pointer Lock refusals excluded — the automation '
|
||
'browser, recorded R30/R37)')
|
||
finally:
|
||
b.close()
|
||
|
||
# ══ arm 6: ZERO DRAWS, and it is structural. Sim quiet on BOTH sides (?roster=v1&pop=0) so the
|
||
# comparison is of the audio layer, not of how many peds happened to spawn on wall-clock time —
|
||
# the trap R37 named and R38 reproduced.
|
||
draws = {}
|
||
for label, q in (('default', 'roster=v1&pop=0'), ('?ambient=0', 'ambient=0&roster=v1&pop=0')):
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, q)
|
||
r = pg.evaluate("() => { window.DBG.setSegment(2); return window.DBG.teleport(0, 0, 0); }")
|
||
draws[label] = (r['drawCalls'], r['tris'])
|
||
finally:
|
||
b.close()
|
||
if draws['default'][0] == draws['?ambient=0'][0] and draws['default'][1] == draws['?ambient=0'][1]:
|
||
OK(f"ZERO DRAWS confirmed: default {draws['default'][0]} draws / {draws['default'][1]:,} tris "
|
||
'== ?ambient=0, byte for byte (nothing in §1.1 reaches the scene graph)')
|
||
else:
|
||
FAIL(f"§1.1 claims zero draws but default {draws['default']} != ?ambient=0 {draws['?ambient=0']}")
|
||
|
||
# ══ arm 2b: THE FALSIFIABILITY CONTROL — kept verbatim ═══════════════════════════════════════
|
||
# One manifest key renamed. Nothing else on the disk differs (symlinked root). That source must
|
||
# report `no-entry`, must never reach `audible`, must have 0 plays; the other five must read
|
||
# exactly what they read unpatched. A missing key is silence by house law, so `status` is the
|
||
# ONLY thing that can tell a missing key from a quiet street — which is why the field exists.
|
||
if ref is None or not poses:
|
||
FAIL('§1.1: the unpatched reference reading was never taken — the control cannot be attributed')
|
||
return
|
||
def _rename_dog(m):
|
||
m['audio']['sfx']['dog-bark-fence-MISSING'] = m['audio']['sfx'].pop('dog-bark-fence')
|
||
root = _r38_patched_root(_rename_dog)
|
||
proc = _r38_serve(root, R38_ALT_PORT)
|
||
if not proc:
|
||
FAIL(f"§1.1 control: could not start the patched-manifest server on :{R38_ALT_PORT}")
|
||
return
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
_r38_boot_at(pg, f'http://127.0.0.1:{R38_ALT_PORT}', '')
|
||
pg.keyboard.press('Shift')
|
||
pg.wait_for_timeout(400)
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
dpos = best['dog-bark-fence'][1]
|
||
pg.evaluate("([x, z]) => window.DBG.teleport(x, z, 0)", list(dpos))
|
||
r = _r38_wait_status(pg, 'dog-bark-fence', {'no-entry'}, timeout_s=12)
|
||
st2 = _r38_amb(pg)
|
||
d = st2['sources']['dog-bark-fence']
|
||
if d['status'] == 'no-entry' and d['plays'] == 0:
|
||
OK(f"CONTROL FIRES: one manifest key renamed → dog-bark-fence reports no-entry, {d['plays']} plays, "
|
||
f"anchors still {d['anchors']} (it is the SOUND that is missing, not the street)")
|
||
else:
|
||
FAIL(f"§1.1 CONTROL DID NOT FIRE: with the key renamed dog-bark-fence reads status "
|
||
f"{d['status']}, plays {d['plays']} — this gate cannot distinguish a shipped sound from a missing one")
|
||
# the other five must be untouched
|
||
drift = [k for k in R38_AMB_KEYS if k != 'dog-bark-fence'
|
||
and st2['sources'][k]['anchors'] != ref['sources'][k]['anchors']]
|
||
if drift: FAIL(f"§1.1 control: renaming one key moved the anchor counts of {drift} — the sources are coupled")
|
||
else: OK('the other five sources are unchanged by the patch (anchor counts identical) — the control is attributable')
|
||
if any(s['status'] == 'audible' for k, s in st2['sources'].items() if k == 'dog-bark-fence'):
|
||
FAIL('§1.1 control: the renamed source reached audible')
|
||
real = [e for e in errs if 'PointerLock' not in e and 'Pointer Lock' not in e]
|
||
if real: FAIL(f"§1.1 control: {len(real)} console error(s) — a missing key must be SILENT, not an "
|
||
f"error; first: {real[0][:140]}")
|
||
else: OK('a missing key is silence, not an error: 0 console errors on the patched boot (house audio law 1)')
|
||
finally:
|
||
b.close()
|
||
proc.terminate()
|
||
import shutil; shutil.rmtree(root, ignore_errors=True)
|
||
|
||
# ── scope + classic, stated rather than assumed ──
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, 'classic=1')
|
||
st = _r38_amb(pg)
|
||
if st is None or st.get('off'):
|
||
OK('?classic=1: the ambient layer is OFF (no sources constructed) — the covenant boot never hears it')
|
||
else:
|
||
FAIL(f"§1.1: ?classic=1 constructed the ambient layer ({st})")
|
||
finally:
|
||
b.close()
|
||
print(' \033[36m[record]\033[0m audible OUTPUT is NOT verified here and cannot be headlessly: the autoplay '
|
||
'policy needs a trusted gesture and nothing plays to a speaker. Everything downstream of the unlock '
|
||
'is measured above. Nobody in this loop has ears.')
|
||
|
||
|
||
def smoke_character(p):
|
||
"""[R38 §1.2] THE PER-TOWN CHARACTER VECTOR — +0 draws, +0 tris, classic byte-identical.
|
||
|
||
Arms:
|
||
1. CLASSIC BY CONSTRUCTION — the default boot's character is the frozen v1 literals, every
|
||
ground material is untinted (#ffffff), the kerb is 0x9a948c, and the ground is still exactly
|
||
5 meshes. No `if (classic)` is needed because a boot with no town key HAS no character.
|
||
2. THE VACUOUS ARM IS PER-TOWN, not per-corpus — every one of the 23 shipped town keys has its
|
||
own entry, INCLUDING the two carrying `state: ''` (`newtown_godverse`, `redhill_godverse`),
|
||
which a `state`-keyed implementation would have dropped silently. Measured against the
|
||
shipped index, not asserted.
|
||
3. THE CONTROL: an unknown key must return `matched:false`. Without this arm, "matched is true
|
||
for all 23" is a claim that a `matched: true` constant would satisfy.
|
||
4. NOT VACUOUS AS A TABLE — the 23 towns must not all carry the SAME palette (a character
|
||
vector that gives every town identical ground is the §1.3 season bug wearing a hat).
|
||
5. THE TWO ORPHANS ARE ON THE GPU — `gravel` (Castlemaine) and `reddust` (Darwin, both Red
|
||
Hills) were selected by NOTHING since v1. Asserted at the bound material's texture URL on a
|
||
live boot, not at the table.
|
||
"""
|
||
head('GATE R38 §1.2: the per-town character vector (classic by construction · per-town arm · the two orphans on the GPU)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
g = pg.evaluate(JS_R38_GROUND)
|
||
c = g['character']
|
||
if not c:
|
||
FAIL('§1.2: PROCITY.character is absent on the default boot'); return
|
||
if c['matched'] is False and c['key'] is None and c['ground'] == R38_V1_GROUND \
|
||
and all(v is None for v in c['tint'].values()) and c['kerb'] == R38_V1_KERB and c['sky'] is None:
|
||
OK('classic BY CONSTRUCTION: default boot → frozen v1 literals '
|
||
f"({c['ground']['base']}/{c['ground']['road']}/{c['ground']['foot']}/{c['ground']['plaza']}), "
|
||
'all four tints null, kerb 0x9a948c, sky null')
|
||
else:
|
||
FAIL(f"§1.2: the default boot's character is NOT the frozen literals: {c}")
|
||
tinted = [m for m in g['meshes'] if m['color'] and m['color'].lower() not in ('#ffffff',)]
|
||
# the kerb mesh is a flat colour by design (0x9a948c) — it is the only non-white one
|
||
tinted = [m for m in tinted if m['color'].lower() != '#9a948c']
|
||
if not tinted:
|
||
OK(f"the default boot's {g['meshCount']} ground meshes are all untinted (#ffffff) + the 0x9a948c kerb "
|
||
'— the shared material is the same object v1 built')
|
||
else:
|
||
FAIL(f"§1.2: default boot ground carries tints {[m['color'] for m in tinted]} — classic is not byte-identical")
|
||
if g['meshCount'] == 5:
|
||
OK('ground is still exactly 5 town-wide meshes (the +0-draw mechanism, re-measured)')
|
||
else:
|
||
FAIL(f"§1.2: ground has {g['meshCount']} meshes, want 5 — the palette swap added draws")
|
||
print(f" \033[36m[record]\033[0m default-boot ground fp: "
|
||
+ ' / '.join(f"{m['fp']:08x}" for m in g['meshes']))
|
||
|
||
# ── arms 2/3/4: the table itself, over the SHIPPED index ──
|
||
idx = json.loads((ROOT / 'web' / 'assets' / 'towns' / 'index.json').read_text())
|
||
towns = idx['towns'] if isinstance(idx, dict) else idx
|
||
keys = [t['key'] for t in towns]
|
||
blank = [t['key'] for t in towns if not (t.get('state') or '').strip()]
|
||
r = pg.evaluate("""async (keys) => {
|
||
const m = await import('./js/world/character.js');
|
||
const out = {};
|
||
for (const k of keys) {
|
||
const c = m.townCharacter(k);
|
||
out[k] = { matched: !!c.matched, label: c.label, sig: JSON.stringify([c.ground, c.tint, c.sky]) };
|
||
}
|
||
const bad = m.townCharacter('__no_such_town_r38');
|
||
out.__control = { matched: !!bad.matched, label: bad.label, sig: JSON.stringify([bad.ground, bad.tint, bad.sky]) };
|
||
out.__null = { matched: !!m.townCharacter(null).matched };
|
||
return out;
|
||
}""", keys)
|
||
unmatched = [k for k in keys if not r[k]['matched']]
|
||
if unmatched: FAIL(f"§1.2: {len(unmatched)} shipped town key(s) have NO entry: {unmatched}")
|
||
else: OK(f"all {len(keys)} shipped town keys carry their own entry (per-town, not per-corpus)")
|
||
if all(r[k]['matched'] for k in blank):
|
||
OK(f"the per-corpus trap is closed: {blank} carry state:'' in the index and are matched BY KEY "
|
||
'— a state-keyed arm would have dropped exactly these two, silently')
|
||
else:
|
||
FAIL(f"§1.2: the two state:'' towns {blank} are not matched by key")
|
||
if r['__control']['matched'] is False and r['__null']['matched'] is False:
|
||
OK("CONTROL FIRES: an unknown key returns matched:false (label '%s') and townCharacter(null) too "
|
||
'— "matched" is a real bit, not a constant' % r['__control']['label'])
|
||
else:
|
||
FAIL('§1.2 CONTROL DID NOT FIRE: an unknown key reports matched:%s — the 23 greens above prove nothing'
|
||
% r['__control']['matched'])
|
||
sigs = {r[k]['sig'] for k in keys}
|
||
if len(sigs) == 1:
|
||
FAIL(f"§1.2 VACUOUS: all {len(keys)} towns share ONE palette — the character vector produces a constant")
|
||
elif len(sigs) >= 20:
|
||
OK(f"{len(sigs)} distinct palettes across {len(keys)} towns "
|
||
f"({len(keys) - len(sigs)} deliberate godverse↔real sibling repeats)")
|
||
else:
|
||
WARN(f"§1.2: only {len(sigs)} distinct palettes across {len(keys)} towns")
|
||
# the orphans, at the table
|
||
orph = {'castlemaine_real': 'gravel', 'darwin_real': 'reddust',
|
||
'redhill_real': 'reddust', 'redhill_godverse': 'reddust'}
|
||
bad = {k: json.loads(r[k]['sig'])[0]['base'] for k, want in orph.items()
|
||
if json.loads(r[k]['sig'])[0]['base'] != want}
|
||
if bad: FAIL(f"§1.2: the formerly-orphaned skins are not selected where claimed: {bad}")
|
||
else: OK('gravel → Castlemaine · reddust → Darwin + both Red Hills (selected by NOTHING since v1)')
|
||
if errs: FAIL(f"§1.2: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# ── arm 5: the orphans reach the GPU. The table is a claim; the bound texture is the fact. ──
|
||
for town, want, skin in (('castlemaine_real', 'ground-gravel.jpg', 'gravel'),
|
||
('darwin_real', 'ground-reddust.jpg', 'reddust'),
|
||
('hobart_real', 'ground-grass-temperate.jpg', 'grass-temperate')):
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, f'plansrc=osm&town={town}')
|
||
g = pg.evaluate(JS_R38_GROUND)
|
||
maps = [m['map'] for m in g['meshes'] if m['map']]
|
||
c = g['character']
|
||
if not c or not c['matched']:
|
||
FAIL(f"§1.2: {town} booted with character matched={c and c['matched']}")
|
||
elif want in maps:
|
||
OK(f"{town}: character '{c['label']}' → {skin} is BOUND ({want} on a live ground mesh), "
|
||
f"{g['meshCount']} meshes, sky {c['sky']}")
|
||
else:
|
||
FAIL(f"§1.2: {town} wants {skin} but the bound ground maps are {maps}")
|
||
if errs: WARN(f"§1.2 {town}: {len(errs)} console error(s); first: {errs[0][:120]}")
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
def smoke_magpie(p):
|
||
"""[R38 §1.3] THE MAGPIE — +1 draw on screen, exactly 0 otherwise.
|
||
|
||
Arms:
|
||
1. THE LATCH, kept verbatim from the packet: the day-roll clears `gotYouToday` AND
|
||
**localStorage gained no new key**. Both halves matter — a positive control first (the latch
|
||
must be TRUE before the roll, or "it is false afterwards" is worthless), then the clear, then
|
||
the delta-law assertion on storage and on the save blob itself.
|
||
2. THE SEASON IS NOT A CONSTANT — Lane B's first cut keyed the year offset on `citySeed` alone
|
||
and made **24 of 24 towns 214 days from swooping season**; it would have gated green because
|
||
`?magpie=1` forces the state. So this arm runs UNFORCED and asserts the season varies across
|
||
the shipped corpus, and that at least one town is in season on day 1.
|
||
3. TERRITORY SCALES WITH THE ROAD GRAPH — synthetic (6.8 km) and bendigo_real (188 km) must not
|
||
land on the same count. A fixed-capacity magpie passes every other arm here.
|
||
4. +1 DRAW, and the 0 is real — same pose, `?magpie=1` vs `?magpie=0`, citizen sim quiet.
|
||
"""
|
||
head('GATE R38 §1.3: the magpie (the latch + no new storage key · the season is not a constant · territory scales · +1 draw)')
|
||
b, pg, errs = new_page(p)
|
||
try:
|
||
boot(pg, 'magpie=1')
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
# Baseline the storage AFTER a save exists. `game.sleep()` writes `procity-save` — that is v7
|
||
# behaviour this item must not be blamed for; the arm is "did the MAGPIE add a key", so the
|
||
# pre-existing key has to be in the baseline or the gate reports someone else's write.
|
||
pg.evaluate("() => { try { window.PROCITY.game.save(); } catch {} }")
|
||
st = pg.evaluate("() => { const m = window.PROCITY.magpie; return m ? JSON.parse(JSON.stringify({"
|
||
"territories: m.state.territories, roadKm: m.state.roadKm, forced: m.state.forced,"
|
||
"inSeason: m.state.inSeason, active: m.state.active, mode: m.state.mode,"
|
||
"seasonOffset: m.state.seasonOffset })) : null; }")
|
||
if not st:
|
||
FAIL('§1.3: PROCITY.magpie is null on ?magpie=1'); return
|
||
syn_terr = st['territories']
|
||
OK(f"synthetic default: {syn_terr} territories over {st['roadKm']} km of road "
|
||
f"(1 per {1000 * st['roadKm'] / max(1, syn_terr):.0f} m), forced={st['forced']}")
|
||
|
||
# ── arm 1: the latch, and the storage delta law ──
|
||
keys0 = pg.evaluate("() => Object.keys(localStorage).sort()")
|
||
act = pg.evaluate("() => window.PROCITY.magpie.state.active")
|
||
if not act:
|
||
FAIL('§1.3: no active territory picked — the latch arm has no subject'); return
|
||
pg.evaluate("(a) => window.DBG.teleport(a.x + 6, a.z + 6, 0)", act)
|
||
got = None
|
||
for _ in range(40):
|
||
pg.wait_for_timeout(400)
|
||
s = pg.evaluate("() => ({ got: window.PROCITY.magpie.state.gotYouToday,"
|
||
" swoops: window.PROCITY.magpie.state.swoopsToday,"
|
||
" launches: window.PROCITY.magpie.state.launches,"
|
||
" mode: window.PROCITY.magpie.state.mode })")
|
||
if s['got']:
|
||
got = s; break
|
||
if not got:
|
||
FAIL(f"§1.3: he never swooped in 16 s at his own pole (last {s}) — the latch arm is vacuous without it")
|
||
else:
|
||
OK(f"positive control: after {got['swoops']} swoop(s) — gotYouToday TRUE, launches {got['launches']}")
|
||
day0 = pg.evaluate("() => window.PROCITY.game.day")
|
||
pg.evaluate("() => window.PROCITY.game.sleep()")
|
||
pg.wait_for_timeout(700)
|
||
after = pg.evaluate("() => ({ got: window.PROCITY.magpie.state.gotYouToday,"
|
||
" swoops: window.PROCITY.magpie.state.swoopsToday,"
|
||
" rolls: window.PROCITY.magpie.state.dayRolls,"
|
||
" day: window.PROCITY.game.day, mode: window.PROCITY.magpie.state.mode })")
|
||
if after['got'] is False and after['swoops'] == 0 and after['day'] == day0 + 1:
|
||
OK(f"THE DAY ROLL CLEARS THE LATCH: day {day0}→{after['day']}, gotYouToday false, swoopsToday 0, "
|
||
f"dayRolls {after['rolls']}, mode '{after['mode']}' (he is back on his pole — a swoop cannot "
|
||
'be in progress across a night)')
|
||
else:
|
||
FAIL(f"§1.3: the day roll did not clear the latch: {after}")
|
||
keys1 = pg.evaluate("() => Object.keys(localStorage).sort()")
|
||
new = [k for k in keys1 if k not in keys0]
|
||
if not new:
|
||
OK(f"DELTA LAW: localStorage gained NO new key across the swoop and the roll (keys: {keys1})")
|
||
else:
|
||
FAIL(f"§1.3: localStorage gained {new} — a v8 content layer must not grow the save file")
|
||
blob = pg.evaluate("() => { try { return window.PROCITY.game.export(); } catch { return ''; } }")
|
||
leaked = [w for w in ('magpie', 'swoop', 'washing', 'yard') if w in blob.lower()]
|
||
if leaked: FAIL(f"§1.3: the save blob mentions {leaked}")
|
||
else: OK('the save blob mentions none of magpie/swoop/washing/yard — '
|
||
+ 'keys: ' + ','.join(sorted(json.loads(blob).keys()) if blob else []))
|
||
if errs: FAIL(f"§1.3: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# ── arm 2: THE SEASON IS NOT A CONSTANT (Lane B's own bug, gated) — UNFORCED ──
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, '')
|
||
forced = pg.evaluate("() => window.PROCITY.magpie ? window.PROCITY.magpie.state.forced : null")
|
||
if forced is not False:
|
||
FAIL(f"§1.3: the default boot reports forced={forced} — the season arm below would be vacuous")
|
||
idx = json.loads((ROOT / 'web' / 'assets' / 'towns' / 'index.json').read_text())
|
||
towns = idx['towns'] if isinstance(idx, dict) else idx
|
||
keys = [t['key'] for t in towns]
|
||
r = pg.evaluate("""async (keys) => {
|
||
const m = await import('./js/world/magpie.js');
|
||
const seed = (window.PROCITY.plan.citySeed >>> 0);
|
||
const out = { synthetic: m.daysToSeason(1, seed, null) };
|
||
for (const k of keys) out[k] = m.daysToSeason(1, seed, k);
|
||
return out;
|
||
}""", keys)
|
||
vals = sorted(r.values())
|
||
distinct = len(set(vals))
|
||
inseason = [k for k, v in r.items() if v == 0]
|
||
if distinct <= 1:
|
||
FAIL(f"§1.3 VACUOUS SEASON: all {len(r)} towns are {vals[0]} days from swooping season — "
|
||
'this is exactly the constant Lane B shipped first (24/24 at 214 days)')
|
||
else:
|
||
OK(f"the season is keyed per town: {distinct} distinct positions in the year across {len(r)} towns "
|
||
f"(min {vals[0]}, max {vals[-1]} days out) — NOT the 24-of-24 constant the first cut produced")
|
||
if inseason:
|
||
OK(f"unforced, on the default seed, {len(inseason)} town(s) are IN swooping season on day 1: {inseason}")
|
||
else:
|
||
WARN(f"no town is in season on day 1 of the default seed (nearest {vals[0]} days) — "
|
||
'the mechanism varies but nobody arrives to a swoop; design ratio is 42/364 = 11.5%')
|
||
# the design ratio itself, over 1000 seeds — the distribution, not one lucky boot
|
||
hit = pg.evaluate("""async () => {
|
||
const m = await import('./js/world/magpie.js');
|
||
let n = 0; for (let s = 1; s <= 1000; s++) if (m.inSwoopSeason(1, s >>> 0, null)) n++;
|
||
return n;
|
||
}""")
|
||
if 60 <= hit <= 200:
|
||
OK(f"{hit}/1000 ?seed= values put the synthetic default in season on day 1 "
|
||
f"({hit/10:.1f}% against a design ratio of 42/364 = 11.5%)")
|
||
else:
|
||
FAIL(f"§1.3: {hit}/1000 seeds in season on day 1 — the distribution is not the designed 11.5%")
|
||
finally:
|
||
b.close()
|
||
|
||
# ── arm 3: territory scales with the road graph ──
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, 'plansrc=osm&town=bendigo_real&magpie=1')
|
||
st = pg.evaluate("() => { const m = window.PROCITY.magpie; return m ? { t: m.state.territories,"
|
||
" km: m.state.roadKm } : null; }")
|
||
if not st:
|
||
FAIL('§1.3: no magpie on bendigo_real')
|
||
elif st['t'] > syn_terr * 3:
|
||
OK(f"territory SCALES with the road graph: synthetic {syn_terr} over {'%.1f' % 6.78} km → "
|
||
f"bendigo_real {st['t']} over {st['km']} km (1 per {1000*st['km']/st['t']:.0f} m). A fixed-capacity "
|
||
'magpie would read the same number in both towns and pass every other arm here.')
|
||
else:
|
||
FAIL(f"§1.3: bendigo_real ({st['km']} km) has {st['t']} territories against synthetic's {syn_terr} "
|
||
'— the count is not scaling with the graph')
|
||
finally:
|
||
b.close()
|
||
|
||
# ── arm 4: +1 draw on screen, exactly 0 off. Sim quiet on both sides. ──
|
||
res = {}; mpose = None
|
||
for label, q in (('magpie=1', 'magpie=1&roster=v1&pop=0'), ('magpie=0', 'magpie=0&roster=v1&pop=0')):
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, q)
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
if mpose is None:
|
||
# the pose is taken ONCE, off the ?magpie=1 boot, and reused verbatim on the ?magpie=0
|
||
# boot — where PROCITY.magpie is null and there is nothing to ask. Two different poses
|
||
# is not an A/B, it is two numbers.
|
||
a = pg.evaluate("() => { const t = window.PROCITY.magpie.state.active; return [t.x + 10, t.z + 10]; }")
|
||
mpose = [round(a[0], 2), round(a[1], 2)]
|
||
pg.evaluate("(m) => window.DBG.teleport(m[0], m[1], 0)", mpose)
|
||
pg.wait_for_timeout(900)
|
||
i = pg.evaluate("(m) => window.DBG.teleport(m[0], m[1], 0)", mpose)
|
||
cnt = pg.evaluate("() => window.PROCITY.magpie ? window.PROCITY.magpie.count : null")
|
||
res[label] = (i['drawCalls'], i['tris'], cnt)
|
||
finally:
|
||
b.close()
|
||
d1, t1, c1 = res['magpie=1']; d0, t0, c0 = res['magpie=0']
|
||
if c0 is not None:
|
||
FAIL(f"§1.3: ?magpie=0 still constructed the layer (count {c0})")
|
||
else:
|
||
OK('?magpie=0 constructs nothing (PROCITY.magpie is null) — the 0 is by absence, not by a count')
|
||
print(f" \033[36m[record]\033[0m the cost pose is {mpose} (his territory + 10 m), sim quiet on both boots")
|
||
if c1 == 1 and d1 - d0 == 1:
|
||
OK(f"+1 DRAW while he is on screen, measured: {d0} → {d1} draws (+1), {t0:,} → {t1:,} tris "
|
||
f"(+{t1-t0}) with the instance count at {c1}")
|
||
elif c1 == 1:
|
||
WARN(f"§1.3: bird on screen (count 1) and draws moved {d0} → {d1} (want +1); tris +{t1-t0}")
|
||
else:
|
||
FAIL(f"§1.3: could not put the bird on screen for the cost arm (count {c1}) — the +1 claim is unmeasured")
|
||
|
||
|
||
def smoke_yards(p):
|
||
"""[R38 §1.4] BACKYARD AUSTRALIA — the box props ride buildings.js's existing boxMats at +0 draws;
|
||
only the washing pays a draw.
|
||
|
||
Arms:
|
||
1. THE SUBJECT IS REAL — 181 house/yard lots on the default boot, 181 hoists, N washing items,
|
||
the cap spent at 256 by day and **0 at night** (the washing comes in).
|
||
2. THE COST DECOMPOSITION, with both halves falsifiable at one pose, sim quiet:
|
||
`?yards=0` vs `?washing=0` → **draws EQUAL, tris DIFFERENT** (the box props are free)
|
||
`?washing=0` vs default → **draws +1** (the cloth's own mesh)
|
||
If the props were paying a draw the first comparison fails; if `?yards=0` were decoration the
|
||
tris would not move.
|
||
3. IT SKIPS LOUDLY ON REAL TOWNS — §1.4 is synthetic-only until the civic fetch lands. On a real
|
||
town the gate asserts the zero is REAL (0 house/yard lots ⇒ 0 hoists ⇒ the mesh is never
|
||
created) and says so as a SCOPE line. A pass there would be vacuous, and the synthetic arm
|
||
above is what makes the zero attributable rather than an empty run.
|
||
"""
|
||
head('GATE R38 §1.4: backyard Australia (the subject · the cost decomposition · the LOUD SKIP on real towns)')
|
||
b, pg, errs = new_page(p)
|
||
syn_lots = None
|
||
try:
|
||
boot(pg, '')
|
||
r = pg.evaluate("""() => {
|
||
const P = window.PROCITY, L = P.plan.lots || [];
|
||
const c = {}; for (const l of L) c[l.use] = (c[l.use] || 0) + 1;
|
||
const w = P.washing;
|
||
return { uses: c, house: c.house || 0, yard: c.yard || 0,
|
||
washing: w ? { hoists: w.state.hoists, items: w.state.items, active: w.state.active,
|
||
cap: w.state.cap, count: w.count, night: w.state.night } : null,
|
||
flags: { yards: P.flags.yards, washing: P.flags.washing } };
|
||
}""")
|
||
syn_lots = r['house'] + r['yard']
|
||
if syn_lots < 100:
|
||
FAIL(f"§1.4: only {syn_lots} house/yard lots on the default boot — the whole item has no subject")
|
||
return
|
||
OK(f"subject: {r['house']} house + {r['yard']} yard = {syn_lots} lots (the 36 `yard` lots had "
|
||
'rendered as blank shopfront sheds since v1)')
|
||
w = r['washing']
|
||
if not w:
|
||
FAIL('§1.4: PROCITY.washing is null on the default boot')
|
||
elif w['hoists'] == syn_lots and w['items'] > w['cap']:
|
||
OK(f"washing: {w['hoists']} hoists → {w['items']} items, cap {w['cap']}, {w['active']} active "
|
||
'(the cap BINDS here — proximity-first spending is doing real work)')
|
||
else:
|
||
WARN(f"§1.4: washing {w} against {syn_lots} lots")
|
||
# night: the washing comes in, and an empty count is a REAL zero
|
||
pg.evaluate("() => window.DBG.setSegment(5)")
|
||
pg.wait_for_timeout(300)
|
||
pg.evaluate("() => window.DBG.teleport(60, 60, 0)")
|
||
pg.wait_for_timeout(500)
|
||
n = pg.evaluate("() => ({ c: window.PROCITY.washing.count, night: window.PROCITY.washing.state.night })")
|
||
if n['c'] == 0 and n['night']:
|
||
OK('at NIGHT the washing comes in: count 0 (the vendored early-return ⇒ the layer costs exactly 0 draws)')
|
||
else:
|
||
FAIL(f"§1.4: at night the washing layer reads {n} — the night zero is not real")
|
||
if errs: FAIL(f"§1.4: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||
finally:
|
||
b.close()
|
||
|
||
# ── arm 2: the cost decomposition at ONE pose, three boots, citizen sim quiet on all three ──
|
||
pose = None
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, 'roster=v1&pop=0')
|
||
pose = pg.evaluate("""() => {
|
||
const L = (window.PROCITY.plan.lots || []).filter((l) => l.use === 'house' || l.use === 'yard');
|
||
let x = 0, z = 0; for (const l of L) { x += l.x; z += l.z; }
|
||
return [ +(x / L.length).toFixed(2), +(z / L.length).toFixed(2) ];
|
||
}""")
|
||
finally:
|
||
b.close()
|
||
cost = {}
|
||
for label, q in (('default', 'roster=v1&pop=0'), ('washing=0', 'washing=0&roster=v1&pop=0'),
|
||
('yards=0', 'yards=0&roster=v1&pop=0')):
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, q)
|
||
pg.evaluate("() => window.DBG.setSegment(2)")
|
||
pg.evaluate("(p) => window.DBG.teleport(p[0], p[1], 0)", pose)
|
||
pg.wait_for_timeout(700)
|
||
i = pg.evaluate("(p) => window.DBG.teleport(p[0], p[1], 0)", pose)
|
||
cost[label] = (i['drawCalls'], i['tris'])
|
||
finally:
|
||
b.close()
|
||
dY, tY = cost['yards=0']; dW, tW = cost['washing=0']; dD, tD = cost['default']
|
||
print(f" \033[36m[record]\033[0m residential pose {pose}, sim quiet — "
|
||
f"?yards=0 {dY}d/{tY:,}t · ?washing=0 {dW}d/{tW:,}t · default {dD}d/{tD:,}t")
|
||
if tW == tY:
|
||
FAIL(f"§1.4 VACUOUS: ?yards=0 and ?washing=0 render identical triangles ({tW:,}) — ?yards=0 reverts NOTHING")
|
||
elif dW <= dY:
|
||
note = ('' if dW == dY else
|
||
f" — and it is {dY - dW} CHEAPER, because 36 `yard` lots that fell through to buildShopfront()"
|
||
' since v1 (facade + signage + awning materials) are now boxMats-only fenced blocks')
|
||
OK(f"+0 DRAWS for every box prop: ?washing=0 {dW} draws vs ?yards=0 {dY} draws while triangles move "
|
||
f"{tY:,} → {tW:,} ({tW-tY:+,}){note}. The fences, hoists, aerials and tanks ride an InstancedMesh "
|
||
'that was already being submitted; a per-chunk yards module would have read +1 × up to 31 here.')
|
||
else:
|
||
FAIL(f"§1.4: the box props ADD {dW - dY} draw(s) (?washing=0 {dW} vs ?yards=0 {dY}) — not +0")
|
||
if dD - dW == 1:
|
||
OK(f"the washing costs exactly +1 draw: {dW} → {dD} ({tW:,} → {tD:,} tris, {tD-tW:+,}) — ONE town-wide "
|
||
'InstancedMesh at any N, not +1 × up to 31 live chunks')
|
||
else:
|
||
WARN(f"§1.4: the washing moved draws {dW} → {dD} ({dD-dW:+d}) at this pose (want +1); "
|
||
f"tris {tW:,} → {tD:,}")
|
||
|
||
# ── arm 3: THE LOUD SKIP. Real towns are 100% use:'shop' until the civic fetch lands. ──
|
||
for town in ('newtown_real', 'bendigo_real'):
|
||
b, pg, _ = new_page(p)
|
||
try:
|
||
boot(pg, f'plansrc=osm&town={town}')
|
||
r = pg.evaluate("""() => {
|
||
const P = window.PROCITY, L = P.plan.lots || [];
|
||
let h = 0; for (const l of L) if (l.use === 'house' || l.use === 'yard') h++;
|
||
const w = P.washing;
|
||
return { lots: L.length, homes: h, washing: w ? { hoists: w.state.hoists, items: w.state.items,
|
||
count: w.count } : null, flag: P.flags.yards };
|
||
}""")
|
||
if r['homes'] == 0 and r['washing'] and r['washing']['hoists'] == 0 and r['washing']['count'] == 0:
|
||
print(f" \033[35m⊘ SCOPE\033[0m §1.4 DOES NOT APPLY on {town}: {r['lots']} lots, "
|
||
f"**0 house/yard**, 0 hoists, 0 washing items, mesh never created. The flag is ON "
|
||
f"({r['flag']}) and the item still costs exactly nothing — measured, not assumed. "
|
||
f"The synthetic arm above found {syn_lots} lots, which is what makes this zero a SCOPE "
|
||
'and not an empty gate. Extends to real towns when A\'s civic fetch lands districts.')
|
||
elif r['homes'] == 0:
|
||
FAIL(f"§1.4 on {town}: 0 house/yard lots but the washing layer reads {r['washing']} — "
|
||
'the "free on real towns" claim is wrong')
|
||
else:
|
||
OK(f"§1.4 APPLIES on {town} after all: {r['homes']} house/yard lots, washing {r['washing']} "
|
||
'— the civic fetch has landed; retire the synthetic-only scope note')
|
||
finally:
|
||
b.close()
|
||
|
||
|
||
|
||
def main():
|
||
# [R37] `--only name[,name…]` runs a subset of the smokes by function name (e.g. `--only smoke_tram`).
|
||
# Development ergonomics only: the suite's contract is still "run them all"; --only prints a banner
|
||
# so a partial run can never be mistaken for a green suite in a log.
|
||
only = None
|
||
for i, arg in enumerate(sys.argv[1:]):
|
||
if arg == '--only' and i + 2 <= len(sys.argv[1:]):
|
||
only = set(sys.argv[i + 2].split(','))
|
||
elif arg.startswith('--only='):
|
||
only = set(arg.split('=', 1)[1].split(','))
|
||
if only:
|
||
print(f"\033[33m● PARTIAL RUN — --only {sorted(only)} (NOT a suite verdict)\033[0m")
|
||
|
||
srv = ensure_server()
|
||
try:
|
||
with sync_playwright() as p:
|
||
if only:
|
||
g = globals()
|
||
for name in sorted(only):
|
||
fn = g.get(name)
|
||
if not callable(fn):
|
||
sys.exit(f"--only: no such check '{name}'")
|
||
fn(p)
|
||
raise SystemExit(_verdict(partial=True))
|
||
flags = list(KNOWN_FLAGS)
|
||
if plansrc_landed(p):
|
||
flags.append('plansrc=osm'); print("· plansrc=osm detected as landed — included")
|
||
else:
|
||
print("· plansrc=osm not landed (Lane A) — skipping that flag")
|
||
|
||
classic_regression(p) # [R16] the frozen v2 covenant moved here: ?classic=1 → byte-identical
|
||
default_boot_gate(p) # [R16] the flip: the no-flag boot is the full experience (all four on)
|
||
interior_budget(p)
|
||
for fl in flags:
|
||
smoke(p, fl.split('=')[0], fl)
|
||
smoke(p, 'ALL-ON combo', '&'.join(flags), is_combo=True)
|
||
smoke_stock(p) # STRICT (r8) — ?stock=real real record sleeves
|
||
smoke_weather(p) # STRICT (r8) — ?weather=rain
|
||
smoke_buy(p) # STRICT (r9) — buy-v0
|
||
smoke_patronage(p) # STRICT (r9) — peds visit open shops
|
||
smoke_booktoy(p) # STRICT (r9) — book/toy real stock
|
||
smoke_presence(p) # new (warn) — interior presence (browsers)
|
||
smoke_shelfbuy(p) # new (warn) — buy-anywhere (book/toy shelves)
|
||
smoke_tram(p) # [R37 0.2] REBUILT STRICT — runs where it runs, genuinely absent where fenced
|
||
smoke_kerb(p) # [R37 0.1] THE KERB IS FOOTPATH — poster clearance on the paint + the ?verge=0 control
|
||
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_glance(p) # R29 — Spike 1: v6's first clip in the game (classic zero-fetch)
|
||
smoke_djdance(p) # R36 — the 2 a.m. dance clips: Rokoko binds · dancer driven · classic zero-fetch · noassets sway
|
||
smoke_sell_routing(p) # R30 — the E-key sell routing (§9.4, the held join, live loop)
|
||
smoke_save_determinism(p) # R30 gate a — export → fresh boot → import byte-equal, world untouched
|
||
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_wantlist_travel(p) # R33 — wantlist · keeper rumor · travel-costs-a-day (+ controls)
|
||
smoke_godbay(p) # R34 — GODBAY offline auction (one hammer · bounded · morning paper)
|
||
smoke_first_five(p) # R35 — the first five minutes (seeded want · splash · beats + controls)
|
||
# ── [R38 v8 WAVE 1] the four inhabitation gates, each with its control demonstrated ──
|
||
smoke_ambient(p) # R38 §1.1 — six point sources · hours · day-of-week · pan · THE PATCHED MANIFEST
|
||
smoke_character(p) # R38 §1.2 — classic by construction · the per-town arm · the two orphans on the GPU
|
||
smoke_magpie(p) # R38 §1.3 — the latch + no new storage key · the season is NOT a constant · +1 draw
|
||
smoke_yards(p) # R38 §1.4 — the cost decomposition · the LOUD SKIP on real towns
|
||
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:
|
||
if srv: srv.terminate()
|
||
|
||
sys.exit(_verdict())
|
||
|
||
|
||
def _verdict(partial=False):
|
||
head('VERDICT' + (' (PARTIAL — --only)' if partial else ''))
|
||
print(json.dumps({'fails': len(fails), 'warns': len(warns)}, indent=0))
|
||
if fails:
|
||
print(f"\033[31m● flags_check RED\033[0m — {len(fails)} failure(s), {len(warns)} warning(s).")
|
||
for f in fails: print(' FAIL:', f)
|
||
return 1
|
||
print(f"\033[32m● flags_check {'PARTIAL-GREEN' if partial else 'GREEN'}\033[0m — {len(warns)} warning(s).")
|
||
for w in warns: print(' warn:', w)
|
||
return 0
|
||
|
||
if __name__ == '__main__':
|
||
main()
|