111 lines
41 KiB
Markdown
111 lines
41 KiB
Markdown
# Ordinary Karama cost: pure preflight and recovery contract
|
||
|
||
Newest checkpoint: [Battle aftermath, movement cancellation and owner promises](BATTLE_AFTERMATH_PREFLIGHT.md) adds bounded shared cleanup and board/collection calculations, post-cost Auditor candidate projection and original-owner intended-use checks. Exhaustive suffix coverage and durable cost frames remain open.
|
||
|
||
Subsequent checkpoint: [Battle, revival and disaster preflight](BATTLE_REVIVAL_DISASTER_PREFLIGHT.md) adds shared revealed-battle calculations, per-location loss custody, canceled-revival quotes and storm/worm guards. Later aftermath, exhaustive response semantics and original-actor promise feasibility remain unfinished.
|
||
|
||
Later checkpoints: [initial pure Richese checks and non-cancel conversions](KARAMA_PREFLIGHT_AND_CONVERSIONS.md), then [cancellation identity, Moritani cleanup and Baliset arrival](KARAMA_CANCELLATION_CONTEXTS.md). The audit below is historical; those bounded prerequisites are now implemented, while exhaustive suffix/promise proof and cost-discard frames remain open.
|
||
|
||
Read-only source audit, 2026-09-07. Inspected current `game/engine.ts` and `docs/NEXT_CONSUMED_DISCARD_CONTRACT.md`. Engine SHA-256: `2e7eed285be1f2b74279c0436c7be5fae29ee9b5bd007cc56dab5986282e2433`. No production or test changes, no tests executed for this contract audit. References are to this snapshot.
|
||
|
||
## Finding
|
||
|
||
A pre-effect Karama discard cannot safely be persisted merely because `karamaSpendingBlock` accepted its card. Today the original public action remains atomic because `completeKarama`, all its continuations and voluntary promise checks execute on the same clone before it is returned. Several suffixes can reject after `discard`; others assume pending objects with non-null assertions and would fail or misapply a crossed save. A durable cost needs both a pure, direction-specific accepted-use validator and exact binding of the original opportunity. Do not run `completeKarama`, `finishResponse`, `resolveBattle` or the general normalizer as a speculative preflight: these can draw, shuffle, create UUIDs, alter phase, or produce successor discard frames.
|
||
|
||
The most concrete unsupported path is `finishResponse('richeseAuction',true)` at 9623: it always throws because canceled normal-auction counting remains unresolved. Reject this intended cancellation before spending either a printed card **or a BG Worthless card whose conversion would eventually execute it**. This is preservation of the existing implementation boundary, not a new rule answer.
|
||
|
||
## Four callers and what they already prove
|
||
|
||
| Use | Caller/pre-cost checks currently present | Necessary accepted-use binding before a durable cost |
|
||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `cancel` | `applyActionInner` response branch, 11869–11891: live response, action mode cancel, actor differs from response.owner, selected usable card. For Fremen it already calls `pendingFremenMovement` before any response action. `spendKarama` repeats custody validation. | Exact response kind/owner/passed/intent and optional fields; corresponding pending records; current phase/turn; actor and supported canceled branch. These generic checks do not validate every branch's pending record or suffix. |
|
||
| `shipment` | 14494–14507: phase 5, recipient is current active seat, unused shipment, no existing `karamaShipping`. | Seated recipient, owner, same turn and unused opportunity; actual consumed card ID written into use. No force shipment, arrival or payment has occurred. Preserve an explicit different recipient; do not restrict this to self. |
|
||
| `purchase` | 14510–14518: phase 3, a normal `g.auction` exists, actor below hand limit before spending. Generic overlay/decision/response gates already passed. | Exact original normal auction, index and physical card, valid current bidder/order metadata, no completed `currentAuctionSale`, and unchanged ability to receive this card. Do not invent Richese free purchase: this path calls `settleAuction`, not `settleRicheseLot`. Preserve the current pre-cost capacity requirement; spending at full hand does not currently authorize this action. |
|
||
| `auctionPayment` | 12765–12776: decision belongs to actor, boolean Karama choice, usable selected card. Dispatcher clears decision before calling `spendKarama`. | Must explicitly bind actor to original auction.bidder, current index/card, completed bidding/payment opportunity, and absent completed sale. Capture/validate the consumed decision before it is cleared or bind its authoritative auction equivalent. A `use.kind` alone does not prove winning-bid ownership. |
|
||
|
||
`karamaSpendingBlock` (4602–4628) already checks own physical held Karama/Advanced BG Worthless, late Snooper, gift reservation, Moritani retention, sealed battle plan, prescience commitment, and existing Worthless conversion. The enclosing dispatcher also rejects use of the reserved Black Market card. These checks are not general future-effect validators. Retain them before physical removal; recovery validates the receipt instead of demanding the card still be held.
|
||
|
||
`settleAuction(g,true)` (4526) skips spice payment, pushes the exact auction card, creates `currentAuctionSale`, then can offer Ix replacement, Harkonnen bonus or next lot. Do not preflight it by charging or revalidating the original spice bid as mandatory: Karama is precisely the alternative payment. Validate normal auction/card identity and structural eligibility without executing acquisition. Preserve free=true through Ix replacement and subsequent income handling; no Emperor paid-bid income should be created for a free purchase.
|
||
|
||
## Cancellation branch inventory
|
||
|
||
The table covers every current ResponseWindow kind, grouped only where the cancellation operation and required evidence are parallel. “No effect on cancellation” does not mean its response can be fabricated: still require the exact authenticated parent/opportunity.
|
||
|
||
| Target kind / source | Work reached on cancellation; concrete failure or binding requirement |
|
||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||
| `richeseAuction` 9622 | Unconditional unsupported-count RuleError. Reject before either form's cost. Do not choose a replacement count. |
|
||
| `richeseBlackMarket` 9630 | Marks unbid, settles and advances declaration. `finishRicheseLot` → `richeseDeclaration` (3907) throws if cache exhausted. Validate current Black Market lot/owner/round/card and the existing nonempty-cache continuation requirement before cost. No transfer or funding is needed for this canceled/unbid branch. |
|
||
| `atreidesAuction` 10019 | Normal lot changes only peek flag; requires actual normal auction. Richese lot also calls `settleRicheseLot`: its sold branch (3998–4020) requires reserved card, capacity, no Black Market self-purchase, exact winning funding and funded ally split. Extract these checks as a pure Richese settlement quote/validation; do not call settlement. Its unbid path can reach the exhausted-cache boundary above. Bind lot event, outcome, funding, card and current sale state. |
|
||
| `richeseGift` 9319 | Explicit pending owner/turn/phase check, records blocked card, restores parent controls. Validate intent owner/recipient/card/event and original resume. Gift transfer feasibility is intentionally **not** required when canceling; no transfer occurs. |
|
||
| `richeseNoField` 9378 | Explicit pending shipment own/allied No-Field owner check, clears pending and records block. Validate exact pending shipment/owner/current opportunity. Do not rerun the allowed shipment quote/arrival; cancellation performs neither. |
|
||
| `moritaniRetention` 9396 | Requires response-stage pending/owner/keep, then `finishMoritaniRetention` (9124) checks turn/phase, every played card still held, and resolved battle context. Discards those cards and creates a successor battleCleanup frame. Pure preflight must check the played IDs' unique physical custody and cleanup context before cost. `cleanupBattleContext` is not pure on legacy saves: it allocates a UUID and mutates context. Extract its condition checks; do not invoke it speculatively. |
|
||
| `moritaniDuke` 9407 | Requires phase5/current acquisitionTurn/Duke record, then `nextPhase`. Bind original owner/opportunity and downstream phase entry state; no acquisition on canceled branch. |
|
||
| `moritaniAlliance` 9426 | Explicit current Terror entry, allianceResponse stage/turn/phase; creates Moritani offer decision. Bind token/entry/owner and ensure response owner matches the actual Moritani opportunity, not merely any seated player. |
|
||
| `ecazPlacement` 9449 | Checks current phase4 placement opportunity and Ecaz owner; blocks remaining placement then `finishEcazPlacement` → completePhase. Requires actual ambassador state. No price quote needed for cancellation, because no placement/spice payment occurs. |
|
||
| `moritaniPlacement` 9488 | Checks phase8/pending turn/Terror state; marks placement used and calls victory continuation. No placement legality quote needed for canceled branch. Bind actual Moritani owner and pending selection rather than allowing unrelated response owner. |
|
||
| `choamWorthless` 9520 | Dereferences pending owner/effect; records blocked card. Crucially, suffix can resume storm, pending revival, pending movement, or Mentat decision via the pending flags. Bind those exact flags and parents. Canceled Baliset can call `resumeChoamMovement` → `completeMove` → unsupported arrival guard or final Ornithopter frame. See movement paragraph below. Do not discard the CHOAM effect card on cancellation. |
|
||
| `ixAuction` 9643 | Dereferences count and draws count cards, then setAuction. Validate integer count/current auction-preparation phase/owner and empty pending selection state. Do not require count cards available; existing draw loop tolerates shortage. No draw/refill in preflight. |
|
||
| `ixTechnology` 9657 | Clears pending substitution then offerAuctionPeek, which dereferences normal auction. Validate current upcoming card/auction and pending owner. No hand replacement validation needed when canceled. |
|
||
| `ixAllyCard` 9681 | Dereferences pending buyer/free, clears it, continues the already-paid sale. Bind actual currentAuctionSale, winner, exact acquired card, free flag, origin and its matching normal index or Black Market event. Cancellation must not pay the purchase again. It can credit seller, create Emperor/Harkonnen response, or advance lot. |
|
||
| `mobileStronghold` 9714 | Clears pending move and enters beginStormTurn; no relocation. Validate exact current turn's move/owner/mobile state and storm stage; do not run the allowed relocation effect. |
|
||
| `fremenMovement` 9720 | `pendingFremenMovement` (5301) explicitly validates current owner/turn/move, destination/amount and `validateMovementOrder`. This is already called by the live response dispatcher before cost and can be reused pure. Cancellation only installs one-move range block and clears intent. |
|
||
| `ixMovement` 9739 | Dereferences pending move/player, clears it and installs move block. Needs matching owner/current turn/move/order/resources shape; no actual move occurs. |
|
||
| `ixSubstitution` 9750 | Dereferences pending player/territory/cards; cancellation skips substitutions then `finishWinner` → cleanup/finishBattle. Bind resolved battle/typed losses/played card context and any remaining cleanup. Do not perform recover/kill calculation for denied substitution. |
|
||
| `choamRevival` 9770 | Requotes or rejects pending revival after disabling benefit, may continue `finishRevival`. Bind pending player/kind/count/typed tanks/cost/checks and real CHOAM response; use pure force-revival quote. Unfunded repriced revival is an accepted no-revival outcome, not a preflight rejection. |
|
||
| `revivalLimit`, `earlyRevival`, `foreignGhola` 9800 | Require pending revival/player/rules structures; canceled branch clears request and records the corresponding block. Validate bound request and relevant leader/KH identity; no revival effect. |
|
||
| `revivalDiscount` 9800 | Reprices request (possibly CHOAM force quote), then `finishRevival`. Validate quote/request/check sequence; unfunded price legitimately abandons revival. Do not impose a sufficient-spice rejection that removes this accepted cancellation. |
|
||
| `worthlessKarama` 9853 | Requires pending owner/use. On cancellation: clear pending; cancel-use restores its older response; auctionPayment calls recoverAuctionPayment; shipment/purchase perform no underlying use. Direction-sensitive nested rules below. |
|
||
| `choamSale` 9862 | Dereferences market owner/sale, blocks selected card then resumes Market. Validate current market/owner/sale/resume. Do not require quoteSale for canceled branch or discard sale card. |
|
||
| `advisorFlip` 9907 | No flip, but finishAdvisorReaction may offer advisor shipment, advance worm ride or begin battle declaration. Bind owner BG/location/resume/followup/remaining and their actual parent. No force-placement preflight for a future unaccepted advisor choice. |
|
||
| `capture` 9924 | Clears pending capture then finishBattle. Validate battle aftermath/capture owner+loser+territory. No random captive selection in canceled branch. |
|
||
| `choamAudit` 9956 | Explicit current pending response stage/event/owner, then finishAuditor(cancel) → finishBattle. Existing auditorIntegrity already validates much of this before cost. New frame must expose its suspended response to that pure validator before drain. No private inspection/payment occurs. |
|
||
| `choamBattleIncome` 9979 | Clears pending income then finishBattle. Bind owner/amount/completed battle and remaining aftermath. |
|
||
| `choamBattleAid`, `eliteStrength`, `fremenSupport`, `kwisatz` 9993 | Mutate current battle's corresponding denied ability, then nextCombatResponse. Bind active battle event, participants, applicable faction/beneficiary and response stage; prevent a stale response changing a later battle. |
|
||
| `guildTiming` 10007 | Sets locked and takes movementRemaining[0]. Bind nonempty current movement queue/phase/turn/Guild owner and original choice. Do not execute movement turn in preflight or infer the unresolved Sapho FIRST/Guild ruling. |
|
||
| `wormPlacement`, `wormAllyProtection`, `wormSurvival` 10029 | Continue Spice or worm survival/devour/afterWorm; can draw/refill spice and restore summoned-worm parents. Bind location/recipient, actual spice/worm sequence and summoned resume. Validate explicit No-Field reveal/removal shape without performing kills or draws. Not equivalent to simply clearing a response. |
|
||
| `stormProtection` 10046 | Storm route changes protection then traverses storm; shipment route kills declared amount at exact key/elite. Bind correct resume branch, resolution distance/traversal/pending or committed shipment group/typed casualty count. Storm advancement can later shuffle storm cards/technology; no effectful preview. |
|
||
| `harkonnenTraitor` 10108 | Changes the allied vote; if all votes complete calls resolveBattle. See explicit battle rejection checks below. Bind actual revealed plans/traitor-voter set and response owner. |
|
||
| `richesePurchaseIncome` 10119 | Explicit pending owner/turn/phase/amount3 check; clears income and restores resume. Validate exact pending purchase and parent records. No purchase replay or Emperor payment when canceled. |
|
||
| `emperorIncome`, `harkonnenBonus` 10144 | Advance already-completed sale through auctionBonus/nextAuction; bind current sale and original winning lot. No income/bonus draw on canceled branch itself, but advancing can enter further auction/phase initialization. |
|
||
| `revivalIncome`, `faceDancerReplacement`, `choamInflation`, `choamCharity`, `bgCharity`, `atreidesSpice`, `stormPeek`, `voice`, `prescience`, `emperorGift`, `emperorRevival`, `guildIncome`, `advisor` | Suppress the named effect or update its public cancellation flag. Require exact source owner/phase/context/amount/location as appropriate. `voice` deletes bound battle Voice; `prescience` deletes it and calls finishBattlePreparation. `faceDancerReplacement` still dereferences its owner.pool before checking canceled, so pool/identity shape is required. `advisor` canceled does not require reserves because no free force is sent. Guild/emperor transfer cancellation must not subtract money again. |
|
||
|
||
### Two concrete transitive rejection families
|
||
|
||
`resumeChoamMovement` catches `validateMovementOrder` failure and abandons movement without rejecting cancellation. Preserve that accepted outcome. If the order is valid, its `completeMove` is outside that catch and can still reject on No-Field event/custody, flight selection, shipment commitments, or unsupported arrival. `finishMovedGroup` → `openTerritoryEntry` (5531) rejects competing Ambassador/Terror/intrusion parents; a final Ornithopter move runs that same arrival guard in its clone preflight at 5114. Extract a pure completed-order/arrival eligibility check over predicted resulting occupancy and flags, respecting which invalid orders become an accepted no-move. Do not merely call resumeChoamMovement on a clone: marker movement creates a UUID and other paths create frames.
|
||
|
||
`resolveBattle` reached through canceled Harkonnen allied traitor has explicit throws for unresolved Stone physical winner (8405), missing chosen Stone mode (8413), empty casualty options (8594), existing pending Auditor (8661), and existing Moritani retention (8695). Its discarded played cards additionally need strict physical custody and any cleanup-context prerequisites. The comparison/casualty functions are pure reusable checks; full resolution is not a safe preflight. Prior battle/Stone/late-defense validators cover parts of this, but do not mistake them for validating every resolution suffix.
|
||
|
||
## BG Worthless direction and nested response
|
||
|
||
`spendKarama` discards first; BG then creates `pendingKarama={owner,use}` plus `worthlessKarama` response. A later allowance calls `completeKarama` with the original use; a later cancellation leaves the underlying effect unperformed. Before accepting BG's initial cost, validate that the intended allowed use has an executable supported suffix. Then bind the original opportunity across conversion, not only the immediate conversion owner.
|
||
|
||
A printed card canceling an existing Worthless conversion is different: its canceled suffix restores the old response or payment selection. **Do not recursively reject it just because executing the saved old use would be unsupported.** A legacy BG attempt to cancel Richese auction counting can be safely canceled, because the old Richese response is restored without canceling Richese. Validate the pending use's shape, owner and bound older context, then the restoration branch, not its allowed-effect semantics.
|
||
|
||
For canceled auctionPayment conversion, `recoverAuctionPayment` (4880) either reopens payment or restarts the same unrevealed lot. Preserve the existing explicitly provisional recovery; do not choose a new rule. Bind bidder/lot/card and ensure its opener loop has an eligible seat in a malformed-save check. Do not blindly require current spice sufficient: insufficient funding is the very recovery branch. A second BG Worthless conversion is already prohibited while pendingKarama exists; printed cancellation of it remains valid.
|
||
|
||
## Minimum extractable validation architecture
|
||
|
||
Use a typed pure `validateKaramaUseContext(g,p,physicalCard,use,stage)` with stages that distinguish initial declaration, saved cost receipt, allowed BG conversion and canceling BG conversion. Return a canonical bound use/opportunity description, not a whole replacement Game. Reuse a second pure `validateCanceledResponseContext` exhaustive over the response union, with explicit pending-parent mapping above. Extract pure quoted Richese settlement, battle-resolution prerequisites and post-move arrival checks where required. Shared `suspendedControlsIntegrity` can validate restored controls, but its current loose generic response shape is not the exhaustive branch proof.
|
||
|
||
This is not a reason to disable all complex cancellations: retain existing accepted paths by checking their actual prerequisites and recording exact original context. An opportunity signature binds a checked state across save/reload; it does not replace the initial semantic checks. Never perform RNG simply to determine whether the suffix is executable.
|
||
|
||
Current generic discard validator only allows pendingRicheseGift/pendingIxAlly/pendingAmbassador with Box/Truth frames. New cost frames require narrow allowances for the genuine bound cancellation parents. Auditor lookup currently sees only live, Box/Truth and gift resumes (9052); include exact Karama target/nested-use contexts or a valid canceled Auditor frame will fail before retirement. Do not detach all pending records wholesale: retain their live semantic context and bind them.
|
||
|
||
Retire the Karama frame before calling its suffix. A suffix may create a new battleCleanup, battleResolved or Ornithopter discard frame. No caller, automatic loop or promise search may consume a second card, draw, or advance the restored controls while that successor frame remains pending. Preserve each sequence/event and producer boundary separately.
|
||
|
||
## Voluntary promises and simulation are additional pre-cost fences
|
||
|
||
The current public wrapper calls `reconcileBattlePromises(g,{actor,action})` and `reconcileShipmentPromises(g,{actor,action})` only after completeKarama/finalization (11485); these throw when voluntary spending makes that actor's promise impossible (8030,8092). `normalizeAutomaticGame` has no voluntary actor and instead may release an impossible promise. Therefore a saved cost cannot defer this validation until ordinary automatic recovery: that would convert an illegal voluntary spend into an automatic release.
|
||
|
||
Preflight needs the actor's post-cost **and intended-use** context, not just a removed-card context. In particular a shipping Karama can itself establish the rate that makes shipment feasible. Preserve that rate in a pure predicted context before checking the promise. Battle cancellation can change Voice or prescience; validate against the correct predicted constraints. Opponent consequences may legitimately release another player's promise. Where a response suffix is not yet represented by a safe pure prediction, retain its original atomic no-pause envelope until that proof is implemented rather than claiming a safely durable prefix. This does not require a new rules interpretation.
|
||
|
||
`findShipmentCompletion` calls inner dispatcher with internal `shipmentPreparation` for Ghola/Karama/escrow preparations (7962). The new cost producer must honor this internal path for deterministic **own shipping Karama only**: execute effect and physical removal on its private clone without a live cost frame, opportunity, RNG or general normalization. The current flag bypasses only ordinaryCardDiscard; it does not automatically cover a newly staged spendKarama. Do not allow a JSON field to select simulation. Keep battle's direct preparation helpers unchanged.
|
||
|
||
## Test contract for implementation
|
||
|
||
Use real declaration → predrain cost → JSON/recovery for all four uses and both card forms; verify exact card/owner/recipient/lot/pending binding, original card consumed once, effect once, and duplicate CAS/worker recovery. Add unsupported Richese count rejection before any spent card/version; nested printed cancellation of a saved BG count attempt must restore the original response. Exercise free auction acquisition with Ix decision and Harkonnen bonus, paid-sale cancellation without replaying bidder or seller payments, invalid Richese sold-lot/funding and cache-exhaustion suffixes, Moritani successor cleanup, Baliset cancellation restoring a final Ornithopter move, Harkonnen traitor battle resolution, and typed storm/worm parents.
|
||
|
||
At least one promise test must reject spending the actor's last preparation Karama for an unrelated cancellation and preserve state; another must accept shipping Karama that makes the promised shipment possible. Repeated private shipment search with Ghola+Karama+escrow must stay immutable and yield executable actions. Preflight repeated calls should be checked with RNG/UUID counters that remain zero. Frame projection must remain generic-only; do not derive a new public response window from hidden card holder identity or display pure settlement/private-promise evidence.
|
||
|
||
This audit identifies implementation prerequisites only. It does not settle canceled Richese auction counting, canceled BG payment recovery, Karama shipment card refund custody, Sapho/Guild ordering or Semuta full-hand ordering.
|