Lane F R27 (v5.0): the tier-2 reader — pick-then-omit, and the server killed mid-session

#2 THE READER, built to C's §8 ruling (direct wire, no hook) and to what the waves measured,
not to the brief's letter:
  · `pack.gone` set, `price` updated IN PLACE — items[] never added to, removed from, or
    reordered, because its identity AND ORDER are the pick's stability (C §8.3). Filtering the
    array instead reshuffles the crate: C measured ONE sale changing 21 OTHER records.
  · fire-and-forget at room build, 1200ms abort, etag-cached, circuit-broken on first failure.
    The dig never blocks on the network (charter risk #1). No await anywhere near the door.
  · fires ONLY for a real-sourced shop. G's fence 404s a mint shop, and asking anyway would be
    R26's blind probe wearing a server. The manifest already knows which is which.

TIER 2 IS OPT-IN (`?live=1` / `?live=<url>`; absent → byte-identical to the beta). That is the
R26 lesson applied to a server: a default boot that probed for a GODVERSE host would ask a
question with no answer on every machine that isn't John's. Tier 2 is a place you go, not a
thing that happens to you. Auto-discovery is a v5.x question with a reachability contract.

PROVEN IN THE BROWSER (formal smoke class still to land):
  · sold-means-gone, via G's make-fixture (production `gone` is EMPTY — 0 sales since
    2026-07-01 — so a live assert would pass vacuously forever; G built the subject before the
    gate could be born blind): 3 sold → gone[3] → items[] 120→120 IDENTITY AND ORDER INTACT,
    117 survivors, COLLATERAL 0. C called this: both pick-then-omit and filter-the-array hide
    the sold record, so "the gone item never renders" cannot tell them apart — the obvious
    assertion is vacuous and goes green. Collateral is the one that discriminates.
  · KILL THE SERVER MID-SESSION → enter took 13.1ms, worst frame gap 14ms, the crate still
    renders its own 120 real covers, and ZERO console errors. Charter law #1 proven by
    execution instead of assertion — the thing A correctly refused to retire from the risk
    list until this gate went green.

Recorded, not hidden: after a mid-session kill the etag-cached `gone` persists for the session,
so a killed server does not resurrect sold records mid-walk. That is tier-1-plus-last-known, not
tier-1-exactly. It is the honest behaviour (a network blink must not re-stock a shop) and a
server-down BOOT is still pure tier 1 — but it is a nuance the brief's "degrades to tier 1
exactly" doesn't cover, so it goes in writing rather than in the gaps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-17 14:34:42 +10:00
parent 70743db47e
commit 39cfc3d3ed
2 changed files with 71 additions and 2 deletions

View File

@ -194,6 +194,16 @@ const DIG_ON = params.has('dig') && params.get('dig') !== '0';
// [Lane F §F2 stock=real] real GODVERSE sleeves in the crates (v2). Preload the record pack at boot so
// the first interior/dig can batch real covers; fail-soft (missing pack → parody canvas). Off under ?noassets.
const STOCK_REAL = params.get('stock') === 'real' && !NOASSETS;
// [Lane F R27 §2 — TIER 2, OPT-IN] `?live=1` → the default GODVERSE base; `?live=<url>` → that base;
// absent → NO tier 2 at all, byte-identical to the beta. Opt-in is deliberate and is the R26 lesson
// applied to a server: a default boot that probed for a GODVERSE host would ask a question with no
// answer on every machine that isn't John's, and "it only logs an error when the server's away" is
// exactly the blind probe the manifest was built to end. Tier 2 is a place you go, not a thing that
// happens to you. Auto-discovery, if ever, is a v5.x question with a reachability contract behind it.
const LIVE_ARG = NOASSETS ? null : params.get('live');
const LIVE_BASE = !LIVE_ARG || LIVE_ARG === '0' ? null
: (LIVE_ARG === '1' ? 'http://127.0.0.1:8778' : LIVE_ARG.replace(/\/$/, ''));
let STOCK_SOURCING = null; // (godverseShopId) → 'real'|'mint'|null, from G's manifest (set below)
// [Lane F R9] preload ALL three packs so record dig sleeves AND book-spine/toy-box shelves (buy-anywhere)
// are real. Fail-soft per pack (missing → parody canvas). getStockPack(type) is what buildInterior reads.
// [Lane F R24 §6a — THE REAL CRATE] The town-wide packs stay exactly as they were (every shop without a
@ -222,6 +232,8 @@ if (STOCK_REAL) {
const e = byId.get(s.godverseShopId);
if (e && (e.types || []).includes(s.type)) preloadStockPack(s.type, { base: godverseBaseFor(s) });
}
// [R27 §2] the same manifest already knows real vs mint — the tier-2 reader asks only real shops.
STOCK_SOURCING = (id) => (byId.get(id) || {}).sourcing || null;
}
} catch (e) { /* no manifest ⇒ no per-shop packs ⇒ parody. The ladder's bottom rung, unchanged. */ }
}
@ -238,7 +250,8 @@ const gigState = (GIGS_ON && plan.gigs && plan.gigs.length) ? createGigState({ p
// time (door click), long after citizens is initialized — so the forward reference is safe.
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS,
dig: DIG_ON, stockReal: STOCK_REAL, wallet, occupancyOf: (id) => citizens.occupancyOf(id),
rosterOf: (id) => citizens.tonightRoster(id), gigState }); // [R15] identity continuity: the crowd IS tonight's roster
rosterOf: (id) => citizens.tonightRoster(id), gigState, // [R15] identity continuity: the crowd IS tonight's roster
liveBase: LIVE_BASE, sourcingOf: (id) => (STOCK_SOURCING ? STOCK_SOURCING(id) : null) }); // [R27] tier 2
// [Lane F §F2] the riffle is cursor-driven (DOM buttons + click-a-sleeve), so release pointer-lock while it's
// open and re-lock (on click) after. digActive gates the unlock→leaveShop guard below so opening a bin
// doesn't read as "walked out of the shop".

