Files
Dune/docs/KARAMA_COST_PREFLIGHT_REVIEW.md
T
2026-09-08 07:37:52 +10:00

41 KiB
Raw Blame History

Ordinary Karama cost: pure preflight and recovery contract

Newest checkpoint: Battle aftermath, movement cancellation and owner promises 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 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, then cancellation identity, Moritani cleanup and Baliset arrival. 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.