Skip to content

cr733(p2): journal logical-zone trigger collection occurrences#6536

Merged
matthewevans merged 1 commit into
mainfrom
ship/cr733-p2-trigger-collection
Jul 23, 2026
Merged

cr733(p2): journal logical-zone trigger collection occurrences#6536
matthewevans merged 1 commit into
mainfrom
ship/cr733-p2-trigger-collection

Conversation

@matthewevans

@matthewevans matthewevans commented Jul 23, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added journaling for resolved trigger collections, including deferred triggers and events consumed before priority.
    • Trigger-collection commands can now be serialized, replayed, and applied consistently.
    • Empty trigger collections are safely treated as no-ops.
  • Bug Fixes

    • Zone-change trigger processing now records resolved trigger effects in the rules journal, improving replay accuracy and consistency.

@matthewevans
matthewevans enabled auto-merge July 23, 2026 05:52
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds resolved trigger collection commands to the rules journal, applies them during normal execution and replay, routes zone-change trigger handling through the journal, and adds integration coverage for serialization, ordering, no-ops, causes, and ChangeZone behavior.

Changes

Resolved trigger collection journaling

Layer / File(s) Summary
Trigger collection journal contract
crates/engine/src/types/resolved_commands.rs, crates/engine/src/types/mod.rs
Defines deferred and consumed trigger collection commands, records them in the resolved rules journal, validates their causes, and re-exports the new types.
Trigger collection application and wiring
crates/engine/src/game/triggers.rs
Adds replay application and journaling helpers, then routes zone-change deferred and consumed trigger paths through them.
Replay and integration validation
crates/engine/tests/integration/cr733_resolved_commands_p2.rs, crates/engine/tests/integration/cr733_resolved_trigger_collection.rs, crates/engine/tests/integration/main.rs
Adds replay dispatch and integration tests for serialization, ordering, empty collections, journal causes, and ChangeZone-trigger recording.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChangeZoneResolver
  participant TriggerHelpers
  participant GameState
  participant RulesJournal
  ChangeZoneResolver->>TriggerHelpers: resolve trigger collection
  TriggerHelpers->>GameState: apply contexts or occurrences
  TriggerHelpers->>RulesJournal: record TriggerCollection command
  RulesJournal-->>TriggerHelpers: command ordinal
Loading

Possibly related PRs

Suggested reviewers: andriypolanski, dripsmvcp

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: journaling trigger-collection occurrences for CR733 p2.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ship/cr733-p2-trigger-collection

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/engine/tests/integration/cr733_resolved_trigger_collection.rs (1)

1-228: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

No production-pipeline test for the ConsumeBeforePriority path.

