Skip to content

refactor(parser): Plan 05b T8-A0 — the ability shell widening (infrastructure only) - #6714

Merged
matthewevans merged 1 commit into
mainfrom
wt/p05b-t8a0
Jul 28, 2026
Merged

refactor(parser): Plan 05b T8-A0 — the ability shell widening (infrastructure only)#6714
matthewevans merged 1 commit into
mainfrom
wt/p05b-t8a0

Conversation

@matthewevans

@matthewevans matthewevans commented Jul 27, 2026

Copy link
Copy Markdown
Member

Plan 05b tranche T8-A0 — the ability shell widening. Infrastructure only: zero producers
converted, zero call sites changed.

Follows #6709 (T5). This is the foundation the rest of T8 stands on: it builds the mechanism that
makes A1–A4's recognizer conversions mechanical rather than a per-site judgment call.

What landed

  1. AbilityShellIr widened from { sub_link } to the CR 602.1 activation envelope — 10 of
    AbilityDefinition's 38 root fields, deliberately not a mirror of the root.
  2. An ordered stages: Vec<ShellStage>, because two of the transforms the call sites run are chain
    structure folds that write root fields, and their order relative to the field stamps is
    load-bearing. A field bag cannot express "run these two, in this order, after the stamps."
  3. lower_ability_ir extended to apply the envelope and the stages in the pinned order
    chain → finalize → anchor → sub_link → envelope → stages.
  4. parse_ability_ir published as pub(crate), plus the two mode-pinned wrappers
    parse_ability_ir_with_context / parse_ability_ir_standalone. These exist so a converted site
    inherits its original ChainLoweringMode by name rather than by a reviewer re-deriving it at
    nine call sites.
  5. The ability_ir_at emitter, which still emits the pre-lowered node.

Byte-identity is by construction, and the property has a name

Defer-on-default: every shell field's applier is a no-op at its default, so
AbilityShellIr::default() — the only shell any producer builds until A1 — leaves the lowered
definition bit-for-bit unchanged. In apply_ability_shell_envelope: Option fields write only under
if let Some; activation_restrictions extends from an empty vec; min_x_value uses max against
0; cant_be_copied ORs with false; the stage loop iterates an empty Vec.

No property of any card's text participates, so a future MTGJSON card reaching one of these sites is
covered by the same argument.

The identity is now also literal in the tree rather than merely argued: parse_effect_chain is
exactly lower_ability_ir(&parse_ability_ir_standalone(t, k)), and parse_effect_chain_with_context
its WithContext twin.

H3 — the verification the plan explicitly deferred to the implementer: PASSED, no site excluded

The live sites split between activation_restrictions = constraints (U0-23 Channel, U0-12 leveler)
and .extend(constraints) (U0-24/25/27). extend is the only correct shell semantic, but that is
only sound if nothing in lower_ability_ir's closure writes the root field first.

parse_ability_ir can dispatch to exactly five whole-body paths, plus three post-parse steps.
Every one lives in crates/engine/src/parser/oracle_effect/, and that entire directory contains
zero occurrences of activation_restrictions at the base commit. That is the whole closure, so
no bypass writes the root's restrictions.

The check did not stop at the file grep, because oracle_effect/ does call into modules that write
the field. All six such calls go to oracle_static, and every result is boxed into a
GrantStaticAbility / StaticDefinition / CreateEmblem inside an Effect payload — those land
on nested definitions, never the root the shell stamps, and StaticDefinition has no
activation_restrictions field at all.

Verdict: extend= for the root at every T8 R1 site; both = sites provably start from empty;
nothing is excluded from T8.
The invariant is maintainable — any future violation has to write the
field inside oracle_effect/, which the same one-line grep catches.

One thing A1–A4 must not "tidy"

