PROCITY/tools/qa/r39_shots.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

127 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""PROCITY Lane F — R39: RE-BASELINE the two frames that photograph the spawn pose.
Lane B's `+sin` fix moved the non-classic spawn (`index.html:500`, the R32 cluster-pose). Two of this
lane's picture baselines were taken at the OLD pose and are therefore photographs of the defect:
· `docs/shots/v7_tour/02_the_strip.png` — "the cluster-pose spawn: midday on the main street"
· `docs/shots/v7_beta/first_five_splash.png` — the R35 first-five-minutes splash, shot at the spawn
Those two files are **not overwritten**: they are artefacts of the tagged v7.0 epoch and re-shooting
them on a v9 tree would falsify a release record. The re-baseline lands in `docs/shots/laneF_r39/`
with its own BEFORE (the same tree, camera parked at the old `-sin` pose), so the pair is a
comparison rather than a claim.
Run: tools/.venv/bin/python tools/qa/r39_shots.py
"""
import sys, os, time, socket, subprocess, pathlib
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
OUT = ROOT / 'docs' / 'shots' / 'laneF_r39'
PORT = int(os.environ.get('PROCITY_R39_SHOT_PORT', '8761'))
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
NOSTORE = r'''
import sys, http.server, functools
class H(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Cache-Control', 'no-store'); 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()
'''
# both spawn derivations, from the live plan — identical to tools/qa/r39_runtime.py's JS_SIGN
JS_POSES = r"""
() => {
const P = window.PROCITY, plan = P.plan;
const lotById = new Map(plan.lots.map(l => [l.id, l]));
const spawnWith = (sgn) => {
const ms = 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 m = doors.filter(d => ms.has(d.block)); if (m.length >= 2) doors = m;
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) };
};
return { fixed: spawnWith(1), old: spawnWith(-1) };
}
"""
def port_up(port):
with socket.socket() as s:
s.settimeout(0.4); return s.connect_ex(('127.0.0.1', port)) == 0
def main():
from playwright.sync_api import sync_playwright
OUT.mkdir(parents=True, exist_ok=True)
proc = subprocess.Popen([sys.executable, '-c', NOSTORE, str(PORT), str(ROOT / 'web')],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(80):
if port_up(PORT): break
time.sleep(0.1)
shots = []
try:
with sync_playwright() as p:
b = p.chromium.launch()
pg = b.new_page(viewport={'width': 1280, 'height': 720})
for town, q, seg, tag in (('synthetic', '', 2, 'the_strip'),
('katoomba_real', 'plansrc=osm&town=katoomba_real', 2, 'the_strip_katoomba')):
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1&roster=v1&pop=0' + (('&' + q) if q 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'; }")
pg.wait_for_timeout(2500)
poses = pg.evaluate(JS_POSES)
pg.evaluate('(s) => window.DBG.setSegment(s)', seg)
for which in ('old', 'fixed'):
pz = poses[which]
pg.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [pz['x'], pz['z'], pz['yaw']])
try: pg.wait_for_function('() => window.PROCITY.chunks.pending === 0', timeout=20000)
except Exception: pass
pg.wait_for_timeout(2000)
pg.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [pz['x'], pz['z'], pz['yaw']])
name = f"{tag}-{'BEFORE-minus-sin' if which == 'old' else 'AFTER-plus-sin'}.png"
pg.screenshot(path=str(OUT / name))
shots.append(name)
print(f" {name} @ ({pz['x']:.1f}, {pz['z']:.1f}) yaw {pz['yaw']:.3f}")
# the R35 first-five frame: a FRESH game at the fixed spawn, splash + hunt line visible
b2 = p.chromium.launch()
pg2 = b2.new_page(viewport={'width': 1280, 'height': 720})
pg2.goto(f'{HOST}/index.html?seed={SEED}&dbg=1&roster=v1&pop=0')
pg2.wait_for_function('window.DBG && window.DBG.ready === true', timeout=30000)
pg2.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
pg2.wait_for_timeout(2500)
pz = pg2.evaluate(JS_POSES)['fixed']
pg2.evaluate('() => window.DBG.setSegment(2)')
pg2.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [pz['x'], pz['z'], pz['yaw']])
try: pg2.wait_for_function('() => window.PROCITY.chunks.pending === 0', timeout=20000)
except Exception: pass
pg2.wait_for_timeout(2000)
pg2.evaluate("([x, z, y]) => window.DBG.teleport(x, z, y)", [pz['x'], pz['z'], pz['yaw']])
hunt = pg2.evaluate("""() => { const h = document.getElementById('pc-hunt');
return h ? { shown: h.style.display !== 'none', text: h.textContent } : null; }""")
pg2.screenshot(path=str(OUT / 'first_five-AFTER-plus-sin.png'))
shots.append('first_five-AFTER-plus-sin.png')
print(f" first_five-AFTER-plus-sin.png hunt line: {hunt}")
b2.close(); b.close()
finally:
proc.terminate()
print(f"\n{len(shots)} frames → {OUT}")
if __name__ == '__main__':
sys.exit(main())