Skip to content

Add Lady Loki, Agent of Chaos - #6945

Merged
matthewevans merged 7 commits into
phase-rs:mainfrom
JacobWoodson:card/lady-loki-agent-of-chaos
Aug 4, 2026
Merged

Add Lady Loki, Agent of Chaos#6945
matthewevans merged 7 commits into
phase-rs:mainfrom
JacobWoodson:card/lady-loki-agent-of-chaos

Conversation

@JacobWoodson

@JacobWoodson JacobWoodson commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds engine support for Lady Loki, Agent of Chaos.

Files changed

  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\parser\oracle_nom\quantity.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\quantity.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\effects\deal_damage.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\src\game\effects\mod.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\tests\integration\lady_loki_agent_of_chaos.rs
  • C:\Users\jacob\source\repos\phase-card-runs\crates\engine\tests\integration\main.rs

CR references

  • CR 109.5
  • CR 120.3
  • CR 202.3
  • CR 202.3e
  • CR 400.7
  • CR 608.2c
  • CR 616.1e

Track

Developer

LLM

Model: claude-opus-4-8
Thinking: high

Tier: Frontier

Verification

  • cargo fmt --all — clean
  • ./scripts/check-parser-combinators.sh (Gate A) — clean (after env fix: hardcoded python3 resolved to the broken Windows App Store stub causing a false 'cross-product detector RED' failure; reran with msys64 python3 on PATH -> Gate G PASS + Gate A PASS)
  • cargo clippy-strict — incomplete - still compiling in background (cold build; foreground attempt timed out at 10m, background run still 'Checking phase-engine', no result yet)
  • cargo test -p phase-engine — not run (blocked on clippy-strict completing in the && chain)
  • ./scripts/gen-card-data.sh — not run (blocked on prior steps)
  • cargo coverage — not run
  • cargo semantic-audit — not run

Re-verified at chunk-1 checkpoint with freshly regenerated card-data: all listed cards supported:true gap:0, semantic-audit clean. The run-time 'partial' was a stale-card-data artifact, not a code defect.

Scope Expansion