Restriction order is site-specific. The Solved recognizer pushes IsSolved before extending
with parsed constraints; Boast/Exhaust/Forecast/Power-up extend parsed constraints first, then push
their implicit ones. A shell with separate "implicit"/"parsed" fields would silently normalize that
away. Hence one ordered Vec composed by the site, applied by a single extend.

CR 707.9a is not gapped by this seam

ability_ir_at emits the pre-lowered spell variant, so every phase-A-converted producer lands in
finish()'s pre-lowered arm and is stamped by stamp_retained_printed_slot — which recurses the
whole definition (effect, sub_ability, else_ability, mode_abilities). A copy-except clause
inside a converted body is stamped, at the same slot it got before. A0 adds zero new reachable
instances.

Moving the stamp to lower_oracle_ir (the recensus's T9 recommendation) would be actively unsafe
here
: it would newly stamp U0-39's Spell node, which #6708 deliberately leaves unstamped. That
changes output for any card pairing a prevention spell with a copy-except clause — an intentional byte
delta, which this tranche is chartered not to land. T9 is the right home, because that is where the
emitter switches payloads and where U0-39's delta is already being adjudicated.

Gates

cargo fmt --all                                      clean
cargo clippy -p engine --lib -- -D warnings          zero warnings
cargo nextest run -p engine --lib                    17844 tests run: 17844 passed, 6 skipped
./scripts/check-prelowered-ratchet.sh                Gate P PASS
./scripts/check-skill-doc.sh                         ✓ oracle-parser skill references valid
./scripts/check-parser-combinators.sh                Gate G PASS / Gate A PASS

Snapshot churn: ZERO, as an infrastructure-only tranche requires. find . -name '*.snap.new' → 0
files after the full 17844-test run; the complete branch delta is three parser files, no *_ir.snap,
no *_lowered.snap, no card-data. Gate P per-file counts are identical to baseline with no ledger
edit
— every file sits exactly at its ceiling, so nothing was silently loosened.

Gate P caught a real defect in the first draft, which is worth recording. It raised oracle.rs
16 → 21, and not one of the five came from code — all five were the token appearing in a new doc
comment. The gate is textual; your own prose counts. The prose was rewritten to carry the meaning
without the token.

That is also why ability_ir_at is written as self.ability_at(line, lower_ability_ir(&ir)) rather
than spelling out the emit_at(.., PreLoweredSpell(..)) form: ability_at is exactly that
emit_at, so the two are behaviorally identical, but the spelled-out form would push oracle.rs to
17 against a ceiling of 16. Delegation adds zero tokens and states the phase-A identity once instead
of maintaining a second copy of it. T9's one-line body swap is unaffected. No ceiling was raised.

Deliberate #[allow(dead_code)], both with a named owner

ShellStage (first producer arrives in A2, the R1 sites running both folds) and ability_ir_at
(first producer in A1). Unlike TriggerNodeIr::Parsed, neither allow moves — both are
deleted by the tranche that adds the first producer, and the code comments say so.

Not run, and why

Full-pool card-data.json byte-identity. For a tranche that converts no producers it is vacuously
green and therefore supplies no signal; manufacturing a green that cannot be interpreted was declined
in favour of saying so. The non-vacuity probe belongs to A1, which is the first tranche with a
converted site to mis-mode.

CR numbers grep-verified against docs/MagicCompRules.txt before entering code: 602.1, 602.1a,
602.1b, 113.3b, 601.2b, 608.2, 601.2f, 113.6m, 707.9a, 702.142a, 702.193b, 106.6, 603.3, 707.10.

One was corrected in draft: cant_be_copied was annotated CR 707.9a, which is the unrelated rule for
copy effects that cause a copy to gain an ability. The printed line "This ability can't be copied"
restricts putting a copy on the stack — CR 707.10. Both the field doc and the applier now cite
707.10 and say why it is not 707.9a.

Summary by CodeRabbit

  • Enhancements

    • Improved ability and effect-chain parsing for more consistent handling of activation costs, restrictions, copying rules, and minimum values.
    • Improved detection of mana-spend triggers and cost-reduction effects during ability processing.
    • Refined serialization so default or empty values are omitted from generated output.
  • Bug Fixes

    • Improved preservation of ability properties when combining and lowering effect-chain information.

…tructure only)

