Skip to content

Zone pipeline Phase B: bucket-A deliveries onto the ApprovedZoneChange proof token - #2837

Merged
matthewevans merged 5 commits into
mainfrom
ship/zone-pipeline-phase-b
Jun 10, 2026
Merged

Zone pipeline Phase B: bucket-A deliveries onto the ApprovedZoneChange proof token#2837
matthewevans merged 5 commits into
mainfrom
ship/zone-pipeline-phase-b

Conversation

@matthewevans

Copy link
Copy Markdown
Member

Summary

Zone-change pipeline Phase B: all production bucket-A post-replacement delivery sites now route through the ApprovedZoneChange proof token + shared zone_pipeline::deliver tail — consult-once/deliver-once is structural, not conventional. Follows #2824/#2829. Also adds the PR template mandating engine-implementer attestation.

The centerpiece

handle_replacement_choice's ZoneChange resume arm — previously a divergent partial copy of the delivery tail that had already silently dropped two fields (face-down profile, batch attribution) — is dissolved. It now seals the post-replacement event via ApprovedZoneChange::approve_post_replacement (private fields + _seal; re-consult impossible by construction) and delivers through the shared tail. Resumed entries now receive the full tail: devour snapshot, EntersWithAdditionalCounters statics (CR 614.1c), attach_to, entered_via_ability_source, exile-link tracking, library auto-shuffle (CR 701.24a).

New machinery: PostReplacementDrainOwner (DeliveryTail / CallerEpilogue) rides DeliveryCtx and the deferred-tail record, so resume-path callers keep their epilogue's ctx-ful continuation drain without duplicating the tail. The counter-pause double-resume semantic is preserved and regression-pinned.

Site migrations

  • Land play (handle_play_land Execute arm): raw delivery + hand-applied enter-mods → token delivery. Lands now receive enters-with-counters statics (fail-first test: counter 0 → 1). mark_land_played_from_zone stamps a fresh origin after delivery on both synchronous and counter-pause paths (CR 305.1).
  • Top-library exile cost (pay_top_library_exile_cost): the (object_id, zone) degradation — which discarded post-replacement payloads and applied sets — replaced by sealing full ProposedEvents. Verified purely structural (the Exile delivery tail early-return predates this change and is preserved exactly).

Status