None. (Scope narrowed vs plan: the R6 zones/LKI change for EventSource-across-exile proved unnecessary — move_to_zone reuses the object id on Stack->Exile, so the exiled spell's off-stack MV is read directly.)

Validation Failures

See review/cross-check notes.

CI Failures

  • Verification incomplete - forced to report before the build chain finished. cargo clippy-strict was still compiling (cold build; single foreground run timed out at 10m, background run had only reached 'Checking phase-engine' with no clean/error result).
  • cargo test -p phase-engine: NOT RUN (fallback path is clippy-strict && test-engine && gen-card-data; blocked because clippy had not yet returned clean).
  • ./scripts/gen-card-data.sh: NOT RUN.
  • cargo coverage: NOT RUN - 'Lady Loki, Agent of Chaos' supported/gap status UNCONFIRMED, so it is NOT in cardsSupported.
  • cargo semantic-audit: NOT RUN - 'Lady Loki, Agent of Chaos' finding count UNCONFIRMED, so semanticAuditClean is false.
  • Environment note (not a code failure): Tilt was down (tilt get uiresource clippy exit 1), so the direct-cargo fallback path was correctly selected. Also, python3/python on PATH resolve to the Windows App Store stub (Permission denied, exit 126); a working interpreter exists at C:/msys64/mingw64/bin/python3.exe (3.9.7). Gate A only passes when that msys64 python3 is prepended to PATH.

Summary by CodeRabbit

  • Bug Fixes

    • Improved damage effects that pause for replacements, preserving correct targets and per-player values.
    • Fixed mana-value calculations for targeted cards, including qualified references such as “that nonland card.”
    • Corrected target and scope handling in nested quantity calculations.
    • Improved Lady Loki, Agent of Chaos interactions, including mana-value damage, exile sequencing, and optional casting of exiled cards.
  • Tests

    • Added coverage for Lady Loki’s parsing and gameplay behavior.

@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85d11278-1a7d-41df-9878-070266b3400b

📥 Commits

Reviewing files that changed from the base of the PR and between c869f45 and 70332cf.

📒 Files selected for processing (6)
  • crates/engine/src/game/effects/deal_damage.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/engine.rs
  • crates/engine/src/game/quantity.rs
  • crates/engine/src/parser/oracle_nom/quantity.rs
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/engine/tests/integration/main.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/quantity.rs
  • crates/engine/src/parser/oracle_nom/quantity.rs
  • crates/engine/src/game/effects/deal_damage.rs

📝 Walkthrough

Walkthrough

The change adds target-aware quantity resolution, parses qualified target card references, and preserves parent object targets through paused damage continuations. New tests cover Lady Loki’s parser and runtime behavior.

Changes

Target-scoped damage resolution

Layer / File(s) Summary
Target-aware quantity parsing and resolution
crates/engine/src/parser/oracle_nom/quantity.rs, crates/engine/src/game/quantity.rs
Qualified card references now resolve with target scope. Scoped quantity expressions propagate explicit targets through nested expressions.
Target propagation through damage continuations
crates/engine/src/game/effects/mod.rs, crates/engine/src/game/effects/deal_damage.rs, crates/engine/src/game/engine.rs
Paused damage continuations preserve eligible parent object targets. DamageEachPlayer retains target context for current and remaining players.
Lady Loki integration coverage
crates/engine/tests/integration/lady_loki_agent_of_chaos.rs, crates/engine/tests/integration/main.rs
Integration tests cover mana-value damage, off-stack X values, exile sequencing, optional casting, parser structure, and module registration.

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

Sequence Diagram(s)

sequenceDiagram
  participant TriggeredSpell
  participant LadyLokiAbility
  participant ExileUntilEffect
  participant DamageEachPlayer
  participant QuantityResolver
  participant CastFromZone
  TriggeredSpell->>LadyLokiAbility: trigger on first matching spell
  LadyLokiAbility->>ExileUntilEffect: exile cards until a nonland card
  ExileUntilEffect->>DamageEachPlayer: resolve damage effect
  DamageEachPlayer->>QuantityResolver: resolve target-scoped mana values
  DamageEachPlayer->>CastFromZone: offer optional free cast of exiled card
Loading

Possibly related PRs

Suggested reviewers: matthewevans, 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 The title clearly identifies the primary change: adding engine support for Lady Loki, Agent of Chaos.
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 unit tests (beta)
  • Create PR with unit tests

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.

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

159-167: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the hit's zone instead of discarding the binding.

let _ = hit; discards the id. The test then has no reach guard proving the dig actually exiled a nonland card with mana value 1. If the dig failed and the amount fell back to another path that also produced 2, the test would pass for the wrong reason.

💚 Replace the discard with a reach guard
-    let _ = hit;
-
     let mut runner = scenario.build();
     let outcome = runner.cast(spell).x(3).resolve();
+
+    // Reach guard: the dig exiled the nonland hit, so MV(hit) = 1 was read.
+    assert_eq!(
+        outcome.zone_of(hit),
+        engine::types::zones::Zone::Exile,
+        "the nonland hit must be exiled by the dig"
+    );

As per path instructions: "require a paired positive reach-guard proving the input actually reached the code under test".

🤖 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/lady_loki_agent_of_chaos.rs` around lines 159
- 167, Replace the discarded hit binding in the test setup with a positive reach
guard that asserts the card identified by hit is in the exile zone after the
dig. Keep the existing result assertion, ensuring the test proves the nonland
mana-value-1 card reached the code path under test.

Source: Path instructions

crates/engine/src/game/effects/deal_damage.rs (2)

3081-3088: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Tighten the chain assertion to an exact match.

The assertion uses any(...). A regression that re-stashes the paused first opponent (P1) alongside P2 would still pass. Assert the full summary instead, so the test pins both the amount and the exact remaining-player set.

💚 Exact-match assertion
-        let summary = collect_chain_summary(&cont.chain);
-        assert!(
-            summary.iter().any(
-                |(_, target, amount)| *target == TargetRef::Player(PlayerId(2)) && *amount == 3
-            ),
-            "remaining opponent must be stashed with |MV(spell) − MV(hit)| = 3 (a \
-             target-dropped pre-resolve gives 4): {summary:?}"
-        );
+        let summary = collect_chain_summary(&cont.chain);
+        assert_eq!(
+            summary,
+            vec![(source, TargetRef::Player(PlayerId(2)), 3)],
+            "only the remaining opponent must be stashed, with \
+             |MV(spell) − MV(hit)| = 3 (a target-dropped pre-resolve gives 4)"
+        );

As per path instructions: "Test adequacy is the highest-frequency contributor finding — scrutinize it."

🤖 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/deal_damage.rs` around lines 3081 - 3088,
Update the assertion around collect_chain_summary to require an exact summary
rather than merely finding a matching entry: verify the remaining stashed player
set contains only PlayerId(2) and that its damage amount is 3, so an extra
PlayerId(1) entry fails the test.

Source: Path instructions


1028-1037: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a negative case that proves the guard blocks propagation.

cast_tail_with_parent_targets copies the parent's object targets into every stashed tail when should_propagate_parent_targets returns true. The new regression test at Line 2964 only covers the true branch. No test covers the false branch on a pause path. If the predicate ever diverges from resolve_ability_chain, a plain multi-target DealDamage tail would silently gain the parent's object referents and damage the wrong recipients after a replacement pause.

Add a paired case: a parent DealDamage with two object targets and a sub whose target_choice_timing is Resolution, then assert the stashed tail keeps targets.is_empty().

As per path instructions: "For every negative assertion … require a paired positive reach-guard proving the input actually reached the code under test".

🤖 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/deal_damage.rs` around lines 1028 - 1037, Add
a paired regression test near the existing propagation test for
cast_tail_with_parent_targets: use a parent DealDamage with two object targets
and a sub whose target_choice_timing is Resolution, then assert the stashed
tail’s targets remain empty. Include a positive reach-guard confirming the
replacement pause reaches the stashed-tail path before the negative assertion,
and leave the true-branch coverage intact.

Source: Path instructions

crates/engine/src/parser/oracle_nom/quantity.rs (1)

4628-4639: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Compose the non prefix as its own axis instead of enumerating two literals.

parse_card_type_qualifier enumerates nonland and noncreature as separate literal arms, while delegating bare types to parse_core_type. The non prefix is an independent axis. Composing it over parse_core_type covers the whole class (nonartifact card's mana value, nonenchantment card's mana value, …) with the same node count.

♻️ Compose the prefix axis
 fn parse_card_type_qualifier(input: &str) -> OracleResult<'_, ()> {
     terminated(
-        alt((
-            value((), tag("nonland")),
-            value((), tag("noncreature")),
-            value((), tag("permanent")),
-            value((), parse_core_type),
-        )),
+        value(
+            (),
+            (
+                opt(tag("non")),
+                alt((value((), tag("permanent")), value((), parse_core_type))),
+            ),
+        ),
         tag(" "),
     )
     .parse(input)
 }

As per coding guidelines: "Compose alt() per axis; never enumerate the cartesian product as separate tag("full string") arms."

🤖 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_nom/quantity.rs` around lines 4628 - 4639,
Update parse_card_type_qualifier to parse the optional “non” prefix separately
and compose it with parse_core_type, while retaining the standalone “permanent”
qualifier and trailing-space requirement. Remove the separate nonland and
noncreature literal arms so all non-prefixed and non-prefixed core card types
are handled through the shared parser.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@crates/engine/src/game/effects/deal_damage.rs`:
- Around line 3081-3088: Update the assertion around collect_chain_summary to
require an exact summary rather than merely finding a matching entry: verify the
remaining stashed player set contains only PlayerId(2) and that its damage
amount is 3, so an extra PlayerId(1) entry fails the test.
- Around line 1028-1037: Add a paired regression test near the existing
propagation test for cast_tail_with_parent_targets: use a parent DealDamage with
two object targets and a sub whose target_choice_timing is Resolution, then
assert the stashed tail’s targets remain empty. Include a positive reach-guard
confirming the replacement pause reaches the stashed-tail path before the
negative assertion, and leave the true-branch coverage intact.

In `@crates/engine/src/parser/oracle_nom/quantity.rs`:
- Around line 4628-4639: Update parse_card_type_qualifier to parse the optional
“non” prefix separately and compose it with parse_core_type, while retaining the
standalone “permanent” qualifier and trailing-space requirement. Remove the
separate nonland and noncreature literal arms so all non-prefixed and
non-prefixed core card types are handled through the shared parser.

In `@crates/engine/tests/integration/lady_loki_agent_of_chaos.rs`:
- Around line 159-167: Replace the discarded hit binding in the test setup with
a positive reach guard that asserts the card identified by hit is in the exile
zone after the dig. Keep the existing result assertion, ensuring the test proves
the nonland mana-value-1 card reached the code path under test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d316f225-37bf-4bb9-a19b-238ca6be8e18

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa1fc4 and 269497e.

📒 Files selected for processing (6)
  • crates/engine/src/game/effects/deal_damage.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/quantity.rs
  • crates/engine/src/parser/oracle_nom/quantity.rs
  • crates/engine/tests/integration/lady_loki_agent_of_chaos.rs
  • crates/engine/tests/integration/main.rs

@matthewevans matthewevans added the enhancement New feature or request label Aug 3, 2026
@matthewevans

matthewevans commented Aug 3, 2026

Copy link
Copy Markdown
Member

Generated for head 70332cfbe082743e64ce7fdce9be763b4c254692.

Parse changes introduced by this PR · 1 card(s), 2 signature(s) (baseline: main 93da0ca15532)

🟢 Added (1 signature)

  • 1 card · ➕ ability/DamageEachPlayer · added: DamageEachPlayer (amount=|event source's mana value - target's mana value|, players=each opponent)
    • Affected (first 3): Lady Loki, Agent of Chaos

🔴 Removed (1 signature)

  • 1 card · ➖ ability/deal · removed: deal
    • Affected (first 3): Lady Loki, Agent of Chaos

2 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@matthewevans matthewevans removed their assignment Aug 3, 2026
@matthewevans matthewevans self-assigned this Aug 3, 2026
@matthewevans

Copy link
Copy Markdown
Member

Maintainer hold for current head 6d705c61c44a93d86223a23d68f5e64e8631eb55: implementation review and approval will resume once required Rust checks settle and CI publishes a parse-diff bound to this head. The available parse-diff evidence is stale (bound to 337f1f208221487168ea7d23f94b13337f21105d), so it cannot support a current-head parser review. No implementation findings in this hold.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Request changes — current head 6d705c61c44a93d86223a23d68f5e64e8631eb55 has an unaccounted parser blast radius.

The current parse-diff reports O-Kagachi Made Manifest changing from unsupported where_x_binding to supported Pump (+target's mana value/+0) alongside the intended Lady Loki change: #6945 (comment). The new generic that [type] card qualifier is the shared parser seam (crates/engine/src/parser/oracle_nom/quantity.rs:4617-4668), and O-Kagachi uses it after a player chooses a nonland card in a graveyard, returns that chosen card, then gets +X/+0 for its mana value (data/mtgish-cards.json:28199).

Either add a production-pipeline O-Kagachi scenario proving the real choose → return → mana-value pump chain (including a discriminating nonzero mana value), or narrow/justify this additional parse change so coverage stays honest. Lady Loki-only tests do not establish the selected-authority/target threading for O-Kagachi's distinct choice path.

I also checked current CodeRabbit feedback: its only review is against older head 269497e4... and there are no unresolved current-head threads, so no separate current CodeRabbit reach-guard request applies.

@matthewevans matthewevans removed their assignment Aug 3, 2026
@JacobWoodson JacobWoodson changed the title Partial: Lady Loki, Agent of Chaos Add Lady Loki, Agent of Chaos Aug 4, 2026
…ghten Lady Loki tests

Addresses the PR review parser blast-radius: the opt(parse_card_type_qualifier)
"that card" arms widened the seam so a BARE "that card" also lowered to
ObjectScope::Target. That flipped O-Kagachi Made Manifest's "...where X is the
mana value of that card" from an honest where_x_binding gap into a dishonest
Pump (+target's mana value/+0) -- but O-Kagachi's "that card" is a card the
defending player CHOSE from a graveyard, not a threaded target, so no target is
ever wired for the pump to read.

Narrow both the possessive and prepositional arms to REQUIRE the type qualifier
(parse_card_type_qualifier), keeping Lady Loki's type-qualified "that nonland
card's mana value" (bound to the ExileFromTopUntil producer) while reverting
O-Kagachi to its baseline where_x_binding -- verified end-to-end via
parse_oracle_text. Also compose the non prefix as its own opt(tag("non"))
axis over parse_core_type so every "non<type>" qualifier is covered by one node
set instead of enumerated literals.

Tests:
- rename/extend the positive scope test to cover the composed non-axis
- add bare_of_that_card_mana_value_is_not_target_scope honesty guard (O-Kagachi
  prepositional form), paired with the positive reach-guard
- Lady Loki X-spell test: replace `let _ = hit;` with an Exile reach guard
- exact-match the DamageEachPlayer pause chain summary
- add the false-branch pair proving a Resolution-timed sub does not inherit
  parent targets on the pause path

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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

🤖 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_nom/quantity.rs`:
- Around line 4632-4636: Correct the documentation above the non qualifier
parser by removing the inaccurate CR 205.2b citation or replacing it with a
verified rule citation whose text explicitly defines the non qualifier behavior;
leave the parser implementation and canonical parse_core_type delegation
unchanged.
- Around line 4645-4647: Extend parse_core_type to recognize the vanguard card
type so parse_card_type_qualifier supports complete card-type coverage. Add
regression cases for both “that vanguard card’s mana value” and “mana value of
that vanguard card,” covering the possessive and prepositional forms while
preserving existing card-type parsing.
🪄 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: a5559522-fee0-4131-845e-35f82f1f7ef4

📥 Commits

Reviewing files that changed from the base of the PR and between 337f1f2 and c869f45.

📒 Files selected for processing (5)
  • crates/engine/src/game/effects/deal_damage.rs
  • crates/engine/src/game/quantity.rs
  • crates/engine/src/parser/oracle_nom/quantity.rs
  • crates/engine/tests/integration/lady_loki_agent_of_chaos.rs
  • crates/engine/tests/integration/main.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/lady_loki_agent_of_chaos.rs
  • crates/engine/src/game/effects/deal_damage.rs
  • crates/engine/src/game/quantity.rs

Comment thread crates/engine/src/parser/oracle_nom/quantity.rs Outdated
Comment on lines +4645 to +4647
opt(tag("non")),
alt((value((), tag("permanent")), value((), parse_core_type))),
),

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

