diff --git a/C-progress.md b/C-progress.md index 45ff1a8..34fb401 100644 --- a/C-progress.md +++ b/C-progress.md @@ -3,7 +3,62 @@ *Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.* -Last updated: 2026-07-17 (round 25) · owner: PROCITY-C · reviewer: Fable +Last updated: 2026-07-17 (round 26) · owner: PROCITY-C · reviewer: Fable + +--- + +## Update 2026-07-17 (round 26, v5.0-beta EVERY CRATE DIFFERENT) — three contract lines + the price sticker + +R26 §Lane C, wave 1 (ledgers #1, #4, #6). Contract lines published FIRST — G and E build to them. + +### The brief said `tier`. It can't be `tier` — that name is taken twice (§7.2b) +G's atlases already emit **`tier: 1`** = the **charter's ladder rung** (0 parody · 1 static-real · 2 live), +and `kind` is already the stock type. **A mint atlas is also rung 1** — same mechanism (a statically baked +per-shop atlas); only the data's *origin* differs. Real-vs-mint is an **orthogonal axis, not a rung**. +Overloading `tier` would put two meanings in one name — the same species as the plan-id/godverse-id +collision that just became a standing note, and as R25's positional ids. **Ruling: top-level +`sourcing: "real" | "mint"`, REQUIRED; `tier` stays the numeric rung.** A mint index MUST NOT carry +POS-snapshot fields (the separation IS the honesty); gates read `sourcing`, and an index without it FAILS. + +### `condition` isn't in the index — the ledger assumed it was (§7.2c) +Ledger #4 said the card gets "the fields the atlas index already carries (artist/title/price/**condition**)". +It carries artist/title/price/price_band — **not condition**. G's *snapshot* has `condition` + `sleeve_cond` +and drops them at emit, so `pickRealOffers` mapped `cond: ''` and the card rendered dangling separators +around a hole. That *was* the "database row". Ruled: `condition`/`sleeve_cond` are **optional** index fields, +emitted when the source has them; **no invented grades** on mint items — a plausible-but-fictional grade is +exactly what `sourcing` exists to prevent. + +### The atlas manifest (§7.2d) — existence declared, never probed +`web/assets/stock_godverse/index.json` is the authority for which shops carry atlases; **no entry ⇒ no +fetch ⇒ zero 404s**, so the zero-console-errors law holds with no attribution exception. Shape + both-ways +consistency (atlas without manifest entry FAILS, and vice versa) in §7.2d. A tree with no godverse atlases +and no manifest is a legitimate silent zero-shop state, not an error. + +### The price sticker (ledger #4, code) +The pull panel is now the shop's **price sticker**, not a data readout: manila card, slight rotation + +shadow, typewriter caps, italic title, a rubber-stamped condition grade, and **the round yellow $ sticker +the sleeve art already uses** (dig.js's own canvas language — the panel now echoes it instead of fighting +it). Fields compose only when present: **a missing field never leaves a stray separator.** + +**Verified — five card shapes, both star branches, fresh context:** +| case | card | +|---|---| +| real grail | `$28` · Deep Dish · *Penetrate Deeper* · `VG+/VG` · **★ GRAIL** | +| real standard, graded | `$15` · `VG+` · `STANDARD` | +| real ungraded (today's index / mint) | `$15` · `COLLECTOR` — grade omitted cleanly | +| parody | `$15` · THE VERANDAHS · *Tinny* · `NM` · `1973 PUNK` | +| parody rare (seed 4) | `$16` · HILLS HOIST · *Servo at Midnight* · `VG` · `1973 POP` · **★ RARE** | + +**The verification caught a defect in my own card:** a real *grail* rendered **"GRAIL ★ GRAIL"** — `s` carried +the price_band *and* `rare` derived from that same band. (Parody is immune: its `s` is a genre.) Fixed at the +mapping — grail becomes the ★ stamp and leaves `s` empty. And I did not leave the parody-rare branch on +assumption: scanned seeds until a rare pull appeared (seed 4) and asserted it says RARE, not GRAIL. An +unexercised branch is an unproven one. + +**Budgets:** the card is DOM — zero draws, zero tris, no 3D change. Ceilings untouched. +**→ G:** `sourcing` + `condition`/`sleeve_cond` + the manifest. **→ E:** assert `sourcing` (absent ⇒ FAIL), +the mint/POS-field separation both ways, and manifest↔files both ways. **→ F:** consult the manifest before +fetching — that's what retires the 404 attribution exception. --- diff --git a/docs/LANES/LANE_C_PUB.md b/docs/LANES/LANE_C_PUB.md index f1bc196..f4b9a75 100644 --- a/docs/LANES/LANE_C_PUB.md +++ b/docs/LANES/LANE_C_PUB.md @@ -15,6 +15,10 @@ > **v5.0-alpha amendment (2026-07-17, R25 ledger #3):** **§7.2a added — slot ids must be sku-derived,** > unique across packs and stable across re-emits (F's id-collision finding, made structural). Index-only; > no atlas bytes, no goldens. §0–§6 unchanged. +> **v5.0-beta amendment (2026-07-17, R26 ledger #1/#4/#6):** **§7.2b `sourcing` (real|mint — NOT `tier`, +> which is already the ladder rung), §7.2c `condition` for the price card, §7.2d the atlas manifest** +> (existence declared, never probed — kills the 404 attribution exception). All index/manifest-only: +> no atlas bytes, no town caches, no goldens. §0–§6 unchanged. *The district ships **three** venue archetypes behind `?gigs=1` — `pub`, `band_room`, `rsl`. Lane A converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off @@ -286,6 +290,57 @@ must not emit into it.** **→ Lane E:** assert the form — `id` present, **unique within the pack**, and `sku_`-prefixed for real-stock packs. +### 7.2b `sourcing` — real vs mint, and why it is NOT `tier` (v5.0-beta, R26 ledger #1) +The beta needs every keyed crate marked as **actual POS inventory** or **seeded plausibility**, so no gate, +doc or human mistakes one for the other. The brief called that field `tier`. **It can't be `tier`: that name +is already taken, twice over.** +- G's atlases already emit **`tier: 1`** = the **charter's ladder rung** (0 parody · 1 static-real · 2 live + GODVERSE). A mint atlas **is also rung 1** — same mechanism (a statically baked per-shop atlas); only the + *data's origin* differs. And `kind` is already the stock type (`record`). +- So real-vs-mint is an **orthogonal axis**, not a rung. Overloading `tier` would put two meanings in one + name — the same species as the plan-id/godverse-id collision now standing-noted, and as the positional + ids in §7.2a. One name, one meaning. + +**Rule: top-level `sourcing: "real" | "mint"`, REQUIRED on every real-stock atlas index.** `tier` stays the +numeric ladder rung, unchanged. +- **`"real"`** — baked from that shop's own POS snapshot (Monster Robot Party). The index carries its + `snapshot` provenance (§7.3). +- **`"mint"`** — a deterministic seeded sample (`random.Random(shop_id)`) standing in for a keyed shop with + no POS. Plausible, never claimed as that shop's stock. **A mint index MUST NOT carry POS-snapshot fields** + (the separation IS the honesty — E asserts both directions). +- **Gates read `sourcing`** (vacuous-gate law: an index with no `sourcing` FAILS). A mint crate can never + report as real. The dig card's treatment is C's call — see §7.2c. + +### 7.2c Price-card fields — `condition` (v5.0-beta, R26 ledger #4) +The dig card is a **record-shop price sticker**, and a sticker's fields are artist / title / **condition** / +price. `condition` is **not in the index today** — G's snapshot carries `condition` + `sleeve_cond` per item +and drops them at emit, so `pickRealOffers` currently maps `cond: ''` and the card renders dangling +separators around a hole. That's the "database row" the ledger wants gone. +**Rule: `condition` (and optionally `sleeve_cond`) are OPTIONAL index item fields; emit them when the source +has them.** Index-only; the loader never reads them (§7.3). C's card renders whatever is present and omits +what isn't — a missing field must never leave a stray separator. **Mint items:** emit `condition` only if the +sampled listing genuinely carries one; a plausible-but-invented grade is exactly the kind of fiction +`sourcing` exists to prevent. + +### 7.2d The atlas manifest — existence is declared, never probed (v5.0-beta, R26 ledger #6) +Fail-soft-by-404 collides with the zero-console-errors law: the runtime probes per-shop atlases blind and +eats the 404 noise (8 in F's R24 gate run), tolerated by attribution — a workaround, not a design. +**Rule: `web/assets/stock_godverse/index.json` is the authority for which shops carry atlases. No entry ⇒ no +fetch.** Shape: +```json +{ "version": 1, + "shops": [ { "godverseShopId": 3962749, "types": ["record"], "sourcing": "real" }, + { "godverseShopId": 31, "types": ["record"], "sourcing": "mint" } ] } +``` +- **`godverseShopId` is a number in the GODVERSE namespace** — never compare it to a plan id (standing note). +- **→ F:** consult the manifest before fetching; no entry ⇒ no probe ⇒ **zero 404s, no attribution + exception**. A shop in the manifest whose fetch still fails keeps the existing fail-soft → parody. +- **→ G:** emit it alongside the atlases (mint entries included). +- **→ E:** manifest↔files consistency **both ways** — an atlas on disk absent from the manifest FAILS, and a + manifest entry with no atlas FAILS. +- **Manifest absent entirely** (a tree with no godverse atlases): that is a legitimate zero-shop state — the + wire probes nothing and stays silent. Not an error. + ### 7.3 Fields the runtime IGNORES — and the provenance ruling (the drift dies here) `version`, `atlas_px`, `cell`/`cell_w`/`cell_h`, `count`, `kind`, `tier`, `shop`, `crate`, `provenance` — **`stockpack.js` reads none of them.** So the authority for their shape is not C's diff --git a/web/js/interiors/dig.js b/web/js/interiors/dig.js index 947acf7..53551e4 100644 --- a/web/js/interiors/dig.js +++ b/web/js/interiors/dig.js @@ -113,13 +113,21 @@ export function createDig(THREE, renderer) { const css = el('style'); css.textContent = ` .pcdg-hint{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:rgba(0,0,0,.7);color:#d8d8e0;padding:6px 14px;font:13px "Courier New",monospace;pointer-events:none;display:none;z-index:60} .pcdg-cur{position:fixed;left:50%;top:64px;transform:translateX(-50%);color:#ffd23d;font:700 15px "Courier New",monospace;text-shadow:0 1px 4px #000;pointer-events:none;text-align:center;max-width:80%;display:none;z-index:60} - .pcdg-panel{position:fixed;right:6%;top:50%;transform:translateY(-50%);width:250px;background:#0e0e16;border:1px solid #ffd23d;padding:16px;color:#d8d8e0;font:14px "Courier New",monospace;display:none;z-index:61} - .pcdg-panel h3{margin:.1em 0;color:#ffd23d;font-size:15px} - .pcdg-panel .meta{color:#8a8a9a;font-size:12px;margin:4px 0 8px} - .pcdg-panel .price{font-size:24px;color:#3dff8b} - .pcdg-panel button{margin-top:12px;width:100%;padding:9px;border:1px solid #666;background:#1d1d2b;color:#d8d8e0;font:inherit;cursor:pointer} - .pcdg-panel button:hover{border-color:#3dff8b;color:#3dff8b} - .pcdg-panel .x{position:absolute;top:6px;right:11px;cursor:pointer;color:#888} + /* The pull panel is the shop's PRICE STICKER, not a data readout: manila card, typewriter ink, the + round yellow $ sticker that already sits on the sleeve art (see the sleeve canvas above), a rubber- + stamped condition grade. Fields render only when present — a missing one leaves no stray separator. */ + .pcdg-panel{position:fixed;right:6%;top:50%;transform:translateY(-50%) rotate(-1.1deg);width:262px;background:#f4ecd8;border:1px solid #cbbf9e;box-shadow:0 10px 26px rgba(0,0,0,.55);padding:15px 16px 14px;color:#241f18;font:14px "Courier New",monospace;display:none;z-index:61} + .pcdg-panel .sticker{float:right;width:54px;height:54px;border-radius:50%;background:#f4e11a;border:1px solid #d3bf00;color:#c1121f;font:700 18px Arial;line-height:54px;text-align:center;transform:rotate(5deg);margin:-2px -4px 4px 10px;box-shadow:0 1px 3px rgba(0,0,0,.25)} + .pcdg-panel h3{margin:.1em 0 3px;color:#241f18;font-size:15px;font-weight:700;text-transform:uppercase;letter-spacing:.02em} + .pcdg-panel .ttl{color:#4c4436;font-size:13px;font-style:italic;margin:0 0 9px;line-height:1.25} + .pcdg-panel .stamps{clear:both;display:flex;gap:6px;align-items:center;flex-wrap:wrap;min-height:1px} + .pcdg-panel .grade{border:1.5px solid #b3121f;color:#b3121f;padding:1px 6px;font:700 11px "Courier New",monospace;letter-spacing:.08em;transform:rotate(-2.5deg)} + .pcdg-panel .tag{color:#6f6553;font-size:11px;letter-spacing:.05em} + .pcdg-panel .star{color:#b3121f;font-size:13px} + .pcdg-panel button{margin-top:13px;width:100%;padding:9px;border:1px solid #8a7f66;background:#e7dcc2;color:#241f18;font:700 13px "Courier New",monospace;letter-spacing:.06em;cursor:pointer} + .pcdg-panel button:hover:not(:disabled){background:#241f18;color:#f4e11a;border-color:#241f18} + .pcdg-panel button:disabled{color:#9a917c;border-color:#c3b99f;cursor:default} + .pcdg-panel .x{position:absolute;top:4px;left:8px;cursor:pointer;color:#a89e86;font-size:13px} .pcdg-out{position:fixed;left:16px;top:16px;background:#1d1d2b;border:1px solid #666;color:#d8d8e0;padding:8px 14px;font:13px "Courier New",monospace;cursor:pointer;display:none;z-index:60} .pcdg-out:hover{border-color:#3dff8b;color:#3dff8b} .pcdg-toast{position:fixed;left:50%;top:40%;transform:translate(-50%,-50%);background:#0e0e16;border:1px solid #3dff8b;color:#3dff8b;padding:14px 22px;font:700 15px "Courier New",monospace;z-index:62;display:none}`; @@ -211,10 +219,21 @@ export function createDig(THREE, renderer) { function showPanel(entry) { const o = entry.offer; const cash = getCashCb ? getCashCb() : 999; - panel.innerHTML = `

${o.a}

` - + `
“${o.t}” · ${o.y} ${o.s} · ${o.cond}${o.rare ? ' · ★ RARE' : ''}
` - + `
$${o.price}
` - + ``; + // A price sticker, composed from what this item ACTUALLY has. Real pack items carry no year (and + // may carry no grade); procedural ones carry both. Build the stamp row from present parts only — + // never emit a separator around a hole (that's what read as a database row). §7.2c. + const esc = (s) => String(s).replace(/[&<>"]/g, m => ({ '&':'&','<':'<','>':'>','"':'"' }[m])); + const stamps = []; + if (o.cond) stamps.push(`${esc(o.cond)}${o.sleeveCond ? '/' + esc(o.sleeveCond) : ''}`); + const line = [o.y, o.s].filter(Boolean).join(' '); // "1974 SOUL" | "SOUL" | "" — never " · " + if (line) stamps.push(`${esc(line)}`); + if (o.rare) stamps.push(`★ ${o.real ? 'GRAIL' : 'RARE'}`); + panel.innerHTML = `
` + + `
$${esc(o.price)}
` + + `

${esc(o.a)}

` + + (o.t ? `
“${esc(o.t)}”
` : '') + + (stamps.length ? `
${stamps.join('')}
` : '') + + ``; panel.style.display = 'block'; panel.querySelector('.x').onclick = unpull; const b = panel.querySelector('button'); @@ -271,8 +290,16 @@ export function createDig(THREE, renderer) { const rr = mulberry32(seed >>> 0), out = []; for (let i = 0; i < n; i++) { const it = pack.items[(rr() * pack.items.length) | 0]; + // Real fields straight off the atlas index (§7.2c): condition/sleeve_cond are OPTIONAL — present on + // POS-sourced items, absent on mint ones that had no genuine grade. `y` (year) is never in the index, + // so it stays empty and the card simply omits it. No invented values: an unstated grade stays unstated. + // `grail` becomes the ★ stamp, so it must NOT also ride in `s` — otherwise the card reads + // "GRAIL ★ GRAIL". (Procedural sleeves are unaffected: their `s` is a genre, not the band.) + const band = (it.price_band || '').toUpperCase(); out.push({ real: true, pack, item: it, a: it.artist, t: it.title, price: it.price, - s: (it.price_band || '').toUpperCase(), y: '', cond: '', rare: it.price_band === 'grail' }); + s: band === 'GRAIL' ? '' : band, y: '', + cond: it.condition || '', sleeveCond: it.sleeve_cond || '', + rare: it.price_band === 'grail' }); } return out; }