Lane C R30 (v7.0-alpha ledger #2, contract first): LANE_C_PUB §9 — the sell counter. Offer = min(p−1, max(1, floor(p·0.5))): the no-pump law structural, not tuned; type=type strict, fail-closed on untyped items; onSell mirrors onBuy (the proven wallet seam); collection read-only, removal via the game API

This commit is contained in:
m3ultra 2026-07-18 14:04:59 +10:00
parent 83ef8c9234
commit c8503a5a2e

View File

@ -29,6 +29,9 @@
> **v5.0-beta amendment (2026-07-17, R26 wave 4):** **§7.2a's id form is now SOURCING-SCOPED** —
> `sku_<POS id>` for real, `mint_<listing id>` for mint. **Prefixes are namespace fences**, not decoration:
> my R25 line was written before mint existed and read as one space where there are two. Doc-only.
> **v7.0-alpha amendment (2026-07-18, R30 ledger #2):** **§9 added — the sell-counter contract** (the
> no-pump law made structural: `offer(p) = min(p1, max(1, floor(p·0.5)))` — strictly below the buy side
> for every p ≥ 1, by construction). §0§8 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
@ -462,3 +465,85 @@ never persisted.
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.
---
## 9. The sell counter (v7.0-alpha, R30 ledger #2) → Lanes F/B
**The mirror of the buy card.** Walk to the keeper's counter holding items the shop trades in → a sell
card (the same manila price-sticker treatment as the dig's pull panel): item, OFFER, SELL. Module:
`web/js/interiors/sell.js` — DOM-only (zero draws, zero GPU resources), same consumer pattern as
`dig.js` (C ships the interaction; F owns the in-game input/mode hook).
### 9.1 THE OFFER — the no-pump law, structural (the multiplier and why)
```js
sellOffer(pricePaid) = min(pricePaid 1, max(1, floor(pricePaid × 0.5))) // and never < 0
```
- **The multiplier is 0.5** — the classic secondhand-dealer margin (a keeper pays about half of what
he'll sticker it at; he has to eat too). It keeps selling *meaningful* (half your money back beats a
full crate of duds) without being an income source.
- **The `min(pricePaid 1, …)` clamp is the law made structural, not tuned:** the offer is **strictly
below what you paid for every `pricePaid ≥ 1`** (at `p = 1` the offer is $0 and the SELL button
disables — the keeper won't buy what he can't sticker; at `p = 2` the naive `max(1, floor(p/2)) = 1`
would meet the clamp anyway — the clamp exists so no future retune of the multiplier can ever push an
offer to break even). **Buy-then-immediately-sell therefore loses ≥ $1 per round trip, always**
F's no-pump gate (`R30 ledger #5`, monotonic cash loss) holds **by construction**, and a change to
this formula that breaks that property is a contract violation, not a balance pass.
- **Alpha basis = `pricePaid`.** The alpha carries no guide-band data (the BIBLE bands are beta), and
in the alpha every buy happens at the shop's asking price — so `pricePaid` IS the buy-side price the
player faced. **Beta swaps only the basis, not the shape:** when E's guide bands land, the offer
becomes `min(bandLow 1, max(1, floor(bandLow × 0.5)))` — below the band's *low* edge by
construction, so a gem bought under band still nets its finder margin only against the band, never
against the keeper. The formula's clamp survives the swap untouched.
### 9.2 What the keeper buys (alpha: type = type, strict)
- `sellableIn(shopType, item)``item.type === shopType`. A record shop buys records, a book shop
books — **any** shop of the type (per-keeper taste, haggling, pawn-buys-everything: all beta, not
built).
- **An item with no `type` is NOT sellable — fail-closed** (vacuous-gate law: a matcher that passes an
untyped item would "match" everywhere). **→ Lane F ASK:** stamp `type` (the registry stock type:
`record`/`book`/`toy`/…) on every collection item at buy time — the R30 published shape
(`{townKey, godverseShopId|shopId, sku|slotId, pricePaid, dayFound}`) doesn't carry it, and without
it nothing is sellable anywhere. Carrying `title`/`artist` too is strongly asked (B's collection UI
needs them; my card falls back to the sku/slotId label when absent — ugly but honest).
### 9.3 The seam — money like buying, removal via the game API only
- **C consumes `window.PROCITY.game.collection` READ-ONLY.** The card never splices, reorders, or
mutates it (same species of law as §8.3's `items[]`).
- **`onSell(item, offer)`** is the single side-effect callback — the exact mirror of the dig's
`onBuy(offer)` (the proven wallet seam). The **consumer** (F) does both halves: credit the wallet,
remove the item from the collection **through the game API**. `onSell` returning `false` = keeper
veto: card unchanged, nothing moves.
- `wallet.sell(item, offer)` is provided (C-owned `wallet.js`, mirrors `buy()`): credits `offer`,
drops the matching v0-inventory entry, notifies. F binds it exactly like it binds `wallet.buy`.
- Till ring: the sale rings `'till'` from the counter emitter (the R28 one-shot pattern, same
distance-gain math as the dig's) — silent-and-happy with no engine / `?mute` / `?noassets`.
### 9.4 The trigger + the card
- **Where:** within **`SELL_DIST = 2.0 m`** of `room.counter.pose` (bench centre, room-local — the
frame the interior camera walks in). Helper exported: `nearCounter(room, pos, dist?)`.
- **Suggested E routing for F** (matches my harness): aimed bin → dig (unchanged) · else near counter
with ≥ 1 sellable → sell card · else shelf-buy. Pointer unlocks while the card is up (dig precedent).
- **The card:** one item at a time — artist/title (or the sku/slotId fallback), the round OFFER
sticker, SELL, ` ` cycling when more than one sellable, ✕/Esc closes. `open()` returns `false`
(no card, no DOM) when nothing is sellable.
### 9.5 Fail-soft (the classic-pure law, C's half)
No game layer (`?classic=1` / `?game=0``window.PROCITY.game` absent) → the consumer never opens
the sell UI, **and** `open()` itself returns `false` on a missing/empty item list — zero errors, zero
DOM, zero localStorage. `createSell()` allocates DOM only; `dispose()` removes every node + listener.
### 9.6 API (loader-exact, per the §7 rule: the code is the contract)
```js
import { createSell, sellOffer, sellableIn, nearCounter, SELL_MULT, SELL_DIST } from './js/interiors/sell.js';
const sell = createSell();
sell.open({ shopType, shopName, items, getCash, onSell, onClose, emitters }) // → bool (false = nothing sellable)
sell.close(); sell.dispose(); sell.active // mirrors dig.js
```