View File

@ -47,9 +47,58 @@ export function godverseBaseFor(shop) {
return id ? `assets/stock_godverse/${id}/` : undefined;
}
// [Lane F R27 §2 — THE TIER-2 READER] Live stock, per C's §8 ruling: direct wire, no lifecycle hook.
// F already holds `currentAdapter` and hands it to `dig.open()` per dig, so a fresh read is picked up by
// the NEXT dig for free. The read is fire-and-forget: if it hasn't landed, hasn't finished, or failed,
// the adapter is simply the tier-1 pack. THE DIG NEVER BLOCKS ON THE NETWORK (charter risk #1).
//
// What it may touch is deliberately tiny, and C's §8.3 is the reason: `items[]` identity and ORDER are
// the pick's stability. So we set `pack.gone` (C picks from the full list and omits after — filtering the
// array instead reshuffles the crate: C measured ONE sale changing 21 other records) and we update
// `price` IN PLACE. We never add, remove, or reorder items[]. Live values are consumed at pick time and
// never persisted — no golden, no seeded stream (charter law #2, the determinism boundary sits here).
const LIVE_TIMEOUT_MS = 1200; // beyond this the shop is tier 1 for this dig; the player never waits
const _liveCache = new Map(); // godverseShopId → { etag, gone:Set, prices:Map } (etag-cached per §4)
let _liveDown = false; // circuit breaker: one failure and we stop asking until reload
async function readLiveStock(base, shop, pack) {
if (_liveDown || !base || !shop.godverseShopId) return null;
const id = shop.godverseShopId;
const ctl = new AbortController();
const timer = setTimeout(() => ctl.abort(), LIVE_TIMEOUT_MS);
try {
const prev = _liveCache.get(id);
const r = await fetch(`${base}/godverse/v1/shop/${id}/stock`,
{ signal: ctl.signal, headers: prev ? { 'If-None-Match': prev.etag } : {} });
if (r.status === 304 && prev) { applyLive(pack, prev); return prev; }
if (!r.ok) return null; // 404 = not a real shop. Tier 1. Silently.
const d = await r.json();
const live = { etag: d.etag, gone: new Set(d.gone || []),
prices: new Map((d.items || []).map((i) => [i.id, i.price])) };
_liveCache.set(id, live);
applyLive(pack, live);
return live;
} catch (e) {
// Dead / unreachable / slow / aborted → tier 1 EXACTLY, and we stop asking. The offline law is not
// a fallback path here, it is the same path: the pack was already complete before we asked.
_liveDown = true;
return null;
} finally { clearTimeout(timer); }
}
function applyLive(pack, live) {
if (!pack) return;
pack.gone = live.gone; // C omits these AFTER the draw (§8.2)
for (const it of (pack.items || [])) { // live price, IN PLACE (§8.3) — no reshuffle
const p = live.prices.get(it.id);
if (p != null) it.price = p;
}
}
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false,
dig: digEnabled = false, stockReal = false, wallet = null,
occupancyOf = null, gigState = null, rosterOf = null }) {
occupancyOf = null, gigState = null, rosterOf = null,
liveBase = null, sourcingOf = null }) {
const scene = new THREE.Scene();
scene.background = new THREE.Color('#0e0b07');
@ -214,6 +263,13 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
// for taking tolerances out. Lookup only: a shop with no pack gets null → parody, silently.
const gBase = stockReal ? godverseBaseFor(shop) : undefined;
currentAdapter = stockReal ? makeStockAdapter(getStockPack(shop.type, gBase)) : null;
// [R27 §2] Tier 2, fire-and-forget. ONLY for a real-sourced shop: G's fence 404s a mint shop, and
// asking anyway would be R26's blind probe wearing a server. The manifest already told us which is
// which, so we ask only where an answer exists. No await — the room is already built and complete.
if (stockReal && gBase && liveBase && sourcingOf && sourcingOf(shop.godverseShopId) === 'real') {
const pack = getStockPack(shop.type, gBase);
if (pack) readLiveStock(liveBase, shop, pack);
}
// [Lane F R13 THE DISTRICT] Is THIS shop the venue with a gig on right now? Per-venue query — a town has
// 24 venues, each with its own latch keyed by shop id. Lane C only stamps room.audio.gigKey when we pass
// opts.gig, so pass NULL unless it's really on: a quiet-night venue then builds as a normal interior