diff --git a/C-progress.md b/C-progress.md index 8955952..a07a209 100644 --- a/C-progress.md +++ b/C-progress.md @@ -7,6 +7,51 @@ Last updated: 2026-07-17 (round 26) · owner: PROCITY-C · reviewer: Fable --- +## Update 2026-07-17 (round 27, v5.0 THE LIVE CRATE) — the tier-2 seam + LANE_C_PUB v5-FROZEN + +R27 §Lane C (ledgers #2 + #6). Wave 1 — ran after F tagged `v5.0-beta` (`78848ed`, 7/0/0). + +### The seam ruling (§8): bless the direct wire — but the *form* was the real question +Fable asked: publish a post-build hook, or bless the direct wire? **Answer: bless it — no lifecycle hook.** +F already holds `currentAdapter` (`interior_mode.js:67`) and passes it to `dig.open()` **per dig** (`:111`), +so the live read lands, F updates the pack, and the next riffle picks it up. Read not landed / slow / dead ⇒ +the adapter is simply the tier-1 pack ⇒ the dig never blocks. No C lifecycle change. + +**But the question underneath it — *how* `gone` applies — is C's, and the obvious wiring is wrong.** +`pickRealOffers` indexes `pack.items[(rr()*len)|0]`, so **filtering the array reshuffles the crate**. +Measured across 5 crates: **one record selling changed 21 OTHER records** (6/2/2/6/5 collateral). Pick-then- +omit — draw from the stable full list, drop gone *after* — has **0 collateral**: a crate with 3 sold shows +13, not a different 16. Selling one record must remove one record, not re-stock the shop. + +**And the trap: both forms hide the sold record.** So F's planned sandbox assertion — "a real `gone` item +never renders" — **goes green on the wrong wiring**. It cannot see the 21. → **F: assert on collateral (the +survivors are unchanged across a `gone` delta), not absence.** That's the vacuous-gate law's third +application, and the cheapest to miss, because it passes. + +**Landed** (`dig.js`): `pack.gone` (Set|array) omitted at pick time; **F's invariant published** — update +item *fields* in place (live price) freely, **never add/remove/reorder `items[]`**; that identity+order IS +the seeded pick's stability (the same lesson as R25's positional ids, one layer up). +**Determinism boundary sits exactly here:** server down ⇒ byte-identical tier 1 (gate-able); server up ⇒ the +crate legitimately changes — tier 2 is non-deterministic *by design*, so **no gate may assert dig determinism +with the server up**, and `pack.gone` never enters a seeded stream or a golden. +**Scoped honestly:** the bin fan is decorative (a different seeded pick from the dig's offers), so a sold +cover may linger there until rebuild — that's when I'd publish a post-build hook, and the epoch's claim +doesn't need it, because *the crate you riffle is true*. + +**Verified** — pure-function measurement for the collateral claim, then end-to-end through the real dig's +public surface (pull → Esc → scroll → pull): sold record gone · **survivors 4/4 · collateral 0** · `items[]` +untouched at 120 · live in-place price shows on the next pull. *My first dig harness returned the same title +4× (I never un-pulled), so its "survivors" number was a harness artifact — I re-ran it properly rather than +report a metric I'd just watched lie.* + +### Docs freeze (§6): LANE_C_PUB is **v5.0-FROZEN**, whole document +Marked, with the case law kept at the top rather than buried: three of this doc's amendments exist because a +rule of mine met a case written *after* it (§7's naming vs its own loader; §7.2a's `sku_` before mint) — and +both times the lane building to my rule took the red. The header now says it plainly: **when a line here +meets a case it didn't foresee, the line is the stale thing — amend it, don't bend the case.** + +--- + ## Update 2026-07-17 (round 26 wave 4) — the mint-id fence: my rule was the stale thing F's fourth hold traced to **my** §7.2a. I wrote "`sku_`-prefixed for real-stock packs" in R25 — **before mint diff --git a/docs/LANES/LANE_C_PUB.md b/docs/LANES/LANE_C_PUB.md index 22dd77f..b6b5333 100644 --- a/docs/LANES/LANE_C_PUB.md +++ b/docs/LANES/LANE_C_PUB.md @@ -1,4 +1,11 @@ -# LANE C — interiors contract (venues + real-town class mapping) → Lanes A/D/E/F +# LANE C — interiors contract (venues · real-town mapping · real stock) → Lanes A/D/E/F/G + +> **v5.0-FROZEN · 2026-07-17 · the whole document.** Every section below is the shipped, measured state of +> Lane C's contracts across five epochs. Changes from here are version-bumped amendments, recorded in the +> marker list — the same discipline §0–§5 have carried since v3.0. **Case law worth keeping:** three of this +> doc's amendments exist because a rule of mine met a case written *after* it (§7's naming vs the loader, +> §7.2a's `sku_` before mint existed) — the lane building to the rule took the red both times. When a line +> here meets a case it didn't foresee, **the line is the stale thing.** Amend it; don't bend the case. > **v3.0-FROZEN** · 2026-07-16 · durable contract for Lane D + Lane F. §0–§5 describe the *shipped* > venue state; changes require a version bump (CITY_SPEC amendment law). Verified R13 (build) + R14 (audit). @@ -413,3 +420,45 @@ Fresh context, against G's merged `stock_godverse/3962749/` (120 real records): - **Collision fixed:** shop pack (120 items) and town pack (350) coexist as distinct objects. - **Fail-soft with a POPULATED cache:** missing base ⇒ `null` ⇒ parody (the R23 bug is gone). - **Ceilings:** 98 draws with real covers via `opts.stockBase`; `pathOK`; 0 carves; 0 console errors. + +--- + +## 8. Tier-2 live enrichment — the seam (v5.0, R27 ledger #2) → Lane F + +**Ruling: bless the direct wire — no new lifecycle hook. But the `gone` *application form* is C's, and it's +landed in `dig.js`.** The question wasn't hook-vs-no-hook; it was *how* `gone` applies. + +**1. Lifecycle — no hook needed.** F already holds `currentAdapter` (`interior_mode.js:67`) and passes it to +`dig.open()` **per dig** (`:111`) — a fresh read every time a crate is opened. When the live read lands, F +updates the pack; the next dig picks it up. If the read 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) and no C +lifecycle changes. + +**2. The form — pick-then-omit, NEVER filter-the-array.** +`pack.gone` = a `Set`/array of ids the live read reports sold. C picks from the **full, stable `items[]`** +and omits gone ids **after** the draw. +- **Why (measured, R27, 5 crates):** filtering `items[]` first **reshuffles the crate** — every pick indexes + by position, so **one sale changed 21 OTHER records**. Pick-then-omit: **0 collateral**. A crate with 3 + sold shows 13, not a different 16. +- **Both forms hide the sold record** — so **“the gone item never renders” cannot tell them apart.** → **F: + the sandbox proof (#4) must assert on *collateral* (the survivors are unchanged across a `gone` delta), + not just absence. The obvious assertion is vacuous here.** (Vacuous-gate law, third application — and the + cheapest one to get wrong, because it goes green.) +- **Proven in the real dig:** sold record gone · survivors **4/4** kept · **collateral 0** · `items[]` + untouched (120). + +**3. F's invariant — `items[]` identity and order ARE the pick's stability.** +- **Do:** update item *fields* in place (live `price`) — free, no reshuffle; the card reads `it.price` at + pull time, so a live price shows on the next pull. +- **Never:** add, remove, or reorder `items[]`. That is what `pack.gone` exists for. + +**4. The determinism boundary sits exactly here.** Server down ⇒ no `gone`, no live fields ⇒ the dig is +tier 1, deterministic and gate-able. Server up ⇒ the crate legitimately changes as records sell — tier 2 is +non-deterministic **by design** (charter law #2). So **no gate may assert dig determinism with the server +up**, and no live value may enter a seeded stream or a golden: `pack.gone` is consumed at pick time and +never persisted. + +**5. The bin fan is decorative, not inventory.** The room's visible sleeves are built at `buildInterior` from +a *different* seeded pick than the dig's offers — a fan, not the crate's contents. A sold record's cover may +linger in the fan until the room rebuilds. If we ever want the fan to honour `gone`, **that** is when I'd +publish a post-build hook; it is not needed for the epoch's claim, because the crate you *riffle* is true. diff --git a/web/js/interiors/dig.js b/web/js/interiors/dig.js index 53551e4..e30581c 100644 --- a/web/js/interiors/dig.js +++ b/web/js/interiors/dig.js @@ -286,10 +286,21 @@ export function createDig(THREE, renderer) { document.addEventListener('keydown', onKey, true); // ?stock=real: seeded pick of real pack items (same bin seed → same crate contents). + // TIER-2 SEAM (v5.0): `pack.gone` = ids the live read says sold IN THE REAL SHOP. We pick from the + // FULL, stable `pack.items` and omit gone ids AFTER the draw — never from a filtered array. + // Measured (R27, 5 crates): filtering the array first reshuffles the crate — one sale changed 21 OTHER + // records, because every pick indexes by position. Pick-then-omit: 0 collateral, the sold record simply + // leaves and the survivors keep their slots (a crate with 3 sold shows 13, not a different 16). + // Both forms hide the sold record, so "the gone item never renders" cannot tell them apart — the + // collateral is what a gate must assert on. F: update item FIELDS in place (live price) freely; do NOT + // add, remove, or reorder `pack.items` — that identity/order IS the seeded pick's stability. function pickRealOffers(pack, seed, n) { const rr = mulberry32(seed >>> 0), out = []; + const gone = pack.gone instanceof Set ? pack.gone + : (Array.isArray(pack.gone) ? new Set(pack.gone) : null); for (let i = 0; i < n; i++) { const it = pack.items[(rr() * pack.items.length) | 0]; + if (gone && gone.has(it.id)) continue; // sold in the real shop → this slot is simply empty // 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.