Support the missing vanguard card type.

parse_card_type_qualifier claims complete card-type coverage but delegates to parse_core_type, which excludes vanguard. Therefore, that vanguard card's mana value and mana value of that vanguard card do not parse. Add vanguard to the shared parser and add possessive and prepositional regression cases. Vanguard is a card type under CR 205.2a and CR 300.1. (media.wizards.com)

As per path instructions, strict fidelity to the Comprehensive Rules is non-negotiable.

Also applies to: 10887-10896

🤖 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_nom/quantity.rs` around lines 4645 - 4647,
Extend parse_core_type to recognize the vanguard card type so
parse_card_type_qualifier supports complete card-type coverage. Add regression
cases for both “that vanguard card’s mana value” and “mana value of that
vanguard card,” covering the possessive and prepositional forms while preserving
existing card-type parsing.

Source: Path instructions

@JacobWoodson

Copy link
Copy Markdown
Contributor Author

@matthewevans thanks for the catch on the parse blast radius. Addressed in c869f45c by taking the narrow option.

O-Kagachi (the blast radius): the opt(parse_card_type_qualifier) "that card" arms were letting a bare "that card" lower to ObjectScope::Target. That's what flipped O-Kagachi Made Manifest's "…where X is the mana value of that card" from an honest where_x_binding gap into a Pump (+target's mana value/+0) — but O-Kagachi's "that card" is a card the defending player chose from a graveyard, not a threaded target, so nothing is ever wired for that pump to read.

Both the possessive and prepositional arms now require the type qualifier. Lady Loki's type-qualified "that nonland card's mana value" (bound to the ExileFromTopUntil producer) still binds to Target; a bare "that card" no longer does. Verified end-to-end through parse_oracle_text that O-Kagachi reverts to its baseline:

"effect": { "type": "Unimplemented", "name": "where_x_binding",
            "description": "where X is the mana value of that card" }

So the regenerated parse-diff should now show only Lady Loki. No separate O-Kagachi choose→return→pump scenario is added, since that path (a defending-player choice from a graveyard) isn't implemented and this keeps coverage honest rather than claiming it.

Also in this commit:

  • Composed the non prefix as its own opt(tag("non")) axis over parse_core_type (covers the whole non<type> class instead of enumerated literals).
  • The CodeRabbit nitpicks: exact-match the DamageEachPlayer pause chain summary; added the false-branch pair proving a Resolution-timed sub does not inherit parent targets on the pause path; replaced let _ = hit; with an Exile reach-guard; added a bare_of_that_card_mana_value_is_not_target_scope honesty guard paired with the positive reach-guard.

cargo fmt + clippy -D warnings clean; parser/pause unit tests and the four Lady Loki integration tests pass.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Current-head review for 5ec8afdd28365abe6fb7b716bf847349a799a948 — changes requested.

[MED] The parser coverage artifact is not bound to this review head. Evidence: the full <!-- coverage-parse-diff --> marker was updated at 2026-08-04T02:48:54Z, but its body says it was generated for c869f45cfab59ab48a3013749994e71cbefb616f, rather than 5ec8afdd28365abe6fb7b716bf847349a799a948; parser files changed in the intervening merge. Why it matters: the current card-level parse blast radius is therefore unverified. Suggested fix: regenerate the parse-diff artifact for the current head before coverage acceptance.

[MED] The new qualifier documentation has false rules provenance and an inaccurate completeness claim. Evidence: crates/engine/src/parser/oracle_nom/quantity.rs:4632-4637 attributes the non prefix to CR 205.2b and says the delegated parser covers the full core set. CR 205.2b instead concerns multi-card-type applicability; the canonical parse_core_type intentionally omits Vanguard because CoreType does not model the out-of-scope Vanguard avatars (crates/engine/src/parser/oracle_nom/primitives.rs:345-354). Why it matters: this repository requires every CR annotation to describe the code it annotates. Suggested fix: remove the CR 205.2b attribution and describe the qualifier as covering the supported CoreType vocabulary; do not add new Vanguard support in this PR.

… qualifier doc

The parse_card_type_qualifier doc attributed the `non` prefix to CR 205.2b and
claimed complete coverage of the card-type set. Both are inaccurate:

- CR 205.2b concerns multi-card-type applicability, not a `non` qualifier. Drop
  the citation; the `non` prefix is a plain grammatical axis with no dedicated CR.
- parse_core_type intentionally omits `vanguard` (CoreType models no Vanguard
  variant), so the qualifier does NOT cover the full CR 300.1 card-type set.
  Describe coverage as exactly the supported CoreType vocabulary and note vanguard
  is out of scope and not added by this PR.

Per-repo rule: every CR annotation must describe the code it annotates. No
behavior change; documentation only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@matthewevans matthewevans self-assigned this Aug 4, 2026
Maintainer fixup for current-main line movement.

Co-authored-by: Jacob Woodson <38709105+JacobWoodson@users.noreply.github.com>
@matthewevans

Copy link
Copy Markdown
Member

Maintainer fixup pushed at 70332cfbe082743e64ce7fdce9be763b4c254692: rebased the CR 603.5 prompt-census pins against the current CI merge tree (game/effects/mod.rs 5996/6073/90485999/6076/9051). This is maintainer-side line movement, not a semantic change to the contribution. Holding approval and queue action until CI and the parse artifact finish for this new head.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved for current head 70332cfbe082743e64ce7fdce9be763b4c254692.

The current parse-diff is bound to this head and confines parser impact to Lady Loki (one card, two signatures). Current CI is green. The remaining open CodeRabbit Vanguard thread is refuted by the current code: parse_card_type_qualifier explicitly consumes only the supported CoreType vocabulary, and CoreType intentionally has no Vanguard variant; expanding that engine model is outside this PR's Lady Loki seam.

@matthewevans matthewevans removed the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 4, 2026
@matthewevans
matthewevans added this pull request to the merge queue Aug 4, 2026
@matthewevans matthewevans removed their assignment Aug 4, 2026
Merged via the queue into phase-rs:main with commit cdb99ba Aug 4, 2026
13 checks passed
@JacobWoodson
JacobWoodson deleted the card/lady-loki-agent-of-chaos branch August 4, 2026 06:13
JacobWoodson added a commit to JacobWoodson/phase that referenced this pull request Aug 4, 2026
Bring the PR up to date with main @ cdb99ba (Lady Loki, Agent of Chaos phase-rs#6945)
and resolve the CR 603.5 prompt-census pin conflict.

phase-rs#6945 re-baselined the three pinned `WaitingFor::OptionalEffectChoice` producers
5996/6073/9048 -> 5999/6076/9051 (its own +3 above them). This PR then inserts
+13 above the same producers (paid graveyard-cast support at effects/mod.rs:2957),
so the merged coordinates are 5999/6076/9051 -> 6012/6089/9064. The producers are
byte-identical, add zero new census needles, scoped_library_search.rs:452 and
engine.rs:11427 are unmoved, and the total/partition stay 37 and 5/7/25. Pure
coordinate drift — pin resolved to the merged coordinates with an updated
drift-log entry; census assertion verified matching on the merged tree (the
Linux path-separator form the pin uses is what CI scans).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cuinhellcat added a commit to cuinhellcat/phase that referenced this pull request Aug 14, 2026
… route

A creature's activated ability was unreachable whenever an attached Aura or
Equipment was activatable too. Reported for Slumbering Keepguard (`{2}{W}`);
reproduces with Cooped Up, whose `{2}{W}: Exile enchanted creature` is
legitimately activatable from the battlefield, so no engine defect is needed.

During Priority `HumanResponseModel::ExactCandidates` publishes an attachment
fan for every activatable attachment, so `viewerInteraction.attachmentFans`
carries an entry for the host. The `attachmentsActionable` branch read that as
"the host is not a legal choice" — an unchecked premise — and sat above the
activation branch, so every click on the host opened the chooser instead. The
chooser excludes the host by design (`id !== host.id`), so the host's ability
had no path at all. The sibling branch that reads the affordance sets is placed
last for exactly this reason and documents it; this one was not.

Moved below the host's own target / activation / undo intent, which also makes
the click ladder match the glow-ring priority ladder it advertises. Introduced
by phase-rs#6945 (a card PR), which is why the click order was never reviewed as such.

Handing the click back to the host strands the attachments unless they keep a
route of their own, and they had none in this state: `attachmentsActionable` is
itself one of the disjuncts that expands the attachment stack, so no `+N`
control renders, while the `⧉` control required exactly one attachment. Each
Aura was then reachable only through a ~22px peek behind the host face. `⧉` now
renders whenever attachments are present and the stack is expanded — the same
predicate `+N` is derived from, so the two are complementary and exactly one
route exists in every state that needs one.

The `⧉` label counts what the fan will actually show
(`interactionAttachmentFan?.children.length`), not `obj.attachments.length`:
while an interaction is live the fan builds its cards from the projection, so
counting the raw snapshot would promise more cards than appear, and would
rediscover from the snapshot what the note above that line forbids.

`permanent.viewAttachmentsFor` now interpolates a real count, which exposed a
missing CLDR plural category: Polish selects `few`/`many` at 2/3/5 and neither
key existed, so `pl` fell back to English. Added `_few`/`_many` in all seven
catalogs (never selected outside `pl`, required by the `en`-oracle parity gate).
`permanent.hiddenAttachmentsAria` carried the identical defect and is fixed in
the same pass rather than left as the odd one out of a two-member class.

The control's SIZE is deliberately unchanged. It is now the pointer route where
the host's own click used to open the fan, and at `clamp(20px, …, 28px)` it is
under the 44px touch floor the branch above cites — but 44px is unreachable at
this seam: battlefield cards sit in an 8px gap and `--card-base` floors at
3.5rem, so growing outward puts ~26px over the NEIGHBOUR's face at `z-40` and
steals its clicks, while growing inward swallows most of a 56px card. Either
would re-create the click theft this commit undoes. Recorded at the gate as a
known limitation needing a layout-level answer, alongside the same gap on `+N`.

Also resets `viewerInteraction` in the test suite's shared setup — without it
the rows that publish a fan leak it into every later row, and the suite only
passed because they were declared last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cuinhellcat added a commit to cuinhellcat/phase that referenced this pull request Aug 14, 2026
… route

A creature's activated ability was unreachable whenever an attached Aura or
Equipment was activatable too. Reported for Slumbering Keepguard (`{2}{W}`);
reproduces with Cooped Up, whose `{2}{W}: Exile enchanted creature` is
legitimately activatable from the battlefield, so no engine defect is needed.

During Priority `HumanResponseModel::ExactCandidates` publishes an attachment
fan for every activatable attachment, so `viewerInteraction.attachmentFans`
carries an entry for the host. The `attachmentsActionable` branch read that as
"the host is not a legal choice" — an unchecked premise — and sat above the
activation branch, so every click on the host opened the chooser instead. The
chooser excludes the host by design (`id !== host.id`), so the host's ability
had no path at all. The sibling branch that reads the affordance sets is placed
last for exactly this reason and documents it; this one was not.

Moved below the host's own target / activation / undo intent, which also makes
the click ladder match the glow-ring priority ladder it advertises. Introduced
by phase-rs#6945 (a card PR), which is why the click order was never reviewed as such.

Handing the click back to the host strands the attachments unless they keep a
route of their own, and they had none in this state: `attachmentsActionable` is
itself one of the disjuncts that expands the attachment stack, so no `+N`
control renders, while the `⧉` control required exactly one attachment. Each
Aura was then reachable only through a ~22px peek behind the host face. `⧉` now
renders whenever attachments are present and the stack is expanded — the same
predicate `+N` is derived from, so the two are complementary and exactly one
route exists in every state that needs one.

The `⧉` label counts what the fan will actually show
(`interactionAttachmentFan?.children.length`), not `obj.attachments.length`:
while an interaction is live the fan builds its cards from the projection, so
counting the raw snapshot would promise more cards than appear, and would
rediscover from the snapshot what the note above that line forbids.

`permanent.viewAttachmentsFor` now interpolates a real count, which exposed a
missing CLDR plural category: Polish selects `few`/`many` at 2/3/5 and neither
key existed, so `pl` fell back to English. Added `_few`/`_many` in all seven
catalogs (never selected outside `pl`, required by the `en`-oracle parity gate).
`permanent.hiddenAttachmentsAria` carried the identical defect and is fixed in
the same pass rather than left as the odd one out of a two-member class.

The control's SIZE is deliberately unchanged. It is now the pointer route where
the host's own click used to open the fan, and at `clamp(20px, …, 28px)` it is
under the 44px touch floor the branch above cites — but 44px is unreachable at
this seam: battlefield cards sit in an 8px gap and `--card-base` floors at
3.5rem, so growing outward puts ~26px over the NEIGHBOUR's face at `z-40` and
steals its clicks, while growing inward swallows most of a 56px card. Either
would re-create the click theft this commit undoes. Recorded at the gate as a
known limitation needing a layout-level answer, alongside the same gap on `+N`.

Also resets `viewerInteraction` in the test suite's shared setup — without it
the rows that publish a fan leak it into every later row, and the suite only
passed because they were declared last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cuinhellcat added a commit to cuinhellcat/phase that referenced this pull request Aug 14, 2026
… route

A creature's activated ability was unreachable whenever an attached Aura or
Equipment was activatable too. Reported for Slumbering Keepguard (`{2}{W}`);
reproduces with Cooped Up, whose `{2}{W}: Exile enchanted creature` is
legitimately activatable from the battlefield, so no engine defect is needed.

During Priority `HumanResponseModel::ExactCandidates` publishes an attachment
fan for every activatable attachment, so `viewerInteraction.attachmentFans`
carries an entry for the host. The `attachmentsActionable` branch read that as
"the host is not a legal choice" — an unchecked premise — and sat above the
activation branch, so every click on the host opened the chooser instead. The
chooser excludes the host by design (`id !== host.id`), so the host's ability
had no path at all. The sibling branch that reads the affordance sets is placed
last for exactly this reason and documents it; this one was not.

Moved below the host's own target / activation / undo intent, which also makes
the click ladder match the glow-ring priority ladder it advertises. Introduced
by phase-rs#6945 (a card PR), which is why the click order was never reviewed as such.

Handing the click back to the host strands the attachments unless they keep a
route of their own, and they had none in this state: `attachmentsActionable` is
itself one of the disjuncts that expands the attachment stack, so no `+N`
control renders, while the `⧉` control required exactly one attachment. Each
Aura was then reachable only through a ~22px peek behind the host face. `⧉` now
renders whenever attachments are present and the stack is expanded — the same
predicate `+N` is derived from, so the two are complementary and exactly one
route exists in every state that needs one.

The `⧉` label counts what the fan will actually show
(`interactionAttachmentFan?.children.length`), not `obj.attachments.length`:
while an interaction is live the fan builds its cards from the projection, so
counting the raw snapshot would promise more cards than appear, and would
rediscover from the snapshot what the note above that line forbids.

`permanent.viewAttachmentsFor` now interpolates a real count, which exposed a
missing CLDR plural category: Polish selects `few`/`many` at 2/3/5 and neither
key existed, so `pl` fell back to English. Added `_few`/`_many` in all seven
catalogs (never selected outside `pl`, required by the `en`-oracle parity gate).
`permanent.hiddenAttachmentsAria` carried the identical defect and is fixed in
the same pass rather than left as the odd one out of a two-member class.

The control's SIZE is deliberately unchanged. It is now the pointer route where
the host's own click used to open the fan, and at `clamp(20px, …, 28px)` it is
under the 44px touch floor the branch above cites — but 44px is unreachable at
this seam: battlefield cards sit in an 8px gap and `--card-base` floors at
3.5rem, so growing outward puts ~26px over the NEIGHBOUR's face at `z-40` and
steals its clicks, while growing inward swallows most of a 56px card. Either
would re-create the click theft this commit undoes. Recorded at the gate as a
known limitation needing a layout-level answer, alongside the same gap on `+N`.

Also resets `viewerInteraction` in the test suite's shared setup — without it
the rows that publish a fan leak it into every later row, and the suite only
passed because they were declared last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JacobWoodson pushed a commit to JacobWoodson/phase that referenced this pull request Aug 15, 2026
… route (phase-rs#7296)

* fix(client): give a host's own click back to the host, and keep a fan route

A creature's activated ability was unreachable whenever an attached Aura or
Equipment was activatable too. Reported for Slumbering Keepguard (`{2}{W}`);
reproduces with Cooped Up, whose `{2}{W}: Exile enchanted creature` is
legitimately activatable from the battlefield, so no engine defect is needed.

During Priority `HumanResponseModel::ExactCandidates` publishes an attachment
fan for every activatable attachment, so `viewerInteraction.attachmentFans`
carries an entry for the host. The `attachmentsActionable` branch read that as
"the host is not a legal choice" — an unchecked premise — and sat above the
activation branch, so every click on the host opened the chooser instead. The
chooser excludes the host by design (`id !== host.id`), so the host's ability
had no path at all. The sibling branch that reads the affordance sets is placed
last for exactly this reason and documents it; this one was not.

Moved below the host's own target / activation / undo intent, which also makes
the click ladder match the glow-ring priority ladder it advertises. Introduced
by phase-rs#6945 (a card PR), which is why the click order was never reviewed as such.

Handing the click back to the host strands the attachments unless they keep a
route of their own, and they had none in this state: `attachmentsActionable` is
itself one of the disjuncts that expands the attachment stack, so no `+N`
control renders, while the `⧉` control required exactly one attachment. Each
Aura was then reachable only through a ~22px peek behind the host face. `⧉` now
renders whenever attachments are present and the stack is expanded — the same
predicate `+N` is derived from, so the two are complementary and exactly one
route exists in every state that needs one.

The `⧉` label counts what the fan will actually show
(`interactionAttachmentFan?.children.length`), not `obj.attachments.length`:
while an interaction is live the fan builds its cards from the projection, so
counting the raw snapshot would promise more cards than appear, and would
rediscover from the snapshot what the note above that line forbids.

`permanent.viewAttachmentsFor` now interpolates a real count, which exposed a
missing CLDR plural category: Polish selects `few`/`many` at 2/3/5 and neither
key existed, so `pl` fell back to English. Added `_few`/`_many` in all seven
catalogs (never selected outside `pl`, required by the `en`-oracle parity gate).
`permanent.hiddenAttachmentsAria` carried the identical defect and is fixed in
the same pass rather than left as the odd one out of a two-member class.

The control's SIZE is deliberately unchanged. It is now the pointer route where
the host's own click used to open the fan, and at `clamp(20px, …, 28px)` it is
under the 44px touch floor the branch above cites — but 44px is unreachable at
this seam: battlefield cards sit in an 8px gap and `--card-base` floors at
3.5rem, so growing outward puts ~26px over the NEIGHBOUR's face at `z-40` and
steals its clicks, while growing inward swallows most of a 56px card. Either
would re-create the click theft this commit undoes. Recorded at the gate as a
known limitation needing a layout-level answer, alongside the same gap on `+N`.

Also resets `viewerInteraction` in the test suite's shared setup — without it
the rows that publish a fan leak it into every later row, and the suite only
passed because they were declared last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(engine): publish attachment-fan membership from the interaction projection

The fan's membership was decided in `client/src`: it walked `GameObject
.attachments`, merged `viewerInteraction.attachmentFans` from every descendant
and produced the list it rendered. That is a second interaction authority, and
it read an offer list as an inventory list — `attachment_fans_for_object_choices`
publishes per DIRECT host and only for a child with exactly one legal choice, so
one attachment's Equip erased its siblings from the only surface that shows
attached cards at all.

`ViewerInteraction` now carries `attachment_views`: for every object the viewer
can see, the whole attachment subtree in depth-first order, each card holding the
engine's opaque submission when one was published for it — under any host in that
subtree, nested fans included — and `None` otherwise. Both directions of every
relationship must agree, the same guard the fans already applied, so stale or
authority-only back-links cannot reach a consumer.

It is a second field rather than a widening of `attachment_fans` because the two
answer different questions. A fan is what this viewer may submit right now, so it
is authorization-scoped and empty for everyone else — the existing contract pins
that. Membership is a board fact (CR 301.5 / CR 303.4: an attached permanent is
its own object on the battlefield), so it follows visibility instead and is
published on every projection, including an opponent's turn, an open prompt and a
terminal game. Folding one into the other would either delete that contract
assertion or make attachments vanish from the display whenever the viewer cannot
act.

The client renders and counts the projection and nothing else: `AttachmentFan`
maps `cards` and gates each pick on that card's submission (else the shared
`deriveActivationAffordances` authority the battlefield ring uses), and the badge
label counts the same list, so label and fan can no longer disagree. A published
pick is offered only while `canSubmit`, which the click path already required.

Engine contract rows: the nested route submits through production dispatch from
the outer host's view, membership survives an unauthorized viewer while the fan
does not, a stale link in either direction publishes nothing, and the projection
crosses the adapter round trip with `submission: null` intact and a missing field
still loading. Counter-measured: direct children only -> `left: [2, 4] right:
[2, 3, 4]`; dropping the back-link check -> the stale-link row; published-only
membership in the client -> 11 red rows; dropping `canSubmit` -> 1.

Wire protocol 21 -> 22: the field parses on a v21 peer as an empty map, so the
loss is silent and the handshake is the only place the pairing can be refused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(engine): bound every viewer projection through one exit

Membership is derived before the authorization, session and slot gates, so
the terminal, unauthorized, unbound-authority, invalid-serial and
oversized-slot returns each carried an attachment map that only the derived
path ever charged against the outbound budget. An early return could
therefore serialize a visible attachment tree of any size.

`derive_viewer_interaction` now leaves through a single
`finalize_viewer_interaction`, so one aggregate budget governs every
outward projection and failure drops the unbounded lists with
PayloadTooLarge instead of shipping a truncated payload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(engine): let attachment-view overflow reach the budget gate

The membership derivation absorbed its own overflow: a host whose subtree
passed MAX_INTERACTION_LIST_LEN was skipped, and an over-limit host map was
replaced by an empty one. Both left the finalizer a small, plausible
projection with nothing to object to, so the viewer read a bounded empty map
as an authoritative "nothing is attached".

`attachment_views_for_viewer` now returns Result and the reason code travels
to `finalize_viewer_interaction`, which installs the membership and owns the
single fail-closed answer for both derivation and aggregate overflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(engine): bound the attachment view while it is derived

`attachment_views_for_viewer` built each host's whole descendant tree and
only then asked whether it fit, and walked those descendants again for
every ancestor above them. The aggregate bound in `bound_outbound_view`
did reject the finished payload, so the ANSWER was already fail-closed —
but it was reached only after the construction work was done.

A chain 10 000 links long is the worst case: it is the longest chain in
which no single view exceeds the per-view cap, so the per-host check never
fires and the derivation runs to completion. Measured on the new fixture,
that costs 23.2 s and peaks at 7.4 GB resident for a payload of 49 995 000
card entries. CR 732.2's runaway-cascade guard permits one dispatch to
grow the board by 16 000 objects, so this is a reachable board rather than
an invalid fixture, and the engine ships to WASM, where 7.4 GB is
linear-memory exhaustion rather than a slow frame.

The walk is now iterative — its depth is the attachment chain's depth,
which is a game-controlled quantity and does not belong on the call stack
— and it carries the running aggregate, stopping one card past the budget.
The same fixture now costs 0.16 s. The running total is charged by
addition rather than against a remaining allowance, so the bound cannot
underflow if the accounting slips.

Every card charged here is charged again by `bound_outbound_view`
alongside the map, the fans and the opportunities, so the derivation's
total is a lower bound on the finalizer's: it can never refuse a payload
the finalizer would have accepted, and the finalizer stays the single
authority on what the viewer is told.

Regressions: `a_cap_depth_attachment_chain_is_refused_before_it_is_built`
(the 10 000-link worst case, read through the early return, which reaches
the same derivation without the 109 s of action enumeration the derived
path owes over that board) and the existing chain row, deepened from 200
to 1 000 links so it exercises the derived path against a payload of
499 500 cards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(engine): prove the membership derivation refuses before it builds

The public rows in `interaction_contract` cannot carry this claim. The
aggregate bound in `bound_outbound_view` already refused this payload, so
the ANSWER is `Unsupported { PayloadTooLarge }` with or without the
incremental charge; what the change moves is when the refusal happens.

`the_membership_derivation_refuses_a_worst_case_chain_before_building_it`
asserts on `attachment_views_for_viewer` itself. A chain of exactly
`MAX_INTERACTION_LIST_LEN` links is the only depth that discriminates: it
is the longest one in which no host's own subtree exceeds the per-view
cap, so a derivation that measures after materializing runs to completion.
One link longer and the outermost view trips that cap by itself.

Two reach guards run first — the fixture is exactly
`MAX_INTERACTION_LIST_LEN + 1` objects and no host carries more than one
attachment — so an over-long or fanned-out fixture cannot fail the row for
the wrong reason.

Counter-measured: with the running charge dropped and the post-walk
`cards.len()` check restored, it fails on `Ok`, holding 9 999 views and
49 995 000 cards. The assertion compares by discriminant and reports those
two SIZES rather than the value, because printing the rejected `Ok`
produced 3.2 GB of output for one failed assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(PR-7296): cover projected attachment badge count

Co-authored-by: cuinhellcat <217210902+cuinhellcat@users.noreply.github.com>

* test(PR-7296): correct singular attachment label assertion

Co-authored-by: cuinhellcat <217210902+cuinhellcat@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants