PROCITY/tools/qa/r39_runtime.py
m3ultra 076b972f71 Lane F R39 (v9 WAVE 1, CLOSE): QA GREEN 10/0/0/0 — and the interior margin is 162, not 21 and never 6
FIVE GATES, FIVE CONTROLS, each demonstrated in the same run. New F-owned tools wired into qa.sh as
Gate 8: r39_address.mjs · r39_transmission.py · r39_runtime.py · r39_shots.py (--no-r39 skips the
browser half). Lane A's selfcheck reproduces exactly: 157,647/157,647 ALL GREEN, fingerprint
0x5f76e76 — ZERO GOLDENS MOVED, in a round that added an address layer, a fog ledger, a spawn move
and a new interior fitting.

THE ROUND'S BIGGEST NUMBER IS NOT THE ONE THE LEDGER ASKED FOR. Two port-isolated no-store roots,
the same inode for every byte except one file — the shipped bookshelf GLB vs a copy with
transmissionFactor:1 re-inserted (BIN sha1 IDENTICAL, only the JSON chunk moved) — both through
interior_test.html's own drawSweep:

  GLB-on worst   329 (opshop/hall)  ->  188 (dept/auto)     -141
  GLB-off worst  162 (opshop/hall)  ->  162 (opshop/hall)   0 — the control's own control
  margin vs <=350            21     ->  162                 +141

opshop 329->174 (-47%) · book 247->125 (-49%) · every other type 0, exactly the shape Lane C
predicted from an isolated room, at sweep scale, by a harness C did not write. The worst interior
room in the game is now dept/auto — a room with no bookshelf in it. The default boot fetches these
GLBs from the public depot, so all four R39 GLBs were fetched off digalot.fyi and scanned: sha1
identical to the local tree, 0 transmissive materials, 4/4. Binding fact #3's third and final value.