real_change_zone_resolver_journals_its_collected_trigger_occurrences nicely drives DeferPending through the real ChangeZone resolver with a positive reach-guard. ConsumeBeforePriority (the other half of this PR's wiring, replacing the direct extend in mark_logical_zone_events_consumed_before_priority) is only exercised via hand-built commands passed straight to apply_resolved_trigger_collection, never through the real engine flow that calls mark_logical_zone_events_consumed_before_priority. Consider adding a scenario-driven test that reaches that call site.

As per path instructions, "Test adequacy is the highest-frequency contributor finding... a parser AST shape test does NOT prove runtime semantics" for crates/engine/tests/**.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/tests/integration/cr733_resolved_trigger_collection.rs` around
lines 1 - 228, Add a scenario-driven integration test that exercises the
production flow invoking mark_logical_zone_events_consumed_before_priority and
verifies it journals a non-empty ConsumeBeforePriority
ResolvedTriggerCollection. Use a real game event/resolution path rather than
directly calling apply_resolved_trigger_collection, and assert the collected
occurrence is recorded in the journal with the expected cause and state effect.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/engine/src/types/resolved_commands.rs`:
- Around line 1644-1650: Update the ResolvedRulesCommand::TriggerCollection arm
in validate_resolved_command to reject empty ResolvedTriggerCollection payloads
by checking that command.contexts is non-empty before validating the cause.
Return the same invalid-command error pattern used by the sibling arms for no-op
payloads, while preserving the existing unrelated-cause validation for non-empty
collections.
- Around line 288-303: Update validate_resolved_command to reject empty
ResolvedTriggerCollection payloads for both DeferPending.contexts and
ConsumeBeforePriority.occurrences, matching the existing validation behavior for
other resolved-command variants while continuing to accept non-empty
collections.

---

Nitpick comments:
In `@crates/engine/tests/integration/cr733_resolved_trigger_collection.rs`:
- Around line 1-228: Add a scenario-driven integration test that exercises the
production flow invoking mark_logical_zone_events_consumed_before_priority and
verifies it journals a non-empty ConsumeBeforePriority
ResolvedTriggerCollection. Use a real game event/resolution path rather than
directly calling apply_resolved_trigger_collection, and assert the collected
occurrence is recorded in the journal with the expected cause and state effect.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5faa924a-1af2-4d2a-8366-ef62ed0e1f59

📥 Commits

Reviewing files that changed from the base of the PR and between 659ddf7 and f97faaf.

📒 Files selected for processing (6)
  • crates/engine/src/game/triggers.rs
  • crates/engine/src/types/mod.rs
  • crates/engine/src/types/resolved_commands.rs
  • crates/engine/tests/integration/cr733_resolved_commands_p2.rs
  • crates/engine/tests/integration/cr733_resolved_trigger_collection.rs
  • crates/engine/tests/integration/main.rs

Comment on lines +288 to +303
/// Exact trigger occurrences collected at one logical trigger/LKI boundary.
///
/// CR 603.2 + CR 603.3b: collected trigger contexts retain their already
/// determined firing and placement order. CR 603.10 + CR 603.10a: final
/// logical zone-change settlement uses the recorded pre-event authority.
/// CR 603.2c: consumed event occurrences prevent the generic priority scan
/// from collecting the same occurrence a second time.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResolvedTriggerCollection {
DeferPending {
contexts: Vec<PendingTriggerContext>,
},
ConsumeBeforePriority {
occurrences: Vec<ConsumedTriggerEventOccurrence>,
},
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i MagicCompRules.txt
rg -n "^603\.2c" -A3 docs/MagicCompRules.txt
rg -n "^603\.3b" -A3 docs/MagicCompRules.txt
rg -n "^603\.10a?\b" -A3 docs/MagicCompRules.txt

Repository: phase-rs/phase

Length of output: 509


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Top-level files:\n'
git ls-files | sed -n '1,120p' | head -n 120

printf '\nPossible rules text files:\n'
fd -i 'MagicCompRules.txt|CompRules|ComprehensiveRules|rules' . -t f

Repository: phase-rs/phase

Length of output: 5198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Relevant files in crates/engine/src/types:\n'
fd -i 'resolved_commands.rs|triggers.rs' crates/engine/src/types crates/engine/src -t f

printf '\nOutline resolved_commands.rs:\n'
ast-grep outline crates/engine/src/types/resolved_commands.rs --view expanded

printf '\nOutline triggers.rs:\n'
ast-grep outline crates/engine/src/triggers.rs --view expanded

Repository: phase-rs/phase

Length of output: 13854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files | rg -n 'MagicCompRules|CompRules|ComprehensiveRules|(^|/)docs/'

Repository: phase-rs/phase

Length of output: 270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'resolved_commands.rs excerpt:\n'
sed -n '280,340p' crates/engine/src/types/resolved_commands.rs

printf '\nvalidate_resolved_command excerpt:\n'
sed -n '1628,1665p' crates/engine/src/types/resolved_commands.rs

printf '\nrecord_trigger_collection excerpt:\n'
sed -n '1188,1206p' crates/engine/src/types/resolved_commands.rs

Repository: phase-rs/phase

Length of output: 5058


🌐 Web query:

Magic: The Gathering Comprehensive Rules 603.2c exact text

💡 Result:

The exact text of section 603.2c of the Magic: The Gathering Comprehensive Rules is: 603.2c An ability triggers only once each time its trigger event occurs. However, it can trigger repeatedly if one event contains multiple occurrences. [1][2]

Citations:


🌐 Web query:

site:magic.wizards.com Comprehensive Rules 603.2c 603.3b 603.10a trigger occurrence stack placement look-back

💡 Result:

In the Magic: The Gathering Comprehensive Rules, sections 603.2c, 603.3b, and 603.10a describe the mechanics of how triggered abilities occur, are placed on the stack, and interact with the game state. Rule 603.2c states that an ability triggers each time its trigger event occurs, even if the event happens during the resolution of another spell or ability. Triggered abilities are then handled by the game system regardless of when the triggering event takes place. Rule 603.3b details that when a triggered ability is put onto the stack, the controller of that ability is the player who controlled the source of the ability at the time it triggered. If the source is no longer on the battlefield, the game uses the last known information (the controller's status) to determine ownership on the stack. Rule 603.10a explains the "look-back" or state-dependent nature of triggers. It clarifies that if an ability's trigger event depends on the game state (such as a creature leaving the battlefield or an object changing zones), the game checks the state of the game immediately before or after the event to verify if the trigger conditions were met. This ensures that triggers like "when a creature dies" correctly identify the object that left the battlefield even though it is no longer in the zone where the trigger occurred. These rules collectively ensure that triggered abilities are reliably accounted for, assigned to the correct controller, and triggered based on the game state at the appropriate moment, maintaining consistency even during complex interactions where objects move between zones or leave play.

Citations:


Reject empty TriggerCollection payloads
validate_resolved_command should reject zero-length collections like the other resolved-command variants; otherwise a malformed journal entry can pass replay validation even though production never records one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/types/resolved_commands.rs` around lines 288 - 303, Update
validate_resolved_command to reject empty ResolvedTriggerCollection payloads for
both DeferPending.contexts and ConsumeBeforePriority.occurrences, matching the
existing validation behavior for other resolved-command variants while
continuing to accept non-empty collections.

Source: Path instructions

Comment on lines +1644 to +1650
ResolvedRulesCommand::TriggerCollection(command) => {
if entry.node != command.cause {
return Err(ResolvedRulesJournalError::InvalidSerializedAuthority(
"trigger-collection command has an unrelated cause".to_string(),
));
}
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

validate_resolved_command's TriggerCollection arm doesn't reject empty collections, unlike every sibling variant.

Every other arm here rejects a no-op payload (player_edit_is_empty, command.expected_old == command.new, object_counter_edit_is_empty, information_command_is_invalid, ledger_edit_is_invalid). Production code (resolve_and_apply_trigger_collection in triggers.rs) short-circuits empty ResolvedTriggerCollection before ever recording it, but record_trigger_collection is pub with no such guard, and this validator doesn't enforce it either — so a deserialized/tampered journal with an empty TriggerCollection { contexts: vec![] } entry silently passes validate_serialized_authority, breaking the invariant this file otherwise enforces for every other command.

🛡️ Proposed fix
+impl ResolvedTriggerCollection {
+    fn is_empty(&self) -> bool {
+        match self {
+            Self::DeferPending { contexts } => contexts.is_empty(),
+            Self::ConsumeBeforePriority { occurrences } => occurrences.is_empty(),
+        }
+    }
+}
+
             ResolvedRulesCommand::TriggerCollection(command) => {
-                if entry.node != command.cause {
+                if entry.node != command.cause || command.collection.is_empty() {
                     return Err(ResolvedRulesJournalError::InvalidSerializedAuthority(
                         "trigger-collection command has an unrelated cause".to_string(),
                     ));
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/types/resolved_commands.rs` around lines 1644 - 1650,
Update the ResolvedRulesCommand::TriggerCollection arm in
validate_resolved_command to reject empty ResolvedTriggerCollection payloads by
checking that command.contexts is non-empty before validating the cause. Return
the same invalid-command error pattern used by the sibling arms for no-op
payloads, while preserving the existing unrelated-cause validation for non-empty
collections.

@matthewevans
matthewevans added this pull request to the merge queue Jul 23, 2026
@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

Merged via the queue into main with commit 203377c Jul 23, 2026
15 checks passed
@matthewevans
matthewevans deleted the ship/cr733-p2-trigger-collection branch July 23, 2026 06:29
jsdevninja pushed a commit to jsdevninja/phase that referenced this pull request Jul 24, 2026
…-rs#6536)

Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
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