Builds the mechanism every later T8 sub-tranche stands on. Converts ZERO
producers: no call site changes, so the tranche is byte-identical BY
CONSTRUCTION and shows zero snapshot churn.

What lands:

* `AbilityShellIr` widens from `{ sub_link }` to the CR 602.1 activation
  envelope — 10 of `AbilityDefinition`'s 38 root fields, deliberately not a
  mirror of the root. CR 602.1 (MagicCompRules.txt:2514) draws exactly this
  seam: cost (CR 602.1a, :2516) / activation instructions (CR 602.1b, :2519) /
  effect, with CR 113.3b (:761) repeating it for abilities generally and
  CR 601.2b (:2459) grounding `min_x_value`. `EffectChainIr` keeps the CR 608.2
  (:2785) resolution instructions. The root-vs-clause axis therefore follows a
  seam the rules already draw rather than straddling rule sections.
* `ShellStage`, an ORDERED `Vec` — not a set of flags. Both stages are chain-
  STRUCTURE folds that also write root fields, so their position relative to the
  field stamps is behavior-load-bearing: `ExtractCostReduction` writes
  `cost_reduction` (so it must not run where a site stamped it explicitly — the
  Power-up recognizer does), and `ExtractManaSpendTrigger` is a no-op unless the
  lowered root effect is already `Effect::Mana`, so it is meaningful only
  post-lowering.
* `lower_ability_ir` gains the envelope applier and the stage loop, in the
  pinned order chain -> finalize -> anchor -> sub_link -> stamps -> stages,
  reproducing what every family-A recognizer does by hand.
* `parse_ability_ir` published as `pub(crate)` plus the two mode-pinned wrappers
  `parse_ability_ir_standalone` / `parse_ability_ir_with_context`, whose
  argument lists match the entry points they replace so a converted site
  inherits its `ChainLoweringMode` mechanically instead of by reviewer judgment.
* `DocEmitter::ability_ir_at`, which lowers eagerly and still emits the
  pre-lowered node, so producers convert tranche by tranche with the node
  payload, `finish()`, `item_ability` and the mutators all untouched.

Applier semantics are defer-on-default throughout, which is what makes the
widening inert until a producer opts in: Option fields override only when
`Some`, `activation_restrictions` uses `extend` (never `=`),
`min_x_value` uses `max`, `cant_be_copied` is a monotone OR.

`extend` is verified, not assumed: `rg activation_restrictions
crates/engine/src/parser/oracle_effect/` returns zero hits, and that directory
holds the whole of `lower_ability_ir` — `lower_effect_chain_ir`,
`finalize_effect_chain`, the owner-library anchor, and all five whole-body
bypasses `parse_ability_ir` dispatches to. Every reachable write lands on a
NESTED granted/static definition inside an `Effect` payload, never on the root
the shell stamps. So the sites whose original was `= constraints` provably start
from empty and `extend` reproduces them.

`ability_ir_at` delegates to `ability_at` rather than emitting its own node.
That states the phase-A identity literally and keeps the textual burn-down
ratchet monotone — `oracle.rs` sits exactly at its ceiling, and the ratchet
counts the token in prose as well as in code.

CR 707.9a: no gap opens here. Stamping runs in `finish()` before lowering, so an
IR-native `Spell` body cannot be stamped — but phase A emits the pre-lowered
variant, so every converted producer is still stamped by
`stamp_retained_printed_slot`, which recurses the whole definition. The gap
becomes reachable only when the emitter switches payloads in T9, which is also
the only place the stamp can move without a byte delta on the one pre-existing
`Spell` producer.

Gates: cargo fmt; clippy -p engine --lib -D warnings clean; Gate P PASS with
per-file counts unchanged (no ledger edit); check-skill-doc PASS; 399/399
snapshot tests pass with zero `.snap.new`.
@matthewevans
matthewevans enabled auto-merge July 27, 2026 23:59
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR defines serialized ability-shell fields and ordered lowering stages, centralizes shell-envelope application, adds mode-specific parsing wrappers, and exposes helpers for IR-based emission and mana-spend trigger extraction.

Changes

Ability lowering

Layer / File(s) Summary
Shell IR contract
crates/engine/src/parser/oracle_ir/effect_chain.rs
AbilityShellIr fields now use explicit default-skipping serialization rules, and ShellStage defines ordered cost-reduction and mana-spend extraction transforms.
Shell envelope lowering
crates/engine/src/parser/oracle_effect/mod.rs
lower_ability_ir applies shell fields through a dedicated helper before iterating the ordered extraction stages.
Parser and emitter integration
crates/engine/src/parser/oracle_effect/mod.rs, crates/engine/src/parser/oracle.rs
Mode-specific parsing wrappers and DocEmitter::ability_ir_at were added, while mana-spend trigger extraction became crate-visible.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AbilityIr
  participant lower_ability_ir
  participant AbilityDefinition
  AbilityIr->>lower_ability_ir: lower ability chain
  lower_ability_ir->>AbilityDefinition: apply shell envelope
  lower_ability_ir->>AbilityDefinition: extract cost reduction
  lower_ability_ir->>AbilityDefinition: extract mana-spend trigger
Loading

Suggested labels: quality

Suggested reviewers: ntindle, jsdevninja, lgray

🚥 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 Title clearly matches the parser refactor and ability shell widening infrastructure change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 wt/p05b-t8a0

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: 3

🤖 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/parser/oracle_effect/mod.rs`:
- Around line 26600-26612: In the lowering flow around
apply_ability_shell_envelope and the ShellStage::ExtractCostReduction branch,
add a debug_assert! that rejects configurations where
AbilityShellIr::cost_reduction is explicitly set while the shell stages also
include ExtractCostReduction. Place the guard before extraction can overwrite
the stamped value, while preserving normal lowering when only one source is
present.

In `@crates/engine/src/parser/oracle_ir/effect_chain.rs`:
- Around line 126-156: Correct the CR citations in the `EffectChainIr` partition
table and corresponding per-field documentation: classify `cost_reduction` under
CR 601.2f rather than CR 602.1a, and cite `min_x_value` with both CR 601.2b and
CR 602.2b wherever its applicability is documented. Keep the runtime
implementation unchanged.
- Around line 220-231: Correct the rule citation in the documentation for the
EffectChain `activation_zone` field from CR 113.6m to CR 113.6b, keeping the
existing explanation of printed-zone declarations such as Channel and Forecast
unchanged.
🪄 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: 7c39179c-94b4-4ef2-aa9c-356541d37fb7

📥 Commits

Reviewing files that changed from the base of the PR and between e9a268a and eb4ae5d.

📒 Files selected for processing (3)
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_ir/effect_chain.rs

Comment on lines +26600 to 26612
apply_ability_shell_envelope(&mut def, &ir.shell);
for stage in &ir.shell.stages {
match stage {
ShellStage::ExtractCostReduction => {
crate::parser::oracle::extract_cost_reduction_from_chain(&mut def);
}
ShellStage::ExtractManaSpendTrigger => {
crate::parser::oracle::extract_mana_spend_trigger_from_chain(&mut def);
}
}
}
def
}

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

Unenforced invariant: explicit cost_reduction stamp + ExtractCostReduction stage can silently collide.

The docs on both AbilityShellIr::cost_reduction and ShellStage::ExtractCostReduction state a site must never set both, because the stage would silently overwrite an explicitly-stamped reduction with whatever it finds in the chain. Nothing in lower_ability_ir/apply_ability_shell_envelope enforces this — it's discipline-only. Currently unreachable (only AbilityShellIr::default() is ever constructed), but this is exactly the kind of latent trap that becomes a real silent-data-loss bug the moment a T8-A2 site mistakenly sets both.

A cheap debug_assert! at the point where both interact would convert a silent-overwrite bug into an immediate, loud failure in dev/test builds.

🛡️ Proposed guard
     apply_ability_shell_envelope(&mut def, &ir.shell);
     for stage in &ir.shell.stages {
         match stage {
             ShellStage::ExtractCostReduction => {
+                debug_assert!(
+                    ir.shell.cost_reduction.is_none(),
+                    "ExtractCostReduction stage must not run when the shell explicitly stamps cost_reduction"
+                );
                 crate::parser::oracle::extract_cost_reduction_from_chain(&mut def);
             }

Also applies to: 26622-26657

🤖 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/parser/oracle_effect/mod.rs` around lines 26600 - 26612, In
the lowering flow around apply_ability_shell_envelope and the
ShellStage::ExtractCostReduction branch, add a debug_assert! that rejects
configurations where AbilityShellIr::cost_reduction is explicitly set while the
shell stages also include ExtractCostReduction. Place the guard before
extraction can overwrite the stamped value, while preserving normal lowering
when only one source is present.

