PROCITY/tools/qa/tour_v5.py
m3ultra 630042fbe1 Lane F R27: the kill-the-server gate GREEN — and two corrections to F's own gate
THE KILL-THE-SERVER GATE, permanent and green (charter law #1 made falsifiable):
  ARM 1 server-up   · gone[3] → 117 survivors · items[] 120, identity AND ORDER intact ·
                      COLLATERAL 0 · `gone` carries the atlas id verbatim (R26 fence)
  ARM 2 killed mid-session · enter 6ms vs control 6ms (Δ+0) · worst frame 204ms vs control
                      202ms (Δ+2) · room complete from files · ZERO game errors · EXACTLY 1
                      refused connection
  ARM 3 server-down boot   · pure tier 1, gone[0] · ZERO game errors · exactly 1 refusal
  SANDBOX · POST/PUT/PATCH/DELETE all 501 · a mint shop 404s
The subject exists only because G built `make-fixture`: production `gone` is EMPTY (0 sales
since 2026-07-01), so this gate would have passed vacuously forever on live data.

CORRECTION 1 — "ZERO console errors on server death" is UNACHIEVABLE, and F measured why. A
fetch to a dead host logs net::ERR_CONNECTION_REFUSED at the BROWSER level; no .catch() sees it.
To log zero the reader would have to know the server is dead WITHOUT ASKING. One refusal is the
irreducible cost of discovering death. F's replacement is strictly stronger: ZERO errors from
game code, and EXACTLY ONE refusal — which PROVES the circuit breaker. Without the breaker it is
one error per shop entry forever, and "zero" could never tell a working breaker from a broken
one because both fail it. F is the beneficiary of this change, so it is filed loudly in §27 for
Fable to ratify or overrule, not quietly relaxed.

CORRECTION 2 — F's frame assertion was weak: `< 250ms` is a number with no control. ARM 2 first
reported worst-frame 201ms and PASSED. But entering a shop rebuilds an interior, which costs the
same with or without a server — so an absolute threshold measures Lane C's room build, not the
network. The arm now walks the same shop with the breaker tripped and asserts the DELTA: 202ms
control, 204ms killed, Δ+2ms. The death cost nothing the room didn't. F told A the same thing one
round ago; a number without a control is not a measurement.

THE TOUR — 3 of 4 frames, and the 4th is FILED NOT SHIPPED. The real crate (the money shot: its
own 120 records, price stickers, the keeper), a mint crate (captioned MINT — plausible, not
real), the classic covenant. 0 page errors. The FRONTAGE frame does not land after three
attempts and F will not ship a frame that doesn't show what its caption claims: the cluster
primitive answers "where is this town's heart", which is not the question a frontage frame asks
— Red Hill's heart is 2 shops/60m and does not contain Monster Robot. Next: ask B, who owns the
frontage and shipped its mark this round. Re-deriving B's anchor in a tour script is how you get
three bad frames.

Also: the interior beauty shot came back with the HUD across it — R21's lesson, fixed on the
street frames only. Now every DOM overlay is hidden, not a guessed selector list.

NOT TAGGED. qa.sh --strict --matrix and the frontage frame remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 15:05:17 +10:00

207 lines
11 KiB
Python

#!/usr/bin/env python3
"""PROCITY v5.0 RELEASE TOUR — THE LIVE CRATE (Lane F, R27 ledger #5)
The crate walk, in the order you'd actually do it: Musgrave Road → Monster Robot Party's frontage (B's
stocked-shopfront mark) → inside, riffling ITS OWN records → a mint shop, captioned honestly → the
`?classic=1` control that still boots the exact v2 town.
Posing: the street frames go through F's R21 cluster primitive (reused from tour_shots.py). Per-EDGE shop
counts are ~1 on a real graph, so "the busiest edge" poses you at bare bitumen — the Fitzroy trap, twice
learned. The interior frames pose themselves: the room builds you a metre inside its own door.
Honesty rules baked in, not bolted on:
· the mint frame says MINT in its caption. A minted crate is plausible, not real, and a tour is exactly
where that distinction gets quietly lost.
· the real frame's caption carries the shop's actual name and address, because it IS one.
· nothing here boots ?live=1. A tour frame must be reproducible by anyone who clones this repo, and
tier 2 needs a POS none of them have. The live tier is proven by the gate, which owns its own server;
the tour shows tier 1, which is what ships in the box.
"""
import json, subprocess, sys, time, pathlib
ROOT = pathlib.Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'tools' / 'qa'))
PORT = 8131
HOST = f'http://127.0.0.1:{PORT}'
SEED = 20261990
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v5'
SHOP = 3962749
# name, query, segment, mode, caption
V5_TOUR = [
('musgrave_road', f'plansrc=osm&town=redhill_godverse&stock=real&seed={SEED}', 3, f'frontage:{SHOP}',
"the frontage — Monster Robot Party on Musgrave Road, Red Hill, Brisbane. It is on this road in the "
"game because it is on this road"),
('the_real_crate', f'plansrc=osm&town=redhill_godverse&stock=real&dig=1&seed={SEED}', 3, f'shop:{SHOP}',
"THE REAL CRATE — Monster Robot Party (147 Musgrave Rd): 120 records off its own till, real covers, "
"real titles, real prices. Sell one in the shop and it leaves this crate"),
('a_mint_crate', f'plansrc=osm&town=redhill_godverse&stock=real&dig=1&seed={SEED}', 3, 'mint',
"a MINT crate — plausible, NOT real: a seeded sample standing in for a shop whose till we don't hold. "
"Same tier, same offline guarantee, different provenance — and the atlas says so in its own index"),
('classic_covenant', f'classic=1&seed={SEED}', 2, 'street',
"?classic=1 — the exact v2 town, byte-identical forever (0x3fa36874). Five epochs, one covenant"),
]
SHOT_JS = r"""
async ({ mode, seg, shop }) => {
const P = window.PROCITY, D = window.DBG, T = P.THREE;
D.setSegment(seg);
const plan = P.plan, shops = plan.shops || [];
if (mode.startsWith('shop:') || mode === 'mint') {
let target;
if (mode === 'mint') {
const man = await (await fetch('assets/stock_godverse/index.json')).json();
const mint = new Set((man.shops||[]).filter(s => s.sourcing === 'mint').map(s => s.godverseShopId));
const byId = new Map((man.shops||[]).map(s => [s.godverseShopId, s]));
target = shops.find(s => mint.has(s.godverseShopId) &&
(byId.get(s.godverseShopId).types||[]).includes(s.type));
} else {
target = shops.find(s => s.godverseShopId === shop);
}
if (!target) return { err: 'no target shop for ' + mode };
D.enterShop('g:' + target.godverseShopId);
await new Promise(r => setTimeout(r, 1500));
const si = D.stockInfo();
// pose: stand back from the spawn looking into the room, so the crates are in frame
const im = P.interiorMode, sp = im.current.spawn;
P.camera.position.set(sp.x, 1.6, sp.z + 0.2);
P.camera.rotation.set(0, sp.ry || 0, 0);
P.camera.updateMatrixWorld();
// Beauty shot: hide EVERY overlay, not just the street HUD. R21 taught this on the street frames
// and F only fixed it there — the first v5 interior frame came back with "press Esc to leave"
// across the top of the money shot. Same lesson, second surface.
// Hide EVERY DOM overlay rather than guessing ids — the interior banner is created at runtime, so a
// hardcoded selector list is exactly how the HUD sneaks back into the next money shot.
for (const el of document.body.children) if (el.tagName !== 'CANVAS') el.style.display = 'none';
for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r));
const info = D.info();
return { name: si.name, gid: si.godverseShopId, items: si.packItems, base: si.base,
shops: shops.length, edges: (plan.streets.edges||[]).length,
draws: info.drawCalls, tris: info.tris };
}
// ── street ──
// R21's cluster primitive answers "where is this town's heart". For `frontage:<id>` the question is
// "where is THIS shop", and they are not the same question: Red Hill's densest cluster is 3 shops/60m
// and does not contain Monster Robot, so the cluster pose produced a beautiful photograph of an empty
// footpath — the Fitzroy trap a third time, wearing the fix for the first two. Anchor on the subject
// when the frame is ABOUT the subject; use the cluster only when the frame is about the town.
const nById = new Map(plan.streets.nodes.map(n => [n.id, n]));
const lots = (plan.lots || []).filter(l => l.use === 'shop' || l.use === 'anchor');
let hub = -1, best = null;
if (mode.startsWith('frontage:')) {
const gid = +mode.split(':')[1];
const sh = shops.find(x => x.godverseShopId === gid);
best = sh ? (plan.lots || []).find(l => l.id === sh.lot) : null; // shop.lot → lot.id
if (best) { hub = lots.filter(o => Math.hypot(o.x - best.x, o.z - best.z) < 60).length; }
if (!best) return { err: 'no lot for godverse ' + gid };
} else {
for (const l of lots) { let c = 0; for (const o of lots) if (Math.hypot(o.x - l.x, o.z - l.z) < 60) c++;
if (c > hub) { hub = c; best = l; } }
}
if (best) {
const e = (plan.streets.edges || []).find(x => x.id === best.frontEdge) || plan.streets.edges[0];
const a = nById.get(e.a), b2 = nById.get(e.b);
const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L;
let t = (best.x - a.x) * ux + (best.z - a.z) * uz;
t = Math.max(6, Math.min(L - 6, t));
if (mode.startsWith('frontage:')) {
// A frontage frame FACES the shop. The cluster pose faces ALONG the edge — correct for "here is a
// street", useless for "here is this shop": it photographed Musgrave Road with Monster Robot out of
// frame beside the lens. Stand on the road at the lot's projection, step back along the lot→road
// normal, and look AT it.
const px = a.x + ux * t, pz = a.z + uz * t; // the lot's own point on the road
let nx = px - best.x, nz = pz - best.z; // lot → road normal
const NL = Math.hypot(nx, nz) || 1; nx /= NL; nz /= NL;
const cx = best.x + nx * (NL + 7), cz = best.z + nz * (NL + 7);
D.teleport(cx, cz, Math.atan2(best.x - cx, best.z - cz));
} else {
const back = Math.min(26, t);
D.teleport(a.x + ux * (t - back), a.z + uz * (t - back), Math.atan2(ux, uz));
}
} else { D.shot('street_noon'); }
await new Promise(r => setTimeout(r, 1800));
const hud = document.getElementById('pc-hud'); if (hud) hud.style.display = 'none';
for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r));
const info = D.info();
return { name: plan.name, shops: shops.length, edges: (plan.streets.edges || []).length,
hub, draws: info.drawCalls, tris: info.tris };
}
"""
def ensure_server():
import socket
s = socket.socket()
try:
s.connect(('127.0.0.1', PORT)); s.close(); return None
except Exception:
pass
srv = subprocess.Popen([sys.executable, '-m', 'http.server', str(PORT)],
cwd=str(ROOT / 'web'), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(1.2)
return srv
def main():
try:
from playwright.sync_api import sync_playwright
except ImportError:
sys.exit('playwright not installed')
print(f"\033[1mv5.0 RELEASE TOUR — THE LIVE CRATE\033[0m ({len(V5_TOUR)} frames → docs/shots/release_v5/)")
OUTDIR.mkdir(parents=True, exist_ok=True)
srv = ensure_server()
shots, errs_total = [], 0
try:
with sync_playwright() as p:
b = p.chromium.launch()
for name, query, seg, mode, cap in V5_TOUR:
pg = b.new_page(viewport={'width': 1280, 'height': 720})
errs = []
pg.on('pageerror', lambda e: errs.append(str(e)))
pg.on('console', lambda m: errs.append(m.text) if m.type == 'error' else None)
pg.goto(f'{HOST}/index.html?{query}&dbg=1')
pg.wait_for_function('window.PROCITY && window.PROCITY.plan && window.DBG && window.DBG.ready',
timeout=25000)
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
try:
pg.wait_for_function('() => !window.PROCITY.fleet || window.PROCITY.fleet.ready', timeout=15000)
except Exception:
pass
r = pg.evaluate(SHOT_JS, {'mode': mode, 'seg': seg, 'shop': SHOP})
if r.get('err'):
print(f" \033[31m✗\033[0m {name}: {r['err']}"); errs_total += 1; pg.close(); continue
pg.screenshot(path=str(OUTDIR / f'{name}.png'))
extra = (f" · {r['items']} records from {r['base']}" if r.get('items')
else (f" · retail heart: {r['hub']} shops/60m" if r.get('hub') else ''))
caption = (f"{cap} · {r['name']}{extra} · {r['draws']} draws · {r['tris']:,} tris")
shots.append({'name': name, 'file': f'{name}.png', 'caption': caption})
print(f" \033[32m●\033[0m {name}: {caption}")
errs_total += len(errs)
if errs:
print(f" \033[31m{len(errs)} page error(s)\033[0m: {errs[0][:110]}")
pg.close()
b.close()
finally:
if srv:
srv.terminate()
html = ["<!doctype html><meta charset=utf-8><title>PROCITY v5.0 — THE LIVE CRATE</title>",
"<style>body{background:#111;color:#ddd;font:15px/1.6 -apple-system,sans-serif;max-width:1100px;"
"margin:40px auto;padding:0 20px}img{width:100%;border-radius:6px;margin:10px 0}"
"h1{font-weight:600}p{color:#aaa}hr{border:0;border-top:1px solid #333;margin:36px 0}</style>",
"<h1>PROCITY v5.0 — THE LIVE CRATE</h1>",
"<p>One real record shop, on its real street, holding its real stock — and a server that can die "
"mid-session without the game noticing.</p>"]
for s in shots:
html.append(f"<hr><h3>{s['name']}</h3><img src='{s['file']}'><p>{s['caption']}</p>")
(OUTDIR / 'contact.html').write_text('\n'.join(html))
(OUTDIR / 'shots.json').write_text(json.dumps(shots, indent=1))
print(f"\n{OUTDIR}/contact.html ({len(shots)} frames, {errs_total} page error(s))")
return 0 if (len(shots) == len(V5_TOUR) and errs_total == 0) else 1
if __name__ == '__main__':
sys.exit(main())