fix(engine): honor CR 702.16n protection Aura exemption (Flickering Ward #6499)#6542
Conversation
…ard phase-rs#6499) Stamp ProtectionDoesNotRemove on continuous protection grants (including post-normalization `~`) and skip SBA removal for matching exempted attachments. Co-authored-by: Cursor <cursoragent@cursor.com>
|
🚨 Contributor flagged. Click here for more info: Superagent Dashboard |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe change adds protection-exemption data to static definitions, parses “doesn’t remove” riders, tracks grant-start attachment snapshots, and applies exemptions during attachment legality checks. Tests cover Flickering Ward and CR 702.16p attachment behavior. ChangesFlickering Ward protection exemption
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant OracleText
participant StaticDefinition
participant GameState
participant attachment_illegality
participant StateBasedActions
OracleText->>StaticDefinition: parse protection rider
StaticDefinition->>GameState: apply protection grant and exemption
GameState->>attachment_illegality: evaluate attachment legality
attachment_illegality->>GameState: read grant snapshots
attachment_illegality-->>StateBasedActions: retain or remove attachment
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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_static/anthem.rs`:
- Around line 1094-1100: Ensure every successful continuous-static construction
in the relevant parser flow passes through with_protection_does_not_remove,
including the preceding repeated-for-each branch that currently returns a bare
StaticDefinition. Prefer a shared final wrapper for all continuous-static return
paths so protection-rule behavior applies consistently to the full supported
card class.
🪄 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: f2034d64-cc86-4178-bc75-2a2d020e1c7d
📒 Files selected for processing (20)
PR_6499.mdcrates/engine/src/ai_support/mod.rscrates/engine/src/game/casting_tests.rscrates/engine/src/game/coverage.rscrates/engine/src/game/dungeon.rscrates/engine/src/game/effects/attach.rscrates/engine/src/game/effects/create_emblem.rscrates/engine/src/parser/oracle_modal.rscrates/engine/src/parser/oracle_static/anthem.rscrates/engine/src/parser/oracle_static/keyword_grant.rscrates/engine/src/parser/oracle_static/mod.rscrates/engine/src/parser/oracle_static/tests.rscrates/engine/src/types/ability.rscrates/engine/tests/integration/issue_6499_flickering_ward_protection_exemption.rscrates/engine/tests/integration/kaito_integration.rscrates/engine/tests/integration/main.rscrates/engine/tests/integration/najeela_extra_combat_grant_2898.rscrates/phase-ai/src/features/aggro_pressure.rscrates/phase-ai/src/policies/planeswalker_loyalty.rscrates/phase-ai/src/policies/reactive_self_protection.rs
Parse changes introduced by this PR✓ No card-parse changes detected. |
matthewevans
left a comment
There was a problem hiding this comment.
Blocked — the CR 702.16p (“already attached”) branch is modeled as a live condition rather than a one-time snapshot.
🔴 Blocker
crates/engine/src/game/effects/attach.rs:871 recomputes already_on_host every time attachment legality is checked. CR 702.16p instead says the exempt objects are those that were attached “when the protection effect starts to apply.” An Aura or Equipment that was attached before the grant but becomes the protected color later is therefore incorrectly retained by this code; it was not a matching object in the required start-time snapshot.
Model the eligible attachment identities when the particular protection grant begins to apply, and consult that per-grant snapshot in the shared attachment-legality authority. Add discriminating runtime coverage for (1) matching controlled attachments present at start, (2) an attachment that becomes matching later, and (3) a second protection instance of the same quality.
Source: CR 702.16p; the rule explicitly fixes the set at the effect-start boundary.
✅ Clean
I refuted the current CodeRabbit inline report: both the repeated and single for each branches in anthem.rs already pass through with_protection_does_not_remove on this head.
Request changes: preserve the one-time CR 702.16p attachment set, then provide the runtime regressions and fresh parse-diff/CI evidence.
702.16p fixes exempt attachments when protection begins applying, not on every later legality check. Record per-grant attachment IDs on the source, refresh once from layer evaluation, and add regressions for start-time retention, late-matching removal, and a second un-ridered grant. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
crates/engine/src/game/effects/attach.rs (1)
2685-2842: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMissing test coverage for the "you control" restriction on
ControlledAttachmentsAlreadyAttached. Both new test suites exercise the grant-start snapshot, the "becomes matching later" case, and the second-un-ridered-grant case, but neither tests the controller restriction thatcapture_protection_start_attachment_snapshotactually enforces (attachment.controller != grant_controller) — the case where an already-attached Aura/Equipment is controlled by a different player than the grant source's controller and must therefore NOT be exempted, per Benevolent Blessing's actual wording ("Auras and Equipment you control...").
crates/engine/src/game/effects/attach.rs#L2685-L2842: add acr_702_16p_*case where the equipment/aura attached to the host is controlled by a differentPlayerIdthan the grant source, asserting it is still removed by SBA despite matching the protection quality and being attached at grant-start.crates/engine/tests/integration/issue_6499_flickering_ward_protection_exemption.rs#L226-L445: add the same negative-case scenario usingapply_host_protection_grant.🤖 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/game/effects/attach.rs` around lines 2685 - 2842, Add negative 702.16p coverage for the controller restriction: in crates/engine/src/game/effects/attach.rs lines 2685-2842, add a cr_702_16p_* test where the attached matching Equipment or Aura is controlled by a different PlayerId than the protection grant source, and assert SBA removes the attachment; add the equivalent scenario using apply_host_protection_grant in crates/engine/tests/integration/issue_6499_flickering_ward_protection_exemption.rs lines 226-445. Ensure both cases are attached at grant start and match the protection quality, but are not exempted because control differs.Source: Path instructions
🤖 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/game/effects/attach.rs`:
- Around line 968-998: Update the snapshot application loop in the
protection-start attachment handling to merge each new snapshot into the
existing per-host vector instead of overwriting it with insert. Use the host
entry in protection_start_exempt_attachments to append or extend snapshot
attachments while preserving snapshots from earlier grants for the same host_id.
---
Nitpick comments:
In `@crates/engine/src/game/effects/attach.rs`:
- Around line 2685-2842: Add negative 702.16p coverage for the controller
restriction: in crates/engine/src/game/effects/attach.rs lines 2685-2842, add a
cr_702_16p_* test where the attached matching Equipment or Aura is controlled by
a different PlayerId than the protection grant source, and assert SBA removes
the attachment; add the equivalent scenario using apply_host_protection_grant in
crates/engine/tests/integration/issue_6499_flickering_ward_protection_exemption.rs
lines 226-445. Ensure both cases are attached at grant start and match the
protection quality, but are not exempted because control differs.
🪄 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: d70946bc-9b70-400a-9cf0-2c9f2c74b66f
📒 Files selected for processing (4)
crates/engine/src/game/effects/attach.rscrates/engine/src/game/game_object.rscrates/engine/src/game/layers.rscrates/engine/tests/integration/issue_6499_flickering_ward_protection_exemption.rs
matthewevans
left a comment
There was a problem hiding this comment.
Requested changes
P1 — Preserve the CR 702.16p snapshot per protection grant
refresh_protection_start_attachment_snapshots builds one ActiveGrant per matching AddKeyword(Protection), but stores only HashMap<host_id, attachment_ids> on the source. Its contains_key(&grant.host_id) guard therefore captures only the first protection grant for a source/host; protection_grant_exempts_attachment likewise cannot distinguish which grant is asking.
That is not a per-effect snapshot. For a supported continuous static that grants protection from red and blue with the rider, a red and a blue controlled attachment may both be present when the effect begins, but only the first quality is captured. The second matching attachment is incorrectly removed. Simply unioning vectors by host is also insufficient when separate same-source grants begin/end independently.
Please key the snapshot by the individual continuous-effect/protection-grant identity (plus host), and add a discriminating runtime test with two qualifying protection grants from the same source to the same host. The test should prove each attachment present at the relevant grant start is retained, and should fail with the current host-only key.
P1 — Cover the printed you control restriction
Add the negative CR 702.16p case: a matching Aura or Equipment controlled by the other player is already attached when the rider grant begins, but must still be removed/unattached. The implementation checks attachment.controller != grant_controller; there is no runtime assertion proving that boundary.
Scope hygiene
Remove PR_6499.md. It is generated implementation narrative duplicated by the PR description, not repository documentation requested for this change.
The parser and core Flickering Ward scenario otherwise trace through the intended shared seams, and current CI/parse-diff evidence is green. These mechanics/rules gaps block approval.
matthewevans
left a comment
There was a problem hiding this comment.
Blocked — CR 702.16p snapshots are keyed to a source/host pair, not the individual protection effect.
🔴 Blocker
crates/engine/src/game/effects/attach.rs:897 groups active ControlledAttachmentsAlreadyAttached grants by source; attach.rs:971 treats any entry for a host as already snapshotted; and attach.rs:1028 looks the exemption up by only source + host. Two separate continuous protection effects from one source can therefore inherit each other’s snapshot. If the first effect captured an attachment and later stops, a second effect on the same host can exempt that attachment even though it was not matching when the second effect began.
CR 702.16p fixes the set “when the protection effect starts to apply” and separately says other protection instances apply normally: official Comprehensive Rules. The existing effect-identity vocabulary is StaticGateKey { source, def_index } at crates/engine/src/types/game_state.rs:6161.
Please key capture, pruning, and lookup by the specific active StaticDefinition effect plus host (not just source + host). Add a runtime regression where one same-source rider effect snapshots a blue attachment, stops, then a second white rider effect begins while the attachment is blue and the attachment later becomes white; SBA must remove it. Also add the controller-negative case CodeRabbit identified: a matching attachment controlled by the other player at effect start is not exempt.
✅ Clean
The earlier CodeRabbit parser thread is refuted on this head: both relevant continuous-static return paths in crates/engine/src/parser/oracle_static/anthem.rs:1094 and anthem.rs:1112 already apply with_protection_does_not_remove. The Oracle text for Flickering Ward, Spectra Ward, and Benevolent Blessing was independently verified with Scryfall.
Request changes: make the CR 702.16p snapshot effect-scoped and cover the two boundary cases above; then rerun the current-head checks.
Key attachment exemptions by (def_index, host) on the grant source rather than source/host alone, store resolved quality with each snapshot so a replaced effect at the same slot cannot inherit a prior capture, and add regressions for same-source effect replacement and opponent control. Co-authored-by: Cursor <cursoragent@cursor.com>
matthewevans
left a comment
There was a problem hiding this comment.
Blocked — the CR 702.16p snapshot key still conflates distinct protection effects inside one static definition.
🔴 Blocker
- [HIGH] Key the snapshot by the individual protection modification, not only its static-definition index and host. Evidence: crates/engine/src/game/effects/attach.rs:928 iterates every AddKeyword(Protection) modification, but attach.rs:999 stores each result under only (def_index, host_id); the later quality overwrites the earlier snapshot, and attach.rs:1066 can therefore no longer exempt the first protection effect. The parser deliberately emits multiple protection modifications in one static definition (crates/engine/src/parser/oracle_static/keyword_grant.rs:1959). Why it matters: an effect granting protection from two qualities with the 702.16p rider can incorrectly remove an attachment that was already attached when the first protection instance began applying. Suggested fix: include the protection modification index (or an equivalent stable per-effect identity) in the snapshot key and add a two-quality, two-attachment runtime test.
Recommendation: request changes; preserve a separate CR 702.16p snapshot for every protection instance before re-review.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/engine/src/game/effects/attach.rs (1)
899-1029: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winInclude the protection modification index in the snapshot key.
expand_protection_partscan turn one grant into multipleAddKeyword(Protection(_))entries, butrefresh_protection_start_attachment_snapshotsstill keys them by(def_index, host_id). The later insert overwrites the earlier quality, so one set of already-attached auras/equipment loses its exemption and gets removed by the 702.16p check. Thread a per-modification discriminator throughActiveGrant, the snapshot map, and the lookup.🤖 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/game/effects/attach.rs` around lines 899 - 1029, Update refresh_protection_start_attachment_snapshots to include each protection modification’s index in ActiveGrant and ProtectionEffectHostKey, using the index from def.modifications while iterating protection entries. Propagate this discriminator through active-key pruning, existing-snapshot lookup, capture tuples, and map insertion so multiple expanded protections for the same definition and host retain separate snapshots.Source: Learnings
🤖 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/game/game_object.rs`:
- Around line 392-396: Implement a serde adapter for the
protection_start_exempt_attachments field in the relevant game-object
serialization code, following the existing custom adapter pattern used by other
tuple-keyed HashMap fields. Apply the adapter to the field’s serde attributes
while preserving its default and skip_serializing_if behavior, so populated
(usize, ObjectId) keys serialize and deserialize correctly.
---
Outside diff comments:
In `@crates/engine/src/game/effects/attach.rs`:
- Around line 899-1029: Update refresh_protection_start_attachment_snapshots to
include each protection modification’s index in ActiveGrant and
ProtectionEffectHostKey, using the index from def.modifications while iterating
protection entries. Propagate this discriminator through active-key pruning,
existing-snapshot lookup, capture tuples, and map insertion so multiple expanded
protections for the same definition and host retain separate snapshots.
🪄 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: 736dc0de-1792-4370-a362-978b6d107a78
📒 Files selected for processing (3)
crates/engine/src/game/effects/attach.rscrates/engine/src/game/game_object.rscrates/engine/tests/integration/issue_6499_flickering_ward_protection_exemption.rs
|
Blocked — the delta fixes the per-modification snapshot key correctly, but it lands CI red on a clippy error it introduced, and the regression test the last review asked for is non-discriminating. Reviewed at head 🔴 Blocker1. CI is red on a lint this delta introduced. This is contributor-caused and specific to this commit, not churn from 2. obj.static_definitions.clear();
obj.base_static_definitions = Arc::new(Vec::new());
obj.base_characteristics_initialized = false;
obj.protection_start_exempt_attachments.clear(); // <-- the thing under testBecause the test empties The inherit-prevention path it claims to cover is the one where production keeps the key live: 🟡 Non-blocking3. The recapture guard was weakened from a quality check to a presence check. 4. The required parse-diff evidence is non-discriminating for this change. The
An +88-line parser change that stamps a new rider onto three real cards should be visible there. Every 5. Mid-file ✅ Clean
Recommendation: fix the clippy error at |
Bundle grant identity into ProtectionEffectHostKey to satisfy clippy, re-snapshot when resolved protection quality changes at the same key, and stop clearing snapshot maps in the inherit-prevention regression. Co-authored-by: Cursor <cursoragent@cursor.com>
362a5e3 to
7b85b13
Compare
|
All three prior blockers are resolved on this head — one new HIGH blocker remains (a serialized-surface contract), plus a note that your green parse-diff is structurally blind here. The CR work, the per-modification keying, and the Oracle-text fidelity are all correct; please don't re-litigate those. Reviewed at head ✅ Resolved since the last review
🔴 Blocker[HIGH]
Production reach: Trigger: only Two in-repo precedents solve exactly this, one of them on
Suggested fix: a CodeRabbit raised this on 2026-07-23T15:32:45Z and it is still unaddressed at head. Honesty note: this was not execution-confirmed — Tilt owns the cargo target lock and I did not run cargo. The evidence is serde_json's documented key-must-be-string constraint plus the two existing in-repo adapters. Confidence high, not observed. 🟡 Non-blocking
✅ Clean — verified, not assumed
Recommendationrequest-changes on the serde adapter only. Add the tuple-key serde module plus a Your CI red is not your fault — |
The main merge bumped client/src-tauri/Cargo.toml to 0.35.2 but left the lockfile package entry at 0.35.1, breaking CI's --locked compile check. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The HIGH serde blocker is resolved, and the parse-diff is now genuinely discriminating. One small test restore and this is good to go. Reviewed at head ✅ The HIGH blocker is fixed
It follows the Rejecting The round-trip test ( ✅ CR 702.16n / 702.16p verifiedBoth grep-verified and both describe the code they annotate:
Worth recording why these are worth double-checking: the subrule sequence runs j, k, m, n, p — the CR skips ✅ The parse-diff projection is the highest-leverage thing in this PR
Heads-up: the sticky currently rendered on this PR is stale and still says "✓ No card-parse changes detected." Don't read that green as evidence — its Pulled from the fresh artifact on the head run instead: 14 cards / 3 signatures against baseline Your other two non-blocking items are resolved too: the Spectra Ward 🟡 One restore before I approve
So the Benevolent Blessing sibling kept its copy and the this-Aura test at This isn't a hypothetical failure mode — it's live in the parser today. From
Unrecognized trailing quality prose genuinely lands in 🟡 Two nits while you're in there
📋 Not yours
And a follow-up worth filing but explicitly not a gate here: Spectra Ward's quality parses as CI: no terminal red at head. Both Rust test shards, lint, card-data, WASM, Tauri, frontend and both AI gates are green; the Recommendation: restore the negative assertion and I'll approve. The CR 702.16n/p work, the per-modification keying, and the Oracle-text fidelity are all correct — no need to revisit any of it. |
matthewevans
left a comment
There was a problem hiding this comment.
Approving. The CR 702.16n / CR 702.16p modeling was already correct after your last round — the per-modification snapshot keying, the CR citations, the 15/15 class coverage, and the Oracle fidelity all held up under review. Thanks for turning this around across six rounds; the architecture landed in the right place.
I pushed two maintainer fixup commits rather than sending you back for a seventh round. Both are co-authored to you.
968df738 — the one real blocker. GameObject::protection_start_exempt_attachments is keyed by a (usize, usize, ObjectId) tuple. serde_json only accepts string-like and integer-like map keys; a tuple serializes as a sequence and is rejected outright. So the entire GameState failed to serialize the moment any rider grant was on the battlefield, breaking export_game_state_json (AI root-parallelism state transfer) and PersistedGameState (server persistence and undo/restore). skip_serializing_if = "HashMap::is_empty" is what hid it — the map is empty until a grant actually resolves, and attach.rs inserts an entry for every active rider grant even when attachment_ids is empty, so simply having Benevolent Blessing in play was enough to trip it.
Fixed with a protection_exempt_map_serde adapter encoding the key as "defIdx_modIdx_hostId", mirroring types::game_state::tuple_key_map (same default + skip_serializing_if + with attribute shape). Deserialization requires exactly three components, so a malformed key is an error rather than a silent truncation.
Worth naming explicitly for future reference: #[serde(skip)] would have been the wrong fix. It compiles and it makes the error go away, but it silently drops the CR 702.16p start-time snapshot across a restore — which revives the exact SBA violation this PR exists to fix, and does so invisibly.
Also in that commit:
coverage::static_detailsnow projectsprotection_does_not_remove. Without this the<!-- coverage-parse-diff -->sticky was structurally blind to the entire layer this PR operates on, so its green carried no signal for these 15 cards.- A parser test for
ProtectionDoesNotRemove::Auras(Spectra Ward), which had zero coverage and is the shortest arm of a longest-matchalt()where ordering is load-bearing. The test also pins that ordering: it asserts the longer Benevolent Blessing rider does not truncate into the bareAurasarm. - An integration test round-tripping a populated snapshot map through serde_json. The map is populated through the real pipeline (
evaluate_layers), and there is an explicit non-vacuity probe asserting it is non-empty before serializing — an empty-map round trip is elided byskip_serializing_ifand would have passed against the broken code.
60d62cb — a CodeRabbit follow-up. The repeated-for each branch of parse_continuous_gets_has returned a bare StaticDefinition while its two siblings in the same function were wrapped in with_protection_does_not_remove. Now uniform. I declined the broader ask to route every continuous-static return through one wrapper — there are roughly 75 StaticDefinition::continuous() sites across oracle_static/ and none of the others parses a "gets/has" clause that could carry a trailing rider, so that refactor is out of proportion to this PR. Reasoning is in the resolved thread.
That second commit is behaviorally inert on the current pool: I enumerated all 15 cards carrying a This effect doesn't remove … rider against Scryfall and none uses repeated dynamic P/T. It is consistency insurance, not a live-defect fix, and I'd rather say that plainly than let it read as a bug that was caught.
One note that is on us, not you: the Tauri compile check red you hit came from our own release commit 21a53d50cb, which left client/src-tauri/Cargo.lock at 0.35.1 against a 0.35.2 Cargo.toml. Your b3448b1f2a fixed it correctly and that job is green now. That failure was never attributable to your work.
Verification on head 60d62cb. CI run 30051269883 completed green, every job: Rust lint (fmt, clippy, parser gate), Rust tests (shard 1/2), Rust tests (shard 2/2), Card data (generate, validate, coverage), WASM compile check, Tauri compile check, Frontend, Lobby worker, and the Rust (fmt, clippy, test, coverage-gate) aggregator. AI gate green.
Test collection moved 11787 → 11789 on shard 2/2 while shard 1/2 held at 11801 — +2 exactly, which is the two tests this fixup adds. That is the check that they were actually collected and run; nextest runs at --status-level fail, so a passing test prints nothing and a silently-uncollected test is indistinguishable from a passing one on the log alone.
And the parse-diff is now non-blind. It previously read "✓ No card-parse changes detected"; on this head it reports:
14 card(s), 3 signature(s) (baseline: main
21a53d50cbcb)
- 12 cards ·
protection_exempt:∅→Source— Black Ward, Blue Ward, Cho-Manno's Blessing (+9 more)- 1 card ·
protection_exempt:∅→Auras— Spectra Ward- 1 card ·
protection_exempt:∅→ControlledAttachmentsAlreadyAttached— Benevolent Blessing- 1 card excluded as Oracle-text errata
14 + 1 excluded = 15, matching the exact Scryfall population for oracle:"this effect doesn't remove". The blast radius is precisely the rider class and nothing else — no scope contamination — and all three enum arms are exercised on real cards. This also answers the open question from the earlier review round about whether that green parse-diff was a genuine no-op or a blind instrument: it was blind, and it isn't now.
f6fa1e3 to
1d9ca6a
Compare
Closes #6499
Summary
ProtectionDoesNotRemoveon continuous protection grants; parser recovers the rider (including post-normalization~); runtime honors per-grant exemptions inattachment_illegality.Test plan
cargo test -p engine --lib -- protection_chosen_color_drops_trailing_this_auracargo test -p engine --test integration -- flickering_wardSummary by CodeRabbit
New Features
Bug Fixes
Tests