Comment on lines +126 to +156
/// # The partition is the rules' own, not an engineering convenience
///
/// CR 602.1 (`MagicCompRules.txt:2514`) — *"Activated abilities have a cost and
/// an effect. They are written as `[Cost]: [Effect.] [Activation instructions
/// (if any).]`"* — draws exactly the seam this type sits on, and CR 113.3b
/// (:761) repeats the tripartite form for abilities generally. So:
///
/// | shell field group | CR |
/// |---|---|
/// | `cost`, `cost_reduction` | CR 602.1a — everything before the colon (:2516) |
/// | `activation_restrictions`, `activation_mana_payment_restriction`, `activator_filter`, `activation_zone` | CR 602.1b — activation instructions, *"not part of the ability's effect"* (:2519) |
/// | `min_x_value` | CR 601.2b — the announced value of a variable cost (:2459) |
/// | `ability_tag`, `cant_be_copied`, `description` | ability-level identity/provenance, not resolution steps |
///
/// while `EffectChainIr` holds the CR 608.2 (:2785) resolution instructions.
/// Because the root-vs-clause axis follows a seam CR 602.1 already draws, the
/// widening satisfies the categorical-boundary rule rather than straddling rule
/// sections.
///
/// **This is 10 of `AbilityDefinition`'s 38 root fields, deliberately not a
/// mirror of the root.** Fields excluded on purpose — `effect`, `sub_ability`,
/// `else_ability`, `condition` — are all CR 608.2 resolution tree and are
/// already expressible as `ClauseIr`/`ClauseDisposition`. A shell that mirrored
/// the root would re-open the escape hatch this type exists to close.
///
/// # Applier semantics (see `lower_ability_ir`)
///
/// Every field is **defer-on-default**: an unset field leaves whatever lowering
/// produced, so a `default()` shell is exactly today's behavior. That is what
/// makes the widening byte-identical by construction — see the per-field docs
/// for the one-line rule each obeys.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Minor CR-citation gaps between the summary table and the per-field docs.

Two small inconsistencies in the CR mapping:

  • The table (line 135) groups cost_reduction under CR 602.1a ("everything before the colon"), but the field's own doc (line 180) correctly cites CR 601.2f (cost reduction is determined while computing the total cost, not part of the literal pre-colon cost text). The table oversimplifies and disagrees with the more precise field-level citation.
  • min_x_value (line 137, line 239) cites only CR 601.2b, which governs the announcement step for casting a spell. This same shell field is shared by AbilityKind::Activated (as evidenced by activation_restrictions/activation_zone being cited under CR 602.1b specifically for the activated-ability half of the type). CR 602.2b is what actually extends 601.2b's announcement steps to activated abilities, so citing 601.2b alone is incomplete for half of this field's stated applicability.