The sole remaining production bucket-A raw delivery is stack.rs:757, parked on a design decision (played_from_zone provenance, PLAN OQ#3). Independent post-rebase census: zero genuinely-unmigrated production sites with live replacement exposure remain; the residual ~36 raw callers are all documented deferral classes (hand/exile no-op-consult, library placements, merged-permanent routing).

Verification

  • Fail-first discriminating tests for both behavior deltas; regression net green (devour post-replacement tests from test(engine): discriminating Devour post-replacement regression tests (Fixes #575) #2795, paused face-down morph / ninjutsu / attraction, mill/seek/bounce batch resumes).
  • Tilt gates green per batch (clippy, test-engine 12134/12134, test-ai).
  • Two independent opus reviews: the resume-arm centerpiece (approved with complete divergence-enumeration audit) and the two site migrations (approved, all exit paths traced, CR cites grep-verified).
  • cargo check --workspace --all-targets green in the ship worktree against current origin/main.

🤖 Generated with Claude Code

…e fork sync

Add .github/PULL_REQUEST_TEMPLATE.md with a required check-one block recording
whether the change went through /engine-implementer (and if not, why — frontend,
docs/CI, or out-of-scope fix). Heading set deliberately omits the
## CR references + ## Track pair so it doesn't trip the AI-contributor labeler.

Add AI-CONTRIBUTOR.md §2.1: sync the fork with upstream on every run (ff-only
merge of upstream/main, best-effort push), and align the self-contained
Appendix B.3 prompt to fetch upstream and cut its branch from upstream/main.
…m into token delivery (CR 614.12a / 614.1c / 616.1)

The handle_replacement_choice ZoneChange arm was a divergent partial copy of
deliver_replaced_zone_change (the pipeline-copy smell this project exists to
kill — it had already dropped played_from_zone and face_down_profile, and
still skipped the devour snapshot, EntersWithAdditionalCounters statics,
attach_to, entered_via_ability_source, exile-link tracking, and the CR 701.24a
library-shuffle arm). It is now dissolved: the post-choice event is a
ReplacementResult::Execute payload, sealed through the third mint path
(ApprovedZoneChange::approve_post_replacement, PLAN §6.2) and delivered by the
shared zone_pipeline::deliver — the consult-once/deliver-once contract is
structural for the resume path.

Divergence reconciliation (PLAN §7 Phase B), by parameterizing the shared tail
rather than keeping two copies:
1. New PostReplacementDrainOwner (types/game_state.rs) rides DeliveryCtx and
   the ContinueZoneDeliveryTail deferred-tail record (#[serde(default)] =
   DeliveryTail for old saves): DeliveryTail = the tail drains
   post_replacement_continuation ctx-less (every direct delivery, unchanged);
   CallerEpilogue = the tail skips it and the resume epilogue keeps draining
   WITH the spell-resolution ctx and post_replacement_source cleared for zone
   changes. A counter-pause mid-resume stashes CallerEpilogue in the deferred
   record, so the NEXT resume's epilogue still owns the drain — matching the
   pre-token double-resume behavior.
2. pending_spell_resolution ordering untouched: apply_pending_spell_resolution
   still runs in the epilogue before that drain.
3. played_from_zone (PLAN OQ#3, UNDECIDED — no adjudication): captured
   pre-move by the resume caller and ridden on DeliveryCtx; deliver re-stamps
   it after a battlefield delivery (including the counter-pause outcome, where
   the pre-token arm restored before the counter application). Both OQ#3
   resolutions slot into this one site.

Resumed entries now receive the full delivery tail. Fail-first evidence:
resumed_entry_receives_enters_with_additional_counters_static — a battlefield
entry parked on the two-enter-tapped CR 616.1 prompt resumed WITHOUT the
'other creatures you control enter with an additional +1/+1 counter' static
(left: 0, right: 1); green after. Regression net all green: paused face-down
morph, ninjutsu, attraction, prevented-ETB, mill/seek/bounce batch resumes
(full test-engine + test-ai).

CR grep (docs/MagicCompRules.txt): 614.12a entry-replacement application;
614.1c enters-with; 616.1 ordering; 701.24a shuffle; 303.4f aura host; 603.6a
ETB trigger provenance.
… through the token path (CR 614.1c / 614.12a)

The land-play `Execute` arm (handle_play_land) was a divergent partial
copy of deliver_replaced_zone_change: it raw-moved via zones::move_to_zone
and hand-applied only enter_tapped / controller_override / the event's own
enter_with_counters / pending_etb_counters, SKIPPING the shared delivery
tail's CR 614.1c EntersWithAdditionalCounters statics snapshot, CR 303.4f
attach_to host, entered_via_ability_source provenance (CR 603.6a), and the
CR 701.24a library-shuffle arm. Dissolved per PLAN §6.2/§7: the
post-replace_event event is a ReplacementResult::Execute payload, sealed
through approve_post_replacement and delivered by zone_pipeline::deliver.

drain = CallerEpilogue: the land-play epilogue owns the
post_replacement_continuation drain (clears post_replacement_source, runs
the land-specific accounting), so the shared tail must not also drain it —
mirroring the d0a593d0e resume arm. played_from_zone is set FRESH after
delivery via mark_land_played_from_zone (recording a brand-new origin, not
preserving a pre-move value), so the ctx re-stamp knob stays None; the
counter-pause NeedsChoice arm stamps it before parking, matching the
pre-token arm that stamped before the apply_etb_counters early-return.

Fail-first evidence: played_land_receives_enters_with_additional_counters_static
— a Forest played through the real PlayLand action while an active
"other permanents you control enter with an additional +1/+1 counter"
static is functioning. Old land-play arm applied only the event's own
enter_with_counters and dropped the statics snapshot (counter = 0); through
the shared tail the played land now receives it (counter = 1).

CR grep (docs/MagicCompRules.txt): 614.1c enters-with seeds (line 3056);
614.6 replaced event never happens (line 3072); 305.1 playing a land
(line 1688).
…rough the token path (CR 614.1 / 614.6)

pay_top_library_exile_cost (Thought Lash cumulative-upkeep class) consulted
replace_event per card then DEGRADED each survivor to an (object_id, to)
pair, delivering via raw zones::move_to_zone — discarding the full
post-replacement ProposedEvent payload (the event's
applied: HashSet<ReplacementId> and every field the delivery tail reads).
Per PLAN §6.2: stash the full ProposedEvents and seal each through
ApprovedZoneChange::approve_post_replacement, then deliver via
zone_pipeline::deliver — a consult-skipping approved delivery. Never
re-proposes through move_object (which would discard `applied` and
double-apply the Moved definitions already applied this pass).

Structural fix (consult-once/deliver-once), not a behavior change: a plain
Library → Exile cost has no battlefield-entry mods to apply, the redirected
destination was already honored pre-migration (the `to` field was captured
from the Execute event), and the delivery tail's post_replacement_continuation
drain early-returns for the Exile destination (apply_zone_delivery_tail:
`to == Exile` with a source attribution and no exile-link returns Done before
the drain) — so no enter-mod or continuation behavior is newly observable
here. ctx: source_id = the event's cause (preserving the proposal's
Some(source_id)); played_from_zone = None (not a battlefield entry);
drain = DeliveryTail (this synchronous cost path has no caller epilogue).
The Library → Exile destination cannot surface a CR 614.1c counter-pause, so
the NeedsChoice arm is a debug_assert guard.

Regression test: top_library_exile_cost_exiles_top_card_through_sealed_delivery
pins the observable outcome (top card exiled, left the library) across both
the old raw delivery and the new sealed one.

CR grep (docs/MagicCompRules.txt): 614.1 replacement effects (line 3050);
614.6 a replaced event never happens (line 3072).
@matthewevans
matthewevans enabled auto-merge June 10, 2026 19:58

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements Phase B of the zone-pipeline migration, unifying land play, top-library exile cost payment, and replacement-choice resume paths with the shared zone_pipeline::deliver machinery to ensure they receive the full delivery tail. Feedback on the changes highlights a potential state corruption issue in release builds where a debug assertion is used instead of returning an error during cost payment. Additionally, several missing mandatory CR 305.1 annotations for land play origin bookkeeping were identified, violating repository style guide rule R6.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/engine/src/game/engine_payment_choices.rs
Comment thread crates/engine/src/game/engine.rs
Comment thread crates/engine/src/game/engine.rs Outdated
Comment on lines +5288 to +5292
// Stamp the play origin (engine bookkeeping for "where it was
// played from"). Set fresh AFTER delivery — the ctx re-stamp
// knob is for preserving a pre-move value, which this site does
// not have (it is recording a brand-new origin).
mark_land_played_from_zone(state, object_id, origin_zone);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

[MED] Add mandatory CR 305.1 annotation for land play origin bookkeeping. Evidence: crates/engine/src/game/engine.rs:5288.

Why it matters: Every rules-touching line of engine code must carry a verified CR <number> comment to adhere to R6 of the repository style guide. Suggested fix: Add CR 305.1 annotation to the mark_land_played_from_zone call.

Suggested change
// Stamp the play origin (engine bookkeeping for "where it was
// played from"). Set fresh AFTER delivery — the ctx re-stamp
// knob is for preserving a pre-move value, which this site does
// not have (it is recording a brand-new origin).
mark_land_played_from_zone(state, object_id, origin_zone);
// CR 305.1: Stamp the play origin (engine bookkeeping for "where it was
// played from") after putting the land onto the battlefield. Set fresh
// AFTER delivery — the ctx re-stamp knob is for preserving a pre-move
// value, which this site does not have (it is recording a brand-new origin).
mark_land_played_from_zone(state, object_id, origin_zone);
References
  1. R6. CR annotations are mandatory and verified. Every rules-touching line of engine code must carry a comment of the form CR : . (link)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 004db8d (same fix as the sibling comment — both the parked-prompt and synchronous stamp sites are annotated with CR 305.1 + CR 400.7i).

@matthewevans
matthewevans added this pull request to the merge queue Jun 10, 2026
@matthewevans
matthewevans removed this pull request from the merge queue due to a manual request Jun 10, 2026
…d-play provenance (CR 305.1 + CR 400.7i) — Gemini review
@matthewevans
matthewevans enabled auto-merge June 10, 2026 20:40
@matthewevans
matthewevans disabled auto-merge June 10, 2026 20:42
@matthewevans
matthewevans merged commit d01f5c0 into main Jun 10, 2026
10 checks passed
@matthewevans
matthewevans deleted the ship/zone-pipeline-phase-b branch June 10, 2026 20:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant