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>
207 lines
11 KiB
Python
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())
|