39.1 ADDRESS — GREEN. 1,192/1,219 shops (97.8%), 23/23 'ways', 857/857 membership agree / 0 wrong
(and the control is asserted to have CHECKED 857 edges). No cache -> supplier 'district', 0 names,
still labels 72/72 from district+block. RED ARM: a 3 m shift error RAISES adelaide 89 -> 98 resolved
shops (Lane A's number to the digit) while INVENTING 9 names; stats().shiftCheck reads DISAGREE on
all 72 perturbations. And the honest limit: at 3 m the membership control reads 0 disagreements,
because those shops front ways OSM leaves unnamed — only the shift cross-derivation catches it.

39.2 FOG — GREEN, both arms on one boot. HIDES 4/72 at spawn, 0 of 61 distant shops known. REVEALS
2,125 m -> 58 shops. FRONT TEST: probe from 5 m BEHIND each of 59 unknown shopfronts, 0 learned.
+0 DRAWS AND +0 TRIS, four freshly launched browsers, both orders, drained, 3 reads each, with the
arms asserted distinguishable first. 8/8 corrupt saves rejected, state untouched. Null store on
classic/game=0/fog=0; game=0 and fog=0 pixel-identical, default differs.

39.3 THE SIGN — GREEN. Shops in front of the player at spawn: 4/4 · 4/4 · 4/4 · 1/1, and ZERO on all
four with the pre-R39 sign re-derived on the same tree. The live spawn is asserted equal to the +sin
derivation, so the shell is what is scored. Re-baselined WITHOUT falsifying a release record: the
v7 tour and v7-beta frames are kept as history; docs/shots/laneF_r39/ ships the new pair with its
own BEFORE. The R35 gate itself is pose-independent and was green either side.

39.4 RUMMAGE BIN — GREEN. 120 rooms: tub 60/60 both arms, kind:'bin' 0/60 default vs 60/60 armed,
0 carves, 60/60 deterministic, reachable 0.80-1.03 m by a flood fill written here. THE CONTENTS DID
NOT SHIP: on the default street boot the tub is kind:'prop', 0 dig targets, E opens nothing.

39.5 TRANSMISSION VALIDATOR — GREEN and it FIRES: hard-fails rc 1 on the fabricated glass asset AND
passes on the shipped tree.

THREE CORRECTIONS, ALL MEASURED. A's "a 30 m shift returns 0 street names" — it returns 40/12/47
wrong ones; a big shift lands you on the next street, it does not degrade to null. B's "a fresh
export has no `known` key" — true of ?fog=0, no longer of a default boot, because B's own +sin fix
means you spawn in front of four shopfronts. And my own: I first aimed the transmission control at
web/assets/models/ when validate_manifest.py reads pipeline/_normalized/, and nearly filed a defect
against Lane E on the strength of it. Aim the control at the code path, not the plausible path.

HELD -> LANE E: check_transmission() skips any GLB with no local _normalized copy and never scans
web/assets/models/. Demonstrated with a live control — remove the local copy and the same glass
asset passes rc 0. A depot-only transmissive GLB (the exact shape of longbench and streetlight)
would ship unseen.

No tag.

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

688 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""PROCITY Lane F — R39 §39.5 runtime gates: THE SIGN (B) · THE FOG (B) · THE RUMMAGE BIN (C).
Three gates, one browser, each with its falsifiability control demonstrated in the same run.
§39.5-SIGN index.html:428/:500 read `(-sin ry, -cos ry)` — the BACK of the building — since R8/R32.
The picture-free arm Lane B specified: AT THE SPAWN, the count of shops within 25 m that
are IN FRONT of the player must be > 0. It was 0 on 4 of 4 towns before. The red arm is
computed here from the plan by re-deriving the OLD spawn with the old sign and scoring
the same metric at it — so the gate carries its own "before" on every run.
§39.5-FOG BOTH ARMS OR IT IS VACUOUS: unwalked shops hidden AND walked shops revealed, measured on
the same boot. Plus zero draws (default vs ?fog=0 at one pose, sim quiet), the save
round-trip and its rejects, and the null store on ?classic=1 / ?game=0 / ?fog=0.
§39.5-BIN The op shop's rummage bin: present and dug-able when armed, and CONTENTS UNARMED on the
boot every player gets (Lane C held the contents for John). Both arms, or the gate only
proves that a tub exists.
Run: tools/.venv/bin/python tools/qa/r39_runtime.py [--only sign,fog,bin] [--json OUT]
"""
import sys, os, json, time, socket, subprocess, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
PORT = int(os.environ.get('PROCITY_R39_RT_PORT', '8751'))
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
FOG_R = 25.0
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 note(m): print(f" \033[33m·\033[0m {m}")
NOSTORE = r'''
import sys, http.server, functools
class H(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
super().end_headers()
def log_message(self, *a): pass
http.server.HTTPServer(('127.0.0.1', int(sys.argv[1])),
functools.partial(H, directory=sys.argv[2])).serve_forever()
'''
def port_up(port):
with socket.socket() as s:
s.settimeout(0.4); return s.connect_ex(('127.0.0.1', port)) == 0
def serve(root, port):
proc = subprocess.Popen([sys.executable, '-c', NOSTORE, str(port), str(root)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(80):
if port_up(port): return proc
time.sleep(0.1)
proc.terminate(); raise SystemExit(f'could not serve on :{port}')
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=30000)
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 SIGN
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# Both spawn algorithms, re-derived from the LIVE plan in the page: `sgn = +1` is index.html:506 as
# shipped after Lane B's fix, `sgn = -1` is what it read from R32 until this round. Then one scorer
# for both poses. Nothing is monkey-patched: the shipped pose is READ off the live player and asserted
# equal to the +1 derivation, which is what makes the -1 derivation a fair "before".
JS_SIGN = r"""
(FOG_R) => {
const P = window.PROCITY, plan = P.plan;
const lotById = new Map(plan.lots.map(l => [l.id, l]));
const spawnWith = (sgn) => {
const msBlocks = new Set((plan.blocks || []).filter(b => b.kind === 'mainstreet').map(b => b.id));
const doorOf = (s) => {
const l = lotById.get(s.lot); if (!l) return null;
const ry = l.ry || 0;
return { x: l.x + sgn * Math.sin(ry) * (l.d / 2 + 2.6), z: l.z + sgn * Math.cos(ry) * (l.d / 2 + 2.6), block: l.block };
};
let doors = plan.shops.map(doorOf).filter(Boolean);
const ms = doors.filter(d => msBlocks.has(d.block));
if (ms.length >= 2) doors = ms;
if (doors.length < 2) return null;
let cx = 0, cz = 0; for (const d of doors) { cx += d.x; cz += d.z; } cx /= doors.length; cz /= doors.length;
let vx = 0, vz = 0; for (const d of doors) { vx += (d.x - cx) ** 2; vz += (d.z - cz) ** 2; }
const ax = vx >= vz ? 'x' : 'z';
let stand = doors[0], bd = Infinity;
for (const d of doors) { const dd = (d.x - cx) ** 2 + (d.z - cz) ** 2; if (dd < bd) { bd = dd; stand = d; } }
let lo = Infinity, hi = -Infinity;
for (const d of doors) { lo = Math.min(lo, d[ax]); hi = Math.max(hi, d[ax]); }
const dir = (hi - stand[ax] >= stand[ax] - lo) ? 1 : -1;
return { x: stand.x, z: stand.z, yaw: ax === 'x' ? Math.atan2(-dir, 0) : Math.atan2(0, -dir) };
};
// the shopfront points the fog probe uses: facade centre + its outward normal (buildings.js's +Z)
const fronts = [];
for (const s of plan.shops) {
const l = lotById.get(s.lot); if (!l) continue;
const ry = l.ry || 0, nx = Math.sin(ry), nz = Math.cos(ry);
fronts.push({ id: s.id, x: l.x + nx * (l.d / 2), z: l.z + nz * (l.d / 2), nx, nz });
}
const score = (pos) => {
if (!pos) return null;
let near = 0, front = 0;
for (const f of fronts) {
const dx = pos.x - f.x, dz = pos.z - f.z;
if (dx * dx + dz * dz > FOG_R * FOG_R) continue;
near++;
if (dx * f.nx + dz * f.nz > 0) front++;
}
return { near, front };
};
const live = { x: P.player.position.x, z: P.player.position.z };
const fixed = spawnWith(1), old = spawnWith(-1);
return {
town: P.townKey || 'synthetic',
shops: plan.shops.length,
live, fixed, old,
liveMatchesFixed: !!fixed && Math.hypot(live.x - fixed.x, live.z - fixed.z) < 0.01,
scoreLive: score(live), scoreFixed: score(fixed), scoreOld: score(old),
discovered: P.discovery ? P.discovery.store.counts().shops : null,
};
}
"""
def gate_sign(p, result):
head('GATE R39 §39.5-SIGN — at the spawn, are there shopfronts in front of you?')
towns = [('synthetic', ''), ('katoomba_real', 'plansrc=osm&town=katoomba_real'),
('bowral_real', 'plansrc=osm&town=bowral_real'), ('fitzroy_real', 'plansrc=osm&town=fitzroy_real')]
rows = {}
b, pg, errs = new_page(p)
try:
for name, q in towns:
boot(pg, q)
r = pg.evaluate(JS_SIGN, FOG_R)
rows[name] = r
print(f" {name:16s} shipped(+sin) at ({r['fixed']['x']:.1f}, {r['fixed']['z']:.1f}): "
f"{r['scoreFixed']['near']} shops within 25 m, \033[1m{r['scoreFixed']['front']} in front\033[0m"
f" · old(-sin) at ({r['old']['x']:.1f}, {r['old']['z']:.1f}): "
f"{r['scoreOld']['near']} within 25 m, \033[31m{r['scoreOld']['front']} in front\033[0m")
finally:
b.close()
result['sign'] = rows
live_ok = all(r['liveMatchesFixed'] for r in rows.values())
if live_ok: OK(f"the LIVE spawn equals the +sin derivation on {len(rows)}/{len(rows)} towns — the shipped shell is the thing being scored, not a re-implementation")
else: FAIL(f"the live spawn does not match the +sin derivation: {[(k, v['live'], v['fixed']) for k, v in rows.items() if not v['liveMatchesFixed']]}")
good = [k for k, r in rows.items() if r['scoreLive']['front'] > 0]
if len(good) == len(rows):
OK(f"THE ARM: shops in front of the player at spawn > 0 on {len(good)}/{len(rows)} towns — "
+ ' · '.join(f"{k} {r['scoreLive']['front']}/{r['scoreLive']['near']}" for k, r in rows.items()))
else:
FAIL(f"spawn faces nothing on {[k for k in rows if k not in good]} — the sign has flipped back")
bad = [k for k, r in rows.items() if r['scoreOld']['front'] == 0]
if len(bad) == len(rows):
OK(f"THE RED ARM, on this same tree: with the pre-R39 sign the identical metric reads ZERO in front on {len(bad)}/{len(bad)} towns — the gate discriminates")
else:
FAIL(f"the red arm does not go red on {[k for k in rows if k not in bad]} — this gate cannot fail, so it is not a gate")
disc = {k: r['discovered'] for k, r in rows.items()}
OK(f"…and the shell's own probe agrees at boot: shops discovered at spawn {disc}")
if errs: WARN(f"{len(errs)} console error(s) across the sign boots; first: {errs[0][:140]}")
else: OK('0 console errors across all four boots')
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# THE FOG
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# Walk the town by driving the SHIPPED probe (`discovery.probe`) from stations along the street graph,
# rather than trusting a throttled rAF loop in a headless browser to cover ground. The cadence itself
# is checked separately (a plain teleport-and-wait must also learn something).
JS_WALK = r"""
(n) => {
const P = window.PROCITY;
const plan = P.plan, nodes = new Map(plan.streets.nodes.map(x => [x.id, x]));
// A SHOPPER'S WALK, not the longest roads: take the edges the most shops front. On a real town the
// longest edges are rural arterials with nothing on them (katoomba: 5.8 km of them reveals 1 shop),
// which measures the walker, not the fog.
const lotById = new Map(plan.lots.map(l => [l.id, l]));
const perEdge = new Map();
for (const s of plan.shops) { const l = lotById.get(s.lot); if (!l) continue;
perEdge.set(l.frontEdge, (perEdge.get(l.frontEdge) || 0) + 1); }
const rank = new Map([...perEdge].sort((a, b) => b[1] - a[1]).slice(0, n).map(([id], i) => [id, i]));
const edges = plan.streets.edges.filter(e => rank.has(e.id)).map(e => {
const a = nodes.get(e.a), b = nodes.get(e.b);
return a && b ? { a, b, len: Math.hypot(b.x - a.x, b.z - a.z) } : null;
}).filter(Boolean);
let metres = 0, learned = 0, probes = 0;
for (const e of edges) {
const steps = Math.max(2, Math.ceil(e.len / 4));
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const pos = { x: e.a.x + (e.b.x - e.a.x) * t, z: e.a.z + (e.b.z - e.a.z) * t };
learned += P.discovery.probe(pos); probes++;
}
metres += e.len;
}
const c = P.discovery.store.counts();
return { metres: Math.round(metres), probes, learned, shops: c.shops, edges: c.edges,
stats: P.discovery.stats, view: P.minimap.view };
}
"""
# count pixels on the minimap canvas that EXACTLY match one of minimap.js's ten shop-type colours.
# An exact palette match, not a threshold: the shipped instrument for "is a shop drawn at all".
JS_MAPPX = r"""
() => {
const P = window.PROCITY;
const PAL = ['#8a5aa8','#8a7a5a','#c86aa0','#7a6a3a','#4a6ab0','#c4a028','#c4863a','#5a6a7a','#5a8a6a','#6b5a48'];
const want = new Set(PAL.map(h => ((parseInt(h.slice(1), 16) << 8) | 255) >>> 0));
const cv = document.querySelector('canvas#minimap, canvas.minimap') ||
[...document.querySelectorAll('canvas')].find(c => c !== P.renderer.domElement && c.width >= 600);
if (!cv) return { err: 'no minimap canvas' };
const g = cv.getContext('2d');
const d = g.getImageData(0, 0, cv.width, cv.height).data;
let n = 0, fnv = 0x811c9dc5 >>> 0;
for (let i = 0; i < d.length; i += 4) {
const v = (((d[i] << 16) | (d[i + 1] << 8) | d[i + 2]) << 8 | d[i + 3]) >>> 0;
if (want.has(v)) n++;
fnv = Math.imul((fnv ^ d[i]) >>> 0, 0x01000193) >>> 0;
fnv = Math.imul((fnv ^ d[i + 1]) >>> 0, 0x01000193) >>> 0;
fnv = Math.imul((fnv ^ d[i + 2]) >>> 0, 0x01000193) >>> 0;
}
return { px: n, w: cv.width, h: cv.height, fnv: fnv >>> 0 };
}
"""
JS_OPENMAP = r"""
() => { window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyM' })); }
"""
def gate_fog(p, result):
head('GATE R39 §39.5-FOG — hides what you have not walked, reveals what you have')
out = {}
for town, q in (('katoomba_real', 'plansrc=osm&town=katoomba_real'), ('synthetic', '')):
b, pg, errs = new_page(p)
try:
boot(pg, q)
fresh = pg.evaluate("""() => { const P = window.PROCITY;
return { fog: !!P.knownStore, key: P.fogKey, total: P.plan.shops.length,
known: P.knownStore ? P.knownStore.counts() : null, view: P.minimap.view,
addr: P.addresses ? P.addresses.stats().supplier : null,
exportHasKnown: 'known' in JSON.parse(P.game.export()) }; }""")
pg.evaluate(JS_OPENMAP); pg.wait_for_timeout(400)
px_fresh = pg.evaluate(JS_MAPPX)
# which shops are FAR from the spawn? they must be unknown.
far = pg.evaluate("""() => { const P = window.PROCITY, p = P.player.position;
const lot = new Map(P.plan.lots.map(l => [l.id, l]));
let farTotal = 0, farKnown = 0;
for (const s of P.plan.shops) { const l = lot.get(s.lot); if (!l) continue;
if (Math.hypot(l.x - p.x, l.z - p.z) < 120) continue;
farTotal++; if (P.knownStore.hasShop(s.id)) farKnown++; }
return { farTotal, farKnown }; }""")
walk = pg.evaluate(JS_WALK, 12)
pg.wait_for_timeout(400)
px_walk = pg.evaluate(JS_MAPPX)
save = pg.evaluate("""() => { const P = window.PROCITY, g = P.game;
const blob = g.export(); const obj = JSON.parse(blob);
const before = P.knownStore.counts();
const ids = P.knownStore.shopIds().slice(0, 5);
// round trip
const rt = g.import(blob);
const after = P.knownStore.counts();
// rejects — each must leave live state untouched
const rej = [];
const mut = (f) => { const o = JSON.parse(blob); f(o); return JSON.stringify(o); };
const cases = {
'string id': mut(o => { o.known[P.fogKey].s[0] = 'x'; }),
'negative id': mut(o => { o.known[P.fogKey].s[0] = -1; }),
'float id': mut(o => { o.known[P.fogKey].s[0] = 1.5; }),
'town = array':mut(o => { o.known[P.fogKey] = [1,2,3]; }),
'known = array':mut(o => { o.known = [1,2,3]; }),
'8193 ids': mut(o => { o.known[P.fogKey].s = Array.from({length: 8193}, (_, i) => i); }),
'65 towns': mut(o => { for (let i = 0; i < 65; i++) o.known['t' + i] = { s: [1], e: [] }; }),
'empty key': mut(o => { o.known[''] = { s: [1], e: [] }; }),
};
for (const [why, blb] of Object.entries(cases)) {
const took = g.import(blb);
const now = P.knownStore.counts();
rej.push({ why, took, intact: now.shops === after.shops && now.edges === after.edges });
}
return { keys: Object.keys(obj), hasKnown: 'known' in obj,
ids: obj.known ? obj.known[P.fogKey].s.length : 0,
edgeIds: obj.known ? obj.known[P.fogKey].e.length : 0,
bytes: blob.length, before, after, roundTrip: rt, sample: ids, rejects: rej }; }""")
# THE SIDE TEST'S RED ARM, on the same boot: stand 5 m BEHIND each unknown shopfront and
# probe. A radius-only fog hands every one of them over through the back wall.
behind = pg.evaluate("""() => { const P = window.PROCITY;
const lot = new Map(P.plan.lots.map(l => [l.id, l]));
let tried = 0, learned = 0;
const b0 = P.discovery.stats.rejectedBehind;
for (const s of P.plan.shops) {
if (P.knownStore.hasShop(s.id)) continue;
const l = lot.get(s.lot); if (!l) continue;
const ry = l.ry || 0, nx = Math.sin(ry), nz = Math.cos(ry);
const fx = l.x + nx * (l.d / 2), fz = l.z + nz * (l.d / 2);
tried++;
P.discovery.probe({ x: fx - nx * 5, z: fz - nz * 5 }); // 5 m behind its own facade
if (P.knownStore.hasShop(s.id)) learned++;
}
return { tried, learned, rejected: P.discovery.stats.rejectedBehind - b0 }; }""")
out[town] = {'fresh': fresh, 'far': far, 'walk': walk, 'save': save, 'behind': behind,
'pxFresh': px_fresh, 'pxWalk': px_walk, 'errs': len(errs)}
finally:
b.close()
# THE DELTA LAW, taken where it can actually be taken — and in its OWN browser, because a
# save written by the walk above lives in localStorage and would be re-exported verbatim
# (my first cut read `known` off a ?fog=0 boot that had simply LOADED the fogged save).
# Since the +sin spawn fix the DEFAULT boot discovers 4 shops before the player touches a key,
# so LANE_B_NOTES §39's "a fresh export has no `known` key" is now true of ?fog=0, not of a
# default boot. The claim the law is actually about — a boot that learns NOTHING writes the
# bytes it always wrote — is what this measures.
b, pg, _ = new_page(p)
try:
boot(pg, q + ('&' if q else '') + 'fog=0')
out[town]['nofog'] = pg.evaluate("""() => { const P = window.PROCITY;
const o = JSON.parse(P.game.export());
return { store: !!P.knownStore, keys: Object.keys(o).sort(), hasKnown: 'known' in o }; }""")
finally:
b.close()
result['fog'] = out
for town, r in out.items():
f, w, s = r['fresh'], r['walk'], r['save']
print(f"\n \033[1m{town}\033[0m {f['total']} shops · fogKey {f['key']}")
print(f" fresh boot : {f['known']['shops']} shops / {f['known']['edges']} segments known · "
f"map frame {w['view']['span']:.0f} m at walk-end (fresh {f['view']['span']:.0f} m) · "
f"lot {f['view']['lotPx']['w']}x{f['view']['lotPx']['d']} px (full-plan {f['view']['lotPxFull']['w']}x{f['view']['lotPxFull']['d']} px) · "
f"{r['pxFresh']['px']} px of shop colour on the map")
print(f" after {w['metres']:>5} m walked ({w['probes']} probes): {w['shops']} shops / {w['edges']} segments · "
f"{r['pxWalk']['px']} px of shop colour · {w['stats']['rejectedBehind']} in-radius rejections through a back wall "
f"({w['stats']['behindOnly']} shops refused and never learned from the front)")
# ARM 1 — HIDES
if f['known']['shops'] < f['total'] and r['far']['farKnown'] == 0 and r['far']['farTotal'] > 0:
OK(f"{town} ARM 1 (HIDES): {f['known']['shops']}/{f['total']} known at spawn, and 0 of the {r['far']['farTotal']} shops more than 120 m away are known")
else:
FAIL(f"{town} ARM 1: the fog is not hiding — fresh {f['known']['shops']}/{f['total']}, far known {r['far']}")
# ARM 2 — REVEALS
if w['shops'] > f['known']['shops'] and w['edges'] > f['known']['edges']:
OK(f"{town} ARM 2 (REVEALS): walking {w['metres']} m took it to {w['shops']} shops / {w['edges']} segments (+{w['shops'] - f['known']['shops']} / +{w['edges'] - f['known']['edges']})")
else:
FAIL(f"{town} ARM 2: walking revealed nothing — {f['known']['shops']}{w['shops']} shops")
# ARM 3 — the SIDE TEST's red arm: a radius-only fog would hand these over through the wall
bh = r['behind']
if bh['tried'] > 0 and bh['learned'] == 0 and bh['rejected'] > 0:
OK(f"{town} ARM 3 (the FRONT test): standing 5 m BEHIND each of the {bh['tried']} still-unknown shopfronts and probing learns "
f"\033[1m{bh['learned']}\033[0m of them — {bh['rejected']} refusals. A radius-only fog would have handed over all {bh['tried']}.")
else:
FAIL(f"{town} ARM 3: the side test does not refuse through a back wall — {bh}")
note(f"{town} on the shopper's walk itself the side test refused {w['stats']['rejectedBehind']} times "
f"({w['stats']['behindOnly']} shops never learned from the front) — free where a shopper stands, decisive behind the buildings")
# the map draws the fog
if r['pxWalk']['px'] != r['pxFresh']['px']:
OK(f"{town} the MAP moves with the ledger: {r['pxFresh']['px']}{r['pxWalk']['px']} px of shop colour (the frame grows too, so this is a change, not a monotone)")
else:
FAIL(f"{town} the map drew the same pixels before and after the walk — it is not reading the ledger")
# save — the delta law
if not r['nofog']['hasKnown'] and not r['nofog']['store']:
OK(f"{town} THE DELTA LAW: a boot that learns nothing (?fog=0) exports {r['nofog']['keys']} — no `known` key, the pre-R39 bytes")
else:
FAIL(f"{town} ?fog=0 wrote a `known` key: {r['nofog']}")
if f['exportHasKnown']:
note(f"{town} on the DEFAULT boot the export DOES carry `known` from the first frame — the +sin spawn now puts you in front of "
f"{f['known']['shops']} shopfronts, so LANE_B_NOTES §39's \"a fresh export has no `known` key\" is true of ?fog=0 and no longer of a default boot")
if s['hasKnown'] and s['ids'] == w['shops'] and s['edgeIds'] == w['edges'] and s['roundTrip'] and s['after'] == s['before']:
OK(f"{town} save round-trip: {s['ids']} shop ids + {s['edgeIds']} edge ids, {s['bytes']:,} B, export→import restores the same counts")
else:
FAIL(f"{town} save round-trip wrong: {s}")
bad = [x for x in s['rejects'] if x['took'] or not x['intact']]
if not bad:
OK(f"{town} all {len(s['rejects'])} corrupt-save arms REJECTED and live state untouched: " + ', '.join(x['why'] for x in s['rejects']))
else:
FAIL(f"{town} corrupt saves accepted or state clobbered: {bad}")
# ── the null store: classic / game=0 / fog=0 ──────────────────────────────────────────────────
head('the null arm — ?classic=1 / ?game=0 / ?fog=0 are the pre-R39 map by construction')
nulls = {}
b, pg, errs = new_page(p)
try:
for q, name in (('classic=1', 'classic'), ('game=0', 'game0'), ('fog=0', 'fog0'), ('', 'default')):
boot(pg, 'plansrc=osm&town=katoomba_real' + (('&' + q) if q else ''))
pg.evaluate(JS_OPENMAP); pg.wait_for_timeout(400)
nulls[name] = pg.evaluate("""() => { const P = window.PROCITY;
return { store: !!P.knownStore, fog: P.minimap.view.fog, flagFog: P.flags.fog,
span: P.minimap.view.span, lot: P.minimap.view.lotPx }; }""")
nulls[name]['map'] = pg.evaluate(JS_MAPPX)
finally:
b.close()
result['fogNull'] = nulls
off = [k for k in ('classic', 'game0', 'fog0') if not nulls[k]['store'] and not nulls[k]['fog']]
if len(off) == 3:
OK(f"null store on all three ({', '.join(off)}) — minimap.js sees `known === null`, which is the pre-R39 map with no flag test in the file")
else:
FAIL(f"a fog store leaked into a null arm: {nulls}")
if nulls['game0']['map']['fnv'] == nulls['fog0']['map']['fnv']:
OK(f"?game=0 and ?fog=0 draw a PIXEL-IDENTICAL map (fnv {nulls['fog0']['map']['fnv']:#010x}) — two different routes to the same null store")
else:
FAIL(f"?game=0 and ?fog=0 draw different maps: {nulls['game0']['map']['fnv']:#010x} vs {nulls['fog0']['map']['fnv']:#010x}")
if nulls['default']['map']['fnv'] != nulls['fog0']['map']['fnv']:
OK(f"…and the DEFAULT boot's map differs from both (fnv {nulls['default']['map']['fnv']:#010x}) — the hash is sensitive to the map's content, so the equality above means something")
else:
FAIL('the fogged map hashes the same as the un-fogged one — the pixel comparison is vacuous')
note(f"katoomba at the shipped full-plan transform draws {nulls['fog0']['map']['px']} px of shop colour "
f"(lot {nulls['fog0']['lot']['w']}x{nulls['fog0']['lot']['d']} px) against the fogged map's "
f"{nulls['default']['map']['px']} px at lot {nulls['default']['lot']['w']}x{nulls['default']['lot']['d']} px")
# ── zero draws ────────────────────────────────────────────────────────────────────────────────
head('zero draws — the fog is information design and must cost nothing on the street')
# Each arm gets its OWN freshly launched browser, and the pair is run in BOTH orders. My first cut
# booted default-then-?fog=0 in ONE browser and read a 12k triangle gap at identical draw counts;
# order is a confound (the second boot has a warm HTTP cache, so more of the asset fleet has
# resolved by sample time) and reversing it is the only way to see that. Each boot is sampled
# DRAINED — teleport, wait for chunks.pending == 0, settle, re-teleport at the same pose to read —
# and three times over, so the arm carries its own noise floor.
def run_arm(name, pose):
q = '' if name == 'default' else 'fog=0'
b, pg, _ = new_page(p)
try:
boot(pg, 'roster=v1&pop=0&gigs=0' + (('&' + q) if q else ''))
pg.evaluate("() => window.DBG.setSegment(2)")
if pose is None:
pose = pg.evaluate("() => [window.PROCITY.player.position.x, window.PROCITY.player.position.z]")
def settled(x, z, yaw, n=3):
pg.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [x, z, yaw])
try: pg.wait_for_function("() => window.PROCITY.chunks.pending === 0", timeout=25000)
except Exception: pass
reads = []
for _ in range(n):
pg.wait_for_timeout(1800)
reads.append(pg.evaluate("""([x, z, y]) => { window.DBG.teleport(x, z, y); const i = window.DBG.info();
return { d: i.drawCalls, t: i.tris, chunks: i.chunks }; }""", [x, z, yaw]))
last = reads[-1]
return {**last, 'dRange': [min(r['d'] for r in reads), max(r['d'] for r in reads)],
'tRange': [min(r['t'] for r in reads), max(r['t'] for r in reads)]}
got = {'a': settled(pose[0], pose[1], 0), 'b': settled(pose[0], pose[1], 3.14159265 / 2)}
# non-vacuity: prove the two arms really are two arms before comparing their counters
got['fogOn'] = pg.evaluate("() => ({ flag: window.PROCITY.flags.fog, store: !!window.PROCITY.knownStore })")
pg.evaluate(JS_OPENMAP); pg.wait_for_timeout(400)
got['mapMode'] = pg.evaluate("() => { const i = window.DBG.info(); return { d: i.drawCalls, t: i.tris, mode: i.mode }; }")
return got, pose
finally:
b.close()
zero, pose = {}, None
for run, order in enumerate((('default', 'fog0'), ('fog0', 'default'))):
for name in order:
zero[f'{name}#{run}'], pose = run_arm(name, pose)
result['fogZero'] = zero
arms_differ = all(zero[f'default#{r}']['fogOn']['flag'] and zero[f'default#{r}']['fogOn']['store'] and
not zero[f'fog0#{r}']['fogOn']['flag'] and not zero[f'fog0#{r}']['fogOn']['store'] for r in (0, 1))
if arms_differ:
OK('the two arms ARE two arms: fog flag on + store present on the default boots, off + null on the ?fog=0 boots (without this, "identical counters" is two identical boots)')
else:
FAIL(f"the zero-draw arms are not distinguishable: {[(k, v['fogOn']) for k, v in zero.items()]}")
draws_same, tris_same = True, True
for run in (0, 1):
d0, d1 = zero[f'default#{run}'], zero[f'fog0#{run}']
for k, lab in (('a', 'yaw 0 '), ('b', 'yaw 90')):
print(f" run {run} ({'default first' if run == 0 else '?fog=0 first '}) {lab}: "
f"default {d0[k]['d']} draws {d0[k]['dRange']} / {d0[k]['t']:,} tris {d0[k]['tRange']} · "
f"?fog=0 {d1[k]['d']} draws {d1[k]['dRange']} / {d1[k]['t']:,} tris {d1[k]['tRange']}")
if d0[k]['d'] != d1[k]['d']: draws_same = False
if d0[k]['t'] != d1[k]['t']: tris_same = False
if draws_same and tris_same:
OK('identical in BOTH counters, both yaws, both orders, four freshly launched browsers — the fog costs +0 draws and +0 triangles on the street')
elif draws_same:
OK(f"DRAWS identical in both orders at both yaws — the budget's own quantity does not move")
# is the triangle gap the FLAG or the ORDER? if it follows the order, it is cache warmth.
g0 = zero['default#0']['a']['t'] - zero['fog0#0']['a']['t']
g1 = zero['default#1']['a']['t'] - zero['fog0#1']['a']['t']
if g0 * g1 < 0:
OK(f"…and the triangle gap FOLLOWS THE ORDER, not the flag: {g0:+,} when default boots first, {g1:+,} when ?fog=0 does. "
f"The second browser has a warm HTTP cache and more of the asset fleet resolved by sample time. Not the fog.")
else:
WARN(f"triangles differ in the same direction in both orders ({g0:+,} / {g1:+,} at yaw 0) — not explained by cache warmth; "
f"the fog touches no scene graph, so this wants a look, but it is not a draw and not the budget's quantity")
else:
FAIL(f"the fog moved the DRAW counter: {zero}")
OK(f"map mode reads {zero['default#0']['mapMode']['d']} draws (mode '{zero['default#0']['mapMode']['mode']}') — the frame loop never calls composer.render() in the map branch, so the counter cannot move")
# ══════════════════════════════════════════════════════════════════════════════════════════════════
# THE RUMMAGE BIN
# ══════════════════════════════════════════════════════════════════════════════════════════════════
JS_BIN_ARMED = r"""
async ([arm, glb]) => {
const C = window.PROCITY_C;
const ARCH = C.ARCHETYPE_KEYS;
const out = { rooms: 0, withTub: 0, withBin: 0, carves: 0, pathOK: 0, deterministic: 0,
binless: [], reach: [], reachDist: [], digPlaces: 0 };
const manifest = glb ? await fetch('assets/manifest.json').then(r => r.ok ? r.json() : null).catch(() => null) : null;
// 4-connected flood over walkable cells from the room spawn — layout.js's own reachability, redone
// here so "reachable" is a measurement and not a flag the builder set about itself.
const nearestReachable = (dbg, x, z) => {
const g = dbg.grid;
const idx = (cx, cz) => cz * g.cols + cx;
const start = Array.isArray(dbg.spawnCell) ? idx(dbg.spawnCell[0], dbg.spawnCell[1]) : dbg.spawnCell;
const seen = new Uint8Array(g.cols * g.rows);
const st = [start]; seen[start] = 1;
let best = Infinity;
while (st.length) {
const i = st.pop(), cx = i % g.cols, cz = (i / g.cols) | 0;
const wx = (cx + 0.5) * g.cw - g.W / 2, wz = (cz + 0.5) * g.cd - g.D / 2;
const d = Math.hypot(wx - x, wz - z); if (d < best) best = d;
for (const [dx, dz] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const nx = cx + dx, nz = cz + dz;
if (nx < 0 || nz < 0 || nx >= g.cols || nz >= g.rows) continue;
const j = idx(nx, nz);
if (seen[j] || g.occ[j] !== 0) continue;
seen[j] = 1; st.push(j);
}
}
return best;
};
const opts = arm ? { fittingOpts: { rummageBin: { dig: true } } } : {};
for (const a of ARCH) {
for (let s = 0; s < 6; s++) {
const shop = { id: 'g' + s, type: 'opshop', seed: 1990 + s * 7717, storeys: 1 };
const base = Object.assign({ archetype: a, useGLB: !!glb }, glb ? { manifest } : {}, opts);
const r = C.buildInterior(shop, C.THREE, base);
const r2 = C.buildInterior(shop, C.THREE, base);
out.rooms++;
const tubs = (r.placement || []).filter(p => p.kind === 'rummageBin');
let bin = 0;
r.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bin++; });
if (tubs.length) out.withTub++; else out.binless.push(a + '/' + s);
if (bin) out.withBin++;
if (r.pathOK !== false) out.pathOK++;
if (r.carved) out.carves++;
out.digPlaces += (r.places || []).filter(m => m.userData && m.userData.kind === 'bin').length;
if (tubs.length && r._debug) out.reachDist.push(+nearestReachable(r._debug, tubs[0].x, tubs[0].z).toFixed(2));
// determinism: the placement summary is the deep-equal instrument the room already publishes
if (JSON.stringify(r.placement) === JSON.stringify(r2.placement)) out.deterministic++;
out.reach.push({ a, s, tub: tubs.length, bin, tubKind: tubs.length ? tubs[0].kindName : null,
fw: tubs.length ? tubs[0].fw : null, fd: tubs.length ? tubs[0].fd : null });
r.dispose(); r2.dispose();
}
}
return out;
}
"""
def gate_bin(p, result):
head('GATE R39 §39.5-BIN — the op shop can be dug, and the contents did NOT ship')
b, pg, errs = new_page(p)
res = {}
try:
# ── (a) the DEFAULT BOOT — the one every player gets. Furniture, no contents. ─────────────
boot(pg, '')
res['street'] = pg.evaluate("""() => {
const P = window.PROCITY, D = window.DBG;
D.setSegment(2);
const s = (P.plan.shops || []).find(x => x.type === 'opshop' && P.isOpen(x));
if (!s) return { why: 'no open op shop on the default boot' };
D.enterShop(s.id);
const cur = P.interiorMode.current;
let bins = 0;
cur.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bins++; });
// the tub, identified off the room's OWN placement summary (kind is the recipe's fitting
// kind) — not a bounding-box guess. Then stand in front of it and press E anyway.
const spec = (cur.placement || []).find(p => p.kind === 'rummageBin');
let opened = null, world = null;
if (spec) {
world = cur.group.localToWorld(new P.THREE.Vector3(spec.x, 0.76, spec.z));
P.camera.position.set(world.x, 1.6, world.z + 1.3); P.camera.lookAt(world.x, world.y, world.z);
P.camera.updateMatrixWorld();
window.dispatchEvent(new KeyboardEvent('keydown', { code: 'KeyE' }));
opened = !!P.interiorMode.digActive;
}
const digPlaces = (cur.places || []).filter(m => m.userData && m.userData.kind === 'bin').length;
const fittings = (cur.placement || []).map(x => x.kind);
D.exitShop();
return { shop: s.name, bins, tub: !!spec, tubKind: spec ? spec.kindName : null,
opened, digPlaces, fittings }; }""")
r = res['street']
if r.get('why'): FAIL(f"bin/default: {r['why']}")
else:
if r['tub'] and r['bins'] == 0 and r['digPlaces'] == 0 and r['opened'] is False:
OK(f"DEFAULT BOOT (\"{r['shop']}\", fittings {r['fittings']}): the tub IS in the room and it is `kind:'{r['tubKind']}'` — "
f"0 `kind:'bin'`, 0 dig targets, and standing in front of it and pressing E opens nothing. "
f"THE CONTENTS DID NOT SHIP, which is how Lane C held them for John.")
else:
FAIL(f"bin/default: contents may have shipped — tub {r['tub']} kind '{r['tubKind']}', kind:'bin' {r['bins']}, dig targets {r['digPlaces']}, E opened {r['opened']}")
finally:
b.close()
# ── (b) ARMED, through interior_test.html's own opt ────────────────────────────────────────────
b, pg, errs2 = new_page(p)
try:
for arm, glb, label in ((False, False, 'default · GLB off'), (True, False, 'ARMED · GLB off'),
(False, True, 'default · GLB on'), (True, True, 'ARMED · GLB on')):
pg.goto(f'{HOST}/interior_test.html?localdepot=1')
pg.wait_for_function('() => !!window.PROCITY_C', timeout=30000)
pg.wait_for_timeout(400)
res[label] = pg.evaluate(JS_BIN_ARMED, [arm, glb])
g = res[label]
rd = g['reachDist']
print(f" {label:18s} {g['rooms']} rooms: tub {g['withTub']}/{g['rooms']} · kind:'bin' {g['withBin']}/{g['rooms']} · "
f"dig targets {g['digPlaces']} · pathOK {g['pathOK']}/{g['rooms']} · carves {g['carves']} · "
f"deterministic {g['deterministic']}/{g['rooms']} · nearest reachable cell "
f"{min(rd):.2f}{max(rd):.2f} m" if rd else '')
rooms = sum(res[k]['rooms'] for k in res if k.startswith(('default ·', 'ARMED')))
dd = [res['default · GLB off'], res['default · GLB on']]
aa = [res['ARMED · GLB off'], res['ARMED · GLB on']]
n = sum(x['rooms'] for x in aa)
if all(x['withTub'] == x['rooms'] for x in dd + aa):
OK(f"the tub lands in {n}/{n} armed and {n}/{n} default op-shop rooms (6 archetypes x 6 seeds x GLB on/off) — "
f"Lane C's fallbackZones/`anywhere` machinery does what it says (it landed in 11 of 24 without it)")
else:
FAIL(f"the tub is missing from rooms — armed misses {[x['binless'] for x in aa]}, default misses {[x['binless'] for x in dd]}")
if all(x['withBin'] == 0 for x in dd) and all(x['withBin'] == x['rooms'] for x in aa):
OK(f"THE ARMING IS THE ONLY DIFFERENCE: 0/{n} default rooms carry a `kind:'bin'`, {n}/{n} armed ones do — same tub, one opt, and the opt is OFF in the shipped recipe")
else:
FAIL(f"the dig gate is not the switch: default bins {[x['withBin'] for x in dd]}, armed {[x['withBin'] for x in aa]}")
if all(x['digPlaces'] == 0 for x in dd) and all(x['digPlaces'] == x['rooms'] for x in aa):
OK(f"…and the AIM agrees: 0 `places` entries of kind 'bin' across the {n} default rooms, {n} across the armed ones — the dig cannot resolve to a tub that is not armed")
else:
FAIL(f"places/kind:'bin' wrong: default {[x['digPlaces'] for x in dd]}, armed {[x['digPlaces'] for x in aa]}")
if all(x['pathOK'] == x['rooms'] and x['carves'] == 0 for x in aa + dd):
OK(f"pathOK {n}/{n} on every arm and ZERO corridor carves — the tub never blocks the room it lands in")
else:
FAIL(f"rooms unwalkable or carved: {[(x['pathOK'], x['carves']) for x in aa + dd]}")
if all(x['deterministic'] == x['rooms'] for x in aa + dd):
OK(f"deterministic {n}/{n}: two builds of the same seed produce a byte-equal placement summary on both arms")
else:
FAIL(f"non-deterministic placement: {[x['deterministic'] for x in aa + dd]}")
allrd = [d for x in aa for d in x['reachDist']]
if allrd and max(allrd) < 2.5:
OK(f"REACHABLE {len(allrd)}/{len(allrd)}: the nearest walkable cell flood-reachable FROM THE ROOM SPAWN sits "
f"{min(allrd):.2f}{max(allrd):.2f} m from the tub, against the dig's 2.5 m proximity / 3.2 m aimed reach")
else:
FAIL(f"the tub is out of reach in some rooms: {sorted(allrd)[-5:] if allrd else 'no measurements'}")
finally:
b.close()
result['bin'] = res
if errs2: WARN(f"{len(errs2)} console error(s) on the interior harness; first: {errs2[0][:140]}")
else: OK('0 console errors across the bin arms')
def main():
from playwright.sync_api import sync_playwright
only = None
if '--only' in sys.argv: only = set(sys.argv[sys.argv.index('--only') + 1].split(','))
out_json = sys.argv[sys.argv.index('--json') + 1] if '--json' in sys.argv else None
result = {}
proc = serve(ROOT / 'web', PORT)
try:
with sync_playwright() as p:
if not only or 'sign' in only: gate_sign(p, result)
if not only or 'fog' in only: gate_fog(p, result)
if not only or 'bin' in only: gate_bin(p, result)
finally:
proc.terminate()
if out_json: pathlib.Path(out_json).write_text(json.dumps(result, indent=1, default=str))
print()
print(f"R39 runtime gates: {len(fails)} fail · {len(warns)} warn")
if fails:
for f in fails: print(f" \033[31m✗\033[0m {f}")
return 1
print('\033[32mR39 RUNTIME GATES GREEN\033[0m')
return 0
if __name__ == '__main__':
sys.exit(main())