Neither changes runtime behavior, but this doc is the reference future tranches (T8-A2/T9) will read when wiring producers, so a precise/compound citation (e.g. CR 601.2b + CR 602.2b) would avoid propagating the gap.

Also applies to: 175-247

🤖 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/parser/oracle_ir/effect_chain.rs` around lines 126 - 156,
Correct the CR citations in the `EffectChainIr` partition table and
corresponding per-field documentation: classify `cost_reduction` under CR 601.2f
rather than CR 602.1a, and cite `min_x_value` with both CR 601.2b and CR 602.2b
wherever its applicability is documented. Keep the runtime implementation
unchanged.

Source: Path instructions

Comment on lines +220 to +231
/// CR 113.6m: the zone the ability functions in. `Some(_)` overrides;
/// `None` defers.
///
/// Note for later tranches: the generic activated-ability recognizer derives
/// this field by *reading the lowered def*
/// (`activation_zone_from_self_cost` / `activation_zone_from_self_effect`),
/// which a shell stamped before lowering cannot express. That is one of the
/// reasons `parse_activated_ability_definition` is scoped to its own unit
/// rather than to T8 — this field is here for the recognizers that know
/// their zone from the printed keyword (Channel, Forecast), not for that one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) activation_zone: Option<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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

CR 113.6m is the wrong rule for activation_zone.

CR 113.6m governs the narrow "ability whose effect moves the object out of a zone functions only in that zone" case (the Reassembling Skeleton example — self-referential zone-exit). The generic "an ability that states which zones it functions in" rule this field actually needs is CR 113.6b: "An ability that states which zones it functions in functions only from those zones." The field's own doc names Channel/Forecast as the target use case — both are printed-zone declarations, i.e. textbook CR 113.6b, not 113.6m.

🐛 Proposed fix
-    /// CR 113.6m: the zone the ability functions in. `Some(_)` overrides;
+    /// CR 113.6b: the zone the ability functions in. `Some(_)` overrides;
     /// `None` defers.

As per path instructions, "rules-touching code with no verified CR annotation, or a CR citation whose rule body does not describe the code" is a flagged finding for crates/engine/**.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// CR 113.6m: the zone the ability functions in. `Some(_)` overrides;
/// `None` defers.
///
/// Note for later tranches: the generic activated-ability recognizer derives
/// this field by *reading the lowered def*
/// (`activation_zone_from_self_cost` / `activation_zone_from_self_effect`),
/// which a shell stamped before lowering cannot express. That is one of the
/// reasons `parse_activated_ability_definition` is scoped to its own unit
/// rather than to T8 — this field is here for the recognizers that know
/// their zone from the printed keyword (Channel, Forecast), not for that one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) activation_zone: Option<Zone>,
/// CR 113.6b: the zone the ability functions in. `Some(_)` overrides;
/// `None` defers.
///
/// Note for later tranches: the generic activated-ability recognizer derives
/// this field by *reading the lowered def*
/// (`activation_zone_from_self_cost` / `activation_zone_from_self_effect`),
/// which a shell stamped before lowering cannot express. That is one of the
/// reasons `parse_activated_ability_definition` is scoped to its own unit
/// rather than to T8 — this field is here for the recognizers that know
/// their zone from the printed keyword (Channel, Forecast), not for that one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) activation_zone: Option<Zone>,
🤖 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/parser/oracle_ir/effect_chain.rs` around lines 220 - 231,
Correct the rule citation in the documentation for the EffectChain
`activation_zone` field from CR 113.6m to CR 113.6b, keeping the existing
explanation of printed-zone declarations such as Channel and Forecast unchanged.

Source: Path instructions

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

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

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