Skip to content

fix(engine): add player-counter Ward cost for "Get five poison counters" - #6662

Open
hurryup52 wants to merge 17 commits into
phase-rs:mainfrom
hurryup52:fix/6640-serpent-society-ward-poison-cost
Open

fix(engine): add player-counter Ward cost for "Get five poison counters"#6662
hurryup52 wants to merge 17 commits into
phase-rs:mainfrom
hurryup52:fix/6640-serpent-society-ward-poison-cost

Conversation

@hurryup52

@hurryup52 hurryup52 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

The Serpent Society's Ward reads "Ward—Get five poison counters." No WardCost variant existed for "give yourself N counters," so the parser fell through to mana-cost parsing, found no mana symbols, and silently produced WardCost::Mana(generic: 0) — a free, always-paid Ward that never gave the targeting opponent any poison counters. Adds WardCost::GetPlayerCounters { counter_kind, count } / AbilityCost::GetPlayerCounters and wires it through the existing Ward/unless-pay machinery — no new interactive round-trip needed.

Files changed

  • crates/engine/src/types/keywords.rs — new WardCost::GetPlayerCounters variant
  • crates/engine/src/types/ability.rs — matching AbilityCost::GetPlayerCounters variant + 4 helper-method arms
  • crates/engine/src/game/triggers.rsward_cost_to_ability_cost arm + test case
  • crates/engine/src/game/costs.rssupported_at_resolution, can_pay_resolution, pay_ability_cost_inner arms
  • crates/engine/src/game/engine_payment_choices.rshandle_unless_payment arm
  • crates/engine/src/parser/oracle_keyword.rs — new nom-combinator parser branch + 3 unit tests
  • crates/engine/src/analysis/ability_graph.rs, crates/engine/src/game/ability_scan.rs, crates/engine/src/game/cost_payability.rs, crates/engine/src/game/printed_cards.rs, crates/engine/src/game/replacement.rs, crates/engine/src/parser/oracle_effect/lower.rs — arms for the six other exhaustive AbilityCost matches in the crate (found via the compiler's own exhaustiveness checking, not manual audit)
  • client/src/pages/GamePage.tsx + client/src/i18n/locales/en/game.json — Ward-payment prompt label (would otherwise show a generic "Pay a cost" fallback for this new cost shape)
  • crates/engine/tests/integration/serpent_society_ward_poison_cost.rs (new) + crates/engine/tests/integration/main.rs — integration tests

Track

Non-developer

LLM

Model: claude-sonnet-5
Tier: Frontier
Thinking: medium

Implementation method (required)

Method: not-applicable — implemented directly. Traced the full Ward pipeline (parser → ward_cost_to_ability_cost → the resolution-time AbilityCost payment authority → the UnlessPayment/PayUnlessCost round trip) against the existing "enchanted player is attacked"-style precedents before writing any code, per the add-keyword/card-test skills, rather than spawning the /engine-implementer pipeline.

CR references

  • CR 702.21a — Ward definition.
  • CR 122.1 — general counter rules (the GetPlayerCounters cost/effect shape).
  • CR 104.3d — ten-or-more-poison loss condition (an existing, unmodified SBA; not exercised by the cost itself, which has no affordability gate).

All three verified against docs/MagicCompRules.txt directly before writing (104.3d at line 346, 702.21a at line 4114).

Verification

  • cargo fmt --all — clean.
  • cargo check -p engine --lib — clean (Finished dev profile ... in ~37s on the final pass), confirming every production-code match arm (including the 6 additional exhaustive AbilityCost sites found only by the compiler) type-checks.
  • cargo test -p engine / cargo clippycould not run to completion in this environment, same limitation already disclosed on PR fix(engine): parse "one or more of your opponents are attacked" triggers #6658 this session: Tilt isn't running here, and every direct cargo test/cargo check --tests invocation for this crate gets OOM-killed (17GB RAM, no swap) regardless of flags — this is a cold, non-incremental, non-Tilt build of a very large crate, not something local retries fix. The new parser unit tests (oracle_keyword.rs) and integration tests (serpent_society_ward_poison_cost.rs) were verified by hand against the exact nom-combinator functions and scenario/runner API calls they use (confirmed each helper's real signature by reading the source, and modeled the integration test line-for-line on the existing phyrexian_fleshgorger_ward.rs), but this is not a substitute for an actual compile+run. Flagging explicitly rather than claiming green — real CI will run the full suite.
  • Gate A / Final review-impl — not run (no /engine-implementer pipeline invocation).

Anchored on

  • crates/engine/src/parser/oracle_keyword.rs (parse_ward_cost_single's existing "pay N life"/"sacrifice ..." branches) — the direct structural precedent for the new "get N counter(s)" branch, same nom-combinator idiom.
  • crates/engine/src/game/costs.rs (EffectCost/Effect::PutCounter arm using effects::counters::add_counter_with_replacement) — the precedent for routing GetPlayerCounters's payment through effects::player_counter::add_player_counter_with_replacement rather than a raw state mutation, so replacement effects still apply.
  • crates/engine/tests/integration/phyrexian_fleshgorger_ward.rs — the exact GameScenario/GameRunner Ward test pattern (add_creature_from_oracle + add_spell_to_hand_from_oraclecast(..).target_objects(..).commit()advance_until_stack_empty() → assert WaitingFor::UnlessPaymentact(PayUnlessCost{pay})) the new test file mirrors.

Claimed parse impact

The Serpent Society: WardCost::Mana(generic: 0)WardCost::GetPlayerCounters { counter_kind: Poison, count: 5 }.

Scope Expansion

None — WardCost::GetPlayerCounters is parameterized over PlayerCounterKind (not poison-only) per this repo's "parameterize, don't proliferate" principle, since the shape is identical for any player-counter kind, but no other card's parse output is claimed as changed by this PR.

Validation Failures

None known — see the Verification section for the one open item (local test execution blocked by sandbox memory, not a known validation failure).

CI Failures

None yet observed (PR not yet run through CI at the time of writing).

Summary by CodeRabbit

  • New Features
    • Added ward/cost support for “Get N counter(s)” (poison, experience, rad, ticket), with correct singular/plural UI text and localized counter-kind labels across supported languages.
  • Bug Fixes
    • Fixed “unless payment” behavior for these ward costs so paying grants the intended counters, declining leaves state consistent, and resolution/parsing correctly handle the poison threshold and related game-over outcomes.
  • Tests
    • Added integration coverage for “Ward—Get five poison counters” (The Serpent Society), along with parsing/mapping updates for the new counter-cost format.

@hurryup52
hurryup52 requested a review from matthewevans as a code owner July 26, 2026 10:51
@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The engine adds GetPlayerCounters Ward and ability costs, parses counter-based Oracle text, supports payment and replacement handling, updates cost classification and display formatting, extends AI policy handling, and adds parser and integration coverage for The Serpent Society’s poison-counter Ward.

Changes

Player-counter Ward costs

Layer / File(s) Summary
Counter-cost contracts and parsing
crates/engine/src/types/..., crates/engine/src/parser/..., crates/mtgish-import/src/convert/action.rs
Adds counter-based Ward and ability cost variants, parses numeric and article-based counter costs, and excludes the cost from where-X binding and rewriting.
Ward conversion and payment
crates/engine/src/game/triggers.rs, crates/engine/src/game/costs.rs, crates/engine/src/game/engine_payment_choices.rs, crates/engine/src/game/cost_payability.rs, crates/engine/src/game/replacement.rs
Converts Ward costs, pays counters through replacement handling, and preserves paid, failed, or paused unless-payment outcomes.
Engine classification and display
crates/engine/src/analysis/..., crates/engine/src/game/..., client/src/pages/GamePage.tsx, client/src/i18n/locales/*/game.json
Classifies the new cost as having no modeled resource reads or axes and renders localized singular/plural counter descriptions.
AI Ward-cost handling
crates/phase-ai/src/policies/..., crates/phase-ai/src/tactical_gate.rs
Treats player-counter Ward costs as payable and scores poison severity by counter count, including lethal poison in compound costs.
Ward payment integration coverage
crates/engine/tests/integration/...
Tests the poison-counter Ward prompt, declining payment, successful payment, and poison-ten game-over behavior while resolving a targeted destroy spell.

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

Sequence Diagram(s)

sequenceDiagram
  participant OracleParser
  participant WardTrigger
  participant PaymentEngine
  participant PlayerCounters
  participant GameState
  OracleParser->>WardTrigger: parse Get five poison counters
  WardTrigger->>PaymentEngine: create unless payment
  PaymentEngine->>PlayerCounters: add five poison counters
  PlayerCounters->>GameState: apply replacement and poison state
  GameState-->>PaymentEngine: Paid, Failed, Paused, or GameOver
Loading

Suggested labels: enhancement

Suggested reviewers: matthewevans

🚥 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 matches the main change: adding a player-counter Ward cost for the "Get five poison counters" ability.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

🤖 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 `@client/src/pages/GamePage.tsx`:
- Around line 3349-3355: Keep engine counter values opaque in the frontend:
remove any lowercasing or fallback substitution for counter_kind and count, and
pass the engine-provided values unchanged into display rendering. Update the
GetPlayerCounters payload type to a discriminated union with required fields per
counter variant instead of optional ad hoc properties, and use raw engine enum
strings when interpolating translated templates.

In `@crates/engine/src/analysis/ability_graph.rs`:
- Around line 1572-1577: The AbilityCost::GetPlayerCounters arm currently
discards counter_kind and count, so update the loop-analysis handling to
classify supported counter kinds explicitly and preserve their semantic axis and
quantity. Keep poison counters excluded only where justified, and handle
unsupported kinds conservatively rather than treating every variant as a no-op;
extend the relevant parser/loop tests to cover Experience and unsupported kinds.

In `@crates/engine/src/parser/oracle_keyword.rs`:
- Around line 753-786: Update the Ward-cost parsing branch before the mana
fallback to return None for counter-shaped “get … counter” or “get … counters”
text whenever the count or counter kind cannot be parsed. Preserve the existing
GetPlayerCounters result for valid inputs, and add negative tests covering
unknown counter kinds and malformed counts so unsupported text never becomes a
free Mana Ward.

In `@crates/engine/tests/integration/serpent_society_ward_poison_cost.rs`:
- Around line 115-156: The existing Serpent Society Ward test only covers poison
increasing from zero to five. Add a paid-Ward scenario using the same setup and
payment flow, initialize P1 with five poison counters, and assert that reaching
ten causes state-based elimination: P1 is no longer an active priority holder
and the targeted destroy spell does not continue resolving after the loss.
🪄 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: 698b3451-a1d0-4198-860d-e6fa5c1e1da8

📥 Commits

Reviewing files that changed from the base of the PR and between eae0582 and 0e2e2f5.

📒 Files selected for processing (16)
  • client/src/i18n/locales/en/game.json
  • client/src/pages/GamePage.tsx
  • crates/engine/src/analysis/ability_graph.rs
  • crates/engine/src/game/ability_scan.rs
  • crates/engine/src/game/cost_payability.rs
  • crates/engine/src/game/costs.rs
  • crates/engine/src/game/engine_payment_choices.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/replacement.rs
  • crates/engine/src/game/triggers.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_keyword.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/keywords.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/serpent_society_ward_poison_cost.rs

Comment thread client/src/pages/GamePage.tsx Outdated
Comment on lines +1572 to 1577
// CR 702.21a: a Ward player-counter cost, like the effect-side
// `Effect::GivePlayerCounter` above, carries no modeled axis here —
// poison accumulation is a loss condition, not a combo resource this
// loop detector tracks.
| AbilityCost::GetPlayerCounters { .. }
| AbilityCost::Unimplemented { .. } => {}

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 | 🏗️ Heavy lift

Preserve counter_kind in loop analysis.

GetPlayerCounters is parameterized, but this arm discards both counter_kind and count. The supplied parser tests create the same variant for Experience, while the comment only justifies ignoring poison counters; this can produce incorrect net-axis and loop conclusions for other player-counter kinds. Classify supported kinds explicitly, or make unsupported kinds conservatively handled and tested.

As per path instructions, parameterized variants must retain their semantic axis; the parser coverage demonstrates this is not a poison-only variant.

🤖 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/analysis/ability_graph.rs` around lines 1572 - 1577, The
AbilityCost::GetPlayerCounters arm currently discards counter_kind and count, so
update the loop-analysis handling to classify supported counter kinds explicitly
and preserve their semantic axis and quantity. Keep poison counters excluded
only where justified, and handle unsupported kinds conservatively rather than
treating every variant as a no-op; extend the relevant parser/loop tests to
cover Experience and unsupported kinds.

Source: Path instructions

Comment thread crates/engine/src/parser/oracle_keyword.rs
Comment thread crates/engine/tests/integration/serpent_society_ward_poison_cost.rs
@matthewevans matthewevans self-assigned this Jul 26, 2026

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

🔴 High — the new counter cost is not fully threaded through the workspace

Reviewed head 0e2e2f5eea6ba3cf11fad16716c11b984f77a348. The submitted claude-sonnet-5 / Frontier declaration is accepted by current policy; it is not a review finding. The implementation still has these blockers:

  1. Rust lint is terminal red with E0004: WardCost::GetPlayerCounters is missing from both severity matches in crates/phase-ai/src/policies/anti_self_harm.rs:784-811 and from strategy_helpers.rs:300-343; AbilityCost::GetPlayerCounters is missing from crates/mtgish-import/src/convert/action.rs:346 and the exhaustive representative test in crates/engine/src/game/costs.rs:1985. Thread the variant through every workspace match without a wildcard escape hatch. In particular, define and test intentional AI semantics for a poison Ward payment that reaches ten: the AI must not treat a lethal self-poison payment as an ordinary affordable/low-severity ward cost. Make compound and non-poison player-counter behavior explicit as well.

  2. crates/engine/src/parser/oracle_keyword.rs:758-786 recognizes get … counter(s) but falls through to mana parsing when its counter shape is malformed or unknown. That can silently lower unsupported text to WardCost::Mana(0). Counter-shaped get Ward text must strict-fail before the mana fallback when either count or kind is unsupported, with negative tests proving malformed counts and unknown counter kinds do not regain free Ward reachability.

  3. Frontend CI fails locale parity: the new English keys game.gamePage.cost.playerCounters_{one,other} are absent from de, es, fr, it, pl, and pt. Add corresponding locale entries.

  4. serpent_society_ward_poison_cost.rs covers only 0→5 poison. Add a production payment-path case beginning P1 at five poison; assert the ten-poison state-based loss removes P1 from active priority and prevents the targeted destroy spell from continuing.

  5. client/src/pages/GamePage.tsx:3349-3393 supplies defaults and lowercases count/counter_kind, reinterpreting engine data in the display layer. Make the counter-cost adapter payload a typed discriminated variant with required fields, and render engine-provided values unchanged (or expose a display-ready engine label); remove frontend fallback/lowercasing behavior.

Required CI is currently red (Rust lint/test shards/WASM/consolidated Rust and frontend). Repair all items, bring the branch current as needed, and request a new review on the resulting head; do not enqueue before required checks pass.

@matthewevans matthewevans added the bug Bug fix label Jul 26, 2026
@matthewevans matthewevans removed their assignment Jul 26, 2026
@github-actions

github-actions Bot commented Jul 26, 2026

Copy link
Copy Markdown

Generated for head 78ae37012f4959a9b7fc3abfb966d6b47639addc.

Parse changes introduced by this PR

✓ No card-parse changes detected.

hurryup52 added a commit to hurryup52/phase that referenced this pull request Jul 26, 2026
CI on phase-rs#6662 caught two gaps my local `cargo check -p engine --lib` never
touched:

- crates/phase-ai has its own exhaustive matches directly on WardCost
  (not AbilityCost) for AI tactical scoring: the Ward-cost "severity"
  pricing in anti_self_harm.rs (both the top-level match and its
  duplicated inline copy inside the Compound arm) and can_pay_ward_cost's
  affordability check in strategy_helpers.rs. Add GetPlayerCounters arms
  to all three: severity scales uncapped with count (voluntarily taking
  poison counters is a real, severe cost, unlike the capped mana/life
  costs), and affordability is unconditionally true (no resource limit
  on giving yourself more counters, mirroring the engine's own
  can_pay_resolution).

- The i18n "locale key parity" test requires es/fr/de/it/pt/pl to have
  the exact same key set as en, not just en as I'd assumed. Add
  cost.playerCounters_one/_other and a cost.playerCounterKind noun map
  to all six locales with real translations (not English placeholders),
  matching the badges section's existing poison/experience/rad counter
  naming conventions. Also redesigned formatUnlessCost's GetPlayerCounters
  case to look the counter-kind noun up through i18n rather than
  interpolating the raw English enum name, so non-English locales render
  correctly.

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/phase-ai/src/policies/anti_self_harm.rs (1)

775-797: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Apply Ward-cost scoring to noncreature permanents.

This loop is nested inside the creature-only branch beginning at Line 722, so the new Ward-cost handling is unreachable for artifacts, enchantments, and planeswalkers with Ward. The AI can therefore target a noncreature permanent with Ward—Get ... counters without accounting for the payment cost. Move Ward valuation outside the creature-specific block or share it through a common helper.

🤖 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/phase-ai/src/policies/anti_self_harm.rs` around lines 775 - 797, Move
the Ward-cost valuation loop out of the creature-only branch surrounding it, or
extract it into a shared helper used by all permanent types. Ensure artifacts,
enchantments, and planeswalkers with Ward invoke can_pay_ward_cost and receive
the same severity scoring, including GetPlayerCounters, while preserving the
existing creature scoring behavior.

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/phase-ai/src/policies/anti_self_harm.rs`:
- Around line 793-797: Update the WardCost::GetPlayerCounters scoring in both
the direct branch around the shown match and the compound-cost path near the
referenced secondary location. Score harmful counters such as poison and rad
using their counter kinds and current player totals where relevant, while
assigning appropriate lower or non-harmful values to beneficial/resource
counters such as experience and tickets; do not continue applying count * 3.0
indiscriminately.

---

Outside diff comments:
In `@crates/phase-ai/src/policies/anti_self_harm.rs`:
- Around line 775-797: Move the Ward-cost valuation loop out of the
creature-only branch surrounding it, or extract it into a shared helper used by
all permanent types. Ensure artifacts, enchantments, and planeswalkers with Ward
invoke can_pay_ward_cost and receive the same severity scoring, including
GetPlayerCounters, while preserving the existing creature scoring behavior.
🪄 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: 543a5e2b-2bf4-4fec-ae3a-1720a50b33d9

📥 Commits

Reviewing files that changed from the base of the PR and between 0e2e2f5 and fe70e11.

📒 Files selected for processing (10)
  • client/src/i18n/locales/de/game.json
  • client/src/i18n/locales/en/game.json
  • client/src/i18n/locales/es/game.json
  • client/src/i18n/locales/fr/game.json
  • client/src/i18n/locales/it/game.json
  • client/src/i18n/locales/pl/game.json
  • client/src/i18n/locales/pt/game.json
  • client/src/pages/GamePage.tsx
  • crates/phase-ai/src/policies/anti_self_harm.rs
  • crates/phase-ai/src/policies/strategy_helpers.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/src/pages/GamePage.tsx

Comment thread crates/phase-ai/src/policies/anti_self_harm.rs Outdated
@matthewevans matthewevans self-assigned this Jul 26, 2026

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

🔴 Blocker — counter Ward is still incomplete on the fresh head

Reviewed commit fe70e11bc3a14b18f1c184c0b9828a4738571c64.

  1. The parse-diff artifact reports no card-parse changes, which conflicts with the PR's claimed The Serpent Society Mana(0)GetPlayerCounters change. Please explain that discrepancy and add actual-card/pipeline evidence that the card parses from card data to the typed Ward cost and reaches the paid-Ward flow; a parser fixture alone is not sufficient.

  2. crates/engine/src/parser/oracle_keyword.rs:758-786 recognizes get N <known> counters but lets malformed or unknown counter-shaped get ... counters text fall through to WardCost::Mana(0). Make this grammar failure explicit (or otherwise prevent the zero-mana fallback) and cover negative reach cases.

  3. The workspace is not exhaustively updated: crates/mtgish-import/src/convert/action.rs and crates/mtgish-import/src/convert/costs.rs omit AbilityCost::GetPlayerCounters; crates/phase-ai/src/ability_graph.rs also discards its kind/count semantics. Update these sites according to their domain contracts rather than treating the new cost as inert.

  4. crates/phase-ai/src/policies/anti_self_harm.rs assigns every player-counter cost count * 3, and crates/phase-ai/src/strategy_helpers.rs calls every such cost payable. Counter kinds are not interchangeable: lethal poison at the current total plus the requested count must be rejected, with explicit behavior for the supported resource kinds and compound costs.

  5. The Serpent Society test covers only poison 0 → 5. Add a discriminating continuation/state-based-action case (for example 5 → 10) that demonstrates the cost is not merely parsed but changes the game outcome correctly.

Please also resolve the corresponding review threads after addressing these points.

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

🔴 Blocker — corrected current-head review for counter Ward

Reviewed commit fe70e11bc3a14b18f1c184c0b9828a4738571c64. This supersedes the inaccurate path claims in my prior review; the following current-head blockers remain:

  1. The parse-diff sticky reports no card-parse changes, contradicting the claimed The Serpent Society Mana(0)GetPlayerCounters conversion. Explain the discrepancy and add actual-card/pipeline proof that card data parses the card to the typed Ward cost and reaches the paid-Ward path. A parser fixture alone does not establish that claim.

  2. crates/engine/src/parser/oracle_keyword.rs:758-786 still lets malformed or unknown counter-shaped get ... counter(s) Ward text drop through to WardCost::Mana(0). Fail closed before the mana fallback and add negative reach tests for malformed counts and unknown counter kinds.

  3. The new AbilityCost::GetPlayerCounters is missing from the rewrite_bound_x_in_ability_cost match in crates/mtgish-import/src/convert/action.rs:346, producing the Rust exhaustive-match failure. Its actual exhaustive test/mirror is crates/engine/src/game/costs.rs:1985-2099, where both the sample_for match and all_variants tags omit the new variant. Thread the no-X semantics through the importer and keep the lockstep test exhaustive.

  4. client/src/pages/GamePage.tsx:3348-3400 silently substitutes a count and converts an unrecognized counter_kind to poison (while lowercasing known kinds). The display layer must not reinterpret an engine cost: make the payload typed with required fields and render engine-provided data unchanged, or provide an engine-owned display value.

  5. crates/phase-ai/src/policies/anti_self_harm.rs:722,775-821 scores Ward only within the creature branch, while crates/phase-ai/src/tactical_gate.rs:308-315 evaluates Ward for every targetable permanent. Give all Ward targets one full typed-cost evaluation: lethal poison must be rejected, supported non-poison kinds must have explicit semantics, compound costs must compose those semantics, and noncreature Ward must receive the same treatment.

  6. serpent_society_ward_poison_cost.rs only tests poison 0 → 5; that does not discriminate the lethal boundary. Add a real payment-path 5 → 10 continuation/SBA test proving the player is eliminated and the targeted spell does not continue.

The required Rust aggregate is still failed in the current recommendation packet. Please address these blockers, resolve the related review threads, and request review on a new head.

@matthewevans matthewevans removed their assignment Jul 26, 2026
hurryup52 added a commit to hurryup52/phase that referenced this pull request Jul 26, 2026
…frontend type

Addresses matthewevans's review on phase-rs#6662:

- Third crate with an exhaustive AbilityCost match my earlier per-crate
  checks missed: crates/mtgish-import/src/convert/action.rs's
  rewrite_bound_x_in_ability_cost (no-op arm — count is a fixed u32, not
  an X-bindable QuantityExpr).
- crates/engine/src/game/costs.rs's exhaustive "lockstep gate" test
  (sample_for + all_variants) only exercised under --tests, which this
  sandbox can't compile, so cargo check --lib alone never caught it.
  Added a real arm so GetPlayerCounters is actually exercised by
  every_ability_cost_variant_has_resolution_support_answer, not just
  made to compile.

- AI severity scoring (anti_self_harm.rs) is now lethality- and
  kind-aware instead of a flat count*3.0: a poison payment that would
  push the AI to LETHAL_POISON (10, per features::poison) scores
  prohibitively (100.0, matching this file's own "never do this"
  sentinel) rather than an ordinary linear severity; non-poison player
  counters (experience/rad/ticket) carry no loss-condition risk and
  score 0. Fixed the Compound arm's sum-then-min(4.0) cap, which would
  otherwise silently clamp a lethal poison sub-cost back down to an
  ordinary-looking severity.

- Parser (oracle_keyword.rs): counter-shaped "get ... counter(s)" text
  that fails to parse (unparseable count, unknown counter kind) now
  fails closed (returns None) instead of falling through to the
  mana-cost fallback — the same silent-free-Ward bug class phase-rs#6640 was
  about, for different malformed input. Added negative tests for both
  failure shapes.

- Frontend (GamePage.tsx): formatUnlessCost's GetPlayerCounters case is
  now a real discriminated union member with required, exact-cased
  fields (count: number, counter_kind: "Poison"|"Experience"|"Rad"|
  "Ticket") — no more toLowerCase()/fallback-default reinterpretation of
  engine data in the display layer. Renamed the playerCounterKind i18n
  keys in all 7 locales to match the engine's exact PascalCase strings.

- New integration test: a Ward payment that pushes the payer to ten
  poison counters triggers the CR 104.3d loss SBA immediately (before
  the targeted destroy spell can resolve), mirroring sba.rs's own
  sba_poison_10_player_loses unit test shape.

Verified with cargo check --lib on all three affected crates (engine,
phase-ai, mtgish-import) — all clean. Full cargo test still blocked by
this sandbox's memory ceiling (disclosed previously on this PR); the
new/changed test code was hand-verified against the real APIs it calls.

@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 (1)
crates/phase-ai/src/policies/anti_self_harm.rs (1)

793-874: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the existing reaches_lethal helper instead of duplicating the threshold check.

Both the direct arm (Lines 811-813) and the Compound has_lethal_poison closure (Lines 834-838) hand-roll current.saturating_add(*count) >= LETHAL_POISON. crates/phase-ai/src/policies/poison.rs already exposes pub(crate) fn reaches_lethal(current_poison: u32, added: u32) -> bool doing exactly this. Reusing it here removes the duplication and the exact kind of drift risk that the earlier review comment on this file already had to flag (the direct vs. compound scoring paths falling out of sync).

♻️ Proposed refactor
-                            let current =
-                                ctx.state.players[ctx.ai_player.0 as usize].poison_counters;
-                            if current.saturating_add(*count)
-                                >= crate::features::poison::LETHAL_POISON
-                            {
+                            let current =
+                                ctx.state.players[ctx.ai_player.0 as usize].poison_counters;
+                            if crate::policies::poison::reaches_lethal(current, *count) {
                                 100.0
                             } else {
                                 *count as f64 * 3.0
                             }
-                            let has_lethal_poison = costs.iter().any(|c| {
-                                matches!(
-                                    c,
-                                    WardCost::GetPlayerCounters {
-                                        counter_kind:
-                                            engine::types::player::PlayerCounterKind::Poison,
-                                        count,
-                                    } if ctx.state.players[ctx.ai_player.0 as usize]
-                                        .poison_counters
-                                        .saturating_add(*count)
-                                        >= crate::features::poison::LETHAL_POISON
-                                )
-                            });
+                            let poison_counter = ctx.state.players[ctx.ai_player.0 as usize].poison_counters;
+                            let has_lethal_poison = costs.iter().any(|c| {
+                                matches!(
+                                    c,
+                                    WardCost::GetPlayerCounters {
+                                        counter_kind:
+                                            engine::types::player::PlayerCounterKind::Poison,
+                                        count,
+                                    } if crate::policies::poison::reaches_lethal(poison_counter, *count)
+                                )
+                            });

Please verify reaches_lethal's exact module path (crate::policies::poison) and its pub(crate) visibility resolves from anti_self_harm.rs.

As per path instructions, "any new helper that duplicates an existing building block... Prefer composing std primitives and reusing existing helpers over re-implementation."

🤖 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/phase-ai/src/policies/anti_self_harm.rs` around lines 793 - 874, In
the direct Poison GetPlayerCounters arm and the Compound arm’s has_lethal_poison
closure, replace the duplicated saturating threshold checks with
crate::policies::poison::reaches_lethal, passing the current poison total and
added count. Preserve the existing lethal severity behavior and verify the
pub(crate) helper is accessible from anti_self_harm.rs.

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.

Nitpick comments:
In `@crates/phase-ai/src/policies/anti_self_harm.rs`:
- Around line 793-874: In the direct Poison GetPlayerCounters arm and the
Compound arm’s has_lethal_poison closure, replace the duplicated saturating
threshold checks with crate::policies::poison::reaches_lethal, passing the
current poison total and added count. Preserve the existing lethal severity
behavior and verify the pub(crate) helper is accessible from anti_self_harm.rs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d6003ea1-89f8-4975-adf3-a754068fd09e

📥 Commits

Reviewing files that changed from the base of the PR and between fe70e11 and 42a538c.

📒 Files selected for processing (13)
  • client/src/i18n/locales/de/game.json
  • client/src/i18n/locales/en/game.json
  • client/src/i18n/locales/es/game.json
  • client/src/i18n/locales/fr/game.json
  • client/src/i18n/locales/it/game.json
  • client/src/i18n/locales/pl/game.json
  • client/src/i18n/locales/pt/game.json
  • client/src/pages/GamePage.tsx
  • crates/engine/src/game/costs.rs
  • crates/engine/src/parser/oracle_keyword.rs
  • crates/engine/tests/integration/serpent_society_ward_poison_cost.rs
  • crates/mtgish-import/src/convert/action.rs
  • crates/phase-ai/src/policies/anti_self_harm.rs
🚧 Files skipped from review as they are similar to previous changes (10)
  • client/src/i18n/locales/pt/game.json
  • client/src/i18n/locales/de/game.json
  • client/src/i18n/locales/en/game.json
  • client/src/i18n/locales/pl/game.json
  • client/src/i18n/locales/fr/game.json
  • client/src/i18n/locales/it/game.json
  • crates/engine/tests/integration/serpent_society_ward_poison_cost.rs
  • client/src/i18n/locales/es/game.json
  • crates/engine/src/parser/oracle_keyword.rs
  • crates/engine/src/game/costs.rs

@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: 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 `@client/src/pages/GamePage.tsx`:
- Around line 3376-3378: Update the counter-kind branch in the cost formatting
logic to pass cost.counter_kind unchanged as the kind interpolation value,
removing the gamePage.cost.playerCounterKind translation. Keep localization
limited to the surrounding gamePage.cost.playerCounters template.
🪄 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: 3f017326-411f-4ad4-b8ec-96ffa40a7fa4

📥 Commits

Reviewing files that changed from the base of the PR and between 42a538c and 4af0dd7.

📒 Files selected for processing (2)
  • client/src/pages/GamePage.tsx
  • crates/engine/tests/integration/serpent_society_ward_poison_cost.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/engine/tests/integration/serpent_society_ward_poison_cost.rs

Comment on lines +3376 to +3378
if ("counter_kind" in cost) {
const kind = t(`gamePage.cost.playerCounterKind.${cost.counter_kind}`);
return t("gamePage.cost.playerCounters", { count: cost.count, kind });

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.

⚠️ Potential issue | 🟠 Major

Keep engine-provided counter_kind unchanged.

This still transforms the engine enum into an i18n key and passes the translated noun as kind. Localize only the surrounding template; pass cost.counter_kind through unchanged or expose a display-ready label from the engine/adapter.

Suggested fix
-    const kind = t(`gamePage.cost.playerCounterKind.${cost.counter_kind}`);
-    return t("gamePage.cost.playerCounters", { count: cost.count, kind });
+    return t("gamePage.cost.playerCounters", {
+      count: cost.count,
+      kind: cost.counter_kind,
+    });
🤖 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 `@client/src/pages/GamePage.tsx` around lines 3376 - 3378, Update the
counter-kind branch in the cost formatting logic to pass cost.counter_kind
unchanged as the kind interpolation value, removing the
gamePage.cost.playerCounterKind translation. Keep localization limited to the
surrounding gamePage.cost.playerCounters template.

Sources: Path instructions, Learnings

@matthewevans matthewevans self-assigned this Jul 26, 2026

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

🔴 Blocker — current head still treats player-counter Ward as safe when it can lose the game

Reviewed commit 4af0dd76762a91769a782cadff20f4f7e609272b.

  1. HIGH — payability is still split from scoring. crates/phase-ai/src/policies/strategy_helpers.rs:337-346 returns true for every GetPlayerCounters, and crates/phase-ai/src/tactical_gate.rs:307-315 uses that result as a hard target-choice gate. anti_self_harm.rs:805-876 only changes the later score, so the AI can still select a Ward payment that takes poison from 5 to 10. Its compound logic also checks each poison component against the same starting total, so individually nonlethal components can be jointly lethal. Put typed player-counter payability in the shared authority, aggregate poison across Compound, and add target-choice tests proving both direct and compound lethal poison Ward are rejected.

  2. MED — Rad is incorrectly valued as harmless. anti_self_harm.rs:801-819 assigns non-poison player counters zero harm, but Rad is a typed admitted counter kind (oracle_nom/quantity.rs:5387-5393) whose engine resolution mills and can cause life loss (game/effects/rad_counters.rs:13-15,99-159). Give every supported kind explicit typed valuation and test Rad directly and inside Compound.

  3. MED — Ward counter parsing remains structurally outside the nom grammar. oracle_keyword.rs:758-788 now fails closed, but it recognizes its counter(s) tail with strip_suffix rather than composable nom productions. Refactor the count/article, kind, and singular/plural axes into nom combinators so this parser family has one grammatical authority rather than ad-hoc string dispatch.

  4. The parse-diff sticky predates this head and still reports zero card changes. Refresh/reconcile the artifact with the claimed The Serpent Society conversion and provide current-head actual-card pipeline evidence.

Lower priority but confirmed: client/src/pages/GamePage.tsx:3367-3378 still localizes/interprets the engine counter kind in the display layer; keep the transport/display contract explicit while fixing the blocking engine/AI behavior. I am not repeating the previously refuted ability-graph claim.

@matthewevans

Copy link
Copy Markdown
Member

Correction to review #4781781812: its statement that the parse-diff sticky predates the current head is no longer accurate. The refreshed parse-diff artifact was updated at 2026-07-26T13:07:37Z for head 4af0dd76762a91769a782cadff20f4f7e609272b and reports no card-parse changes.

This corrects only the artifact-freshness claim. Reconciliation and actual-card pipeline evidence remain required, and all other blockers in the current changes-requested review stand.

@matthewevans matthewevans removed their assignment Jul 26, 2026
@hurryup52

Copy link
Copy Markdown
Contributor Author

@matthewevans — pushed d5d0f44 addressing all three blockers plus the parse-diff evidence and the lower-priority note:

1. Payability split from scoring (HIGH) — fixed. total_poison_from_ward_cost (strategy_helpers.rs) now recursively sums poison across the whole Compound cost tree, and can_pay_ward_cost checks that aggregate once against the current total, instead of checking each sub-cost against the same unchanged starting total. Added rejects_targeting_ward_with_jointly_lethal_compound_poison in tactical_gate.rs (P0 at 4 poison, Compound([GetPlayerCounters{Poison,3}, GetPlayerCounters{Poison,3}])) — neither 3 alone is lethal from 4, but 4+3+3=10 is — asserting Reject. Also added rejects_targeting_noncreature_ward_that_would_be_lethal_poison since the hard gate is target-type-agnostic.

2. Rad valued as harmless (MED) — fixed. Rad now scores (count * 0.2).min(2.0) with a CR 728.1 rationale (mills + possible life loss), Experience/Ticket score 0.0 (no loss condition), Poison keeps count * 3.0 uncapped since lethal payments never reach this scoring at all now (rejected upstream). Tested directly and inside Compound.

3. Ward parsing outside the nom grammar (MED) — fixed. parse_get_player_counters_ward_cost in oracle_keyword.rs composes the count/article, kind, and singular/plural axes as nom combinators (tag, alt, value, space0, opt, all_consuming) instead of strip_suffix dispatch, and fails closed (None, no fallback to a free Mana Ward) on unparseable count or unknown kind — added ward_get_counters_with_unparseable_count_fails_closed and ward_get_counters_with_unknown_kind_fails_closed asserting exactly that.

4. Parse-diff reconciliation / actual-card pipeline evidence — ran oracle-gen directly against cached MTGJSON data (--filter "The Serpent Society") at current head d5d0f44fd:

{
  "name": "The Serpent Society",
  "keywords": [
    "Deathtouch",
    { "Ward": { "type": "GetPlayerCounters", "data": { "counter_kind": "Poison", "count": 5 } } }
  ]
}

Before this issue's fix (verified against upstream/main's parse_ward_cost_single): WardCost::GetPlayerCounters doesn't exist on main at all, so "Ward—Get five poison counters." fell through every recognized pattern (pay life / discard / sacrifice / waterbend) straight to the final parse_mtgjson_mana_cost fallback, producing WardCost::Mana(generic: 0) — a free Ward that was silently wrong. The sticky parse-diff artifact reports coverage-status (supported: true/false), not raw AST, and this was a "supported-aspect defect" (already marked supported: true pre-fix despite parsing wrong) — invisible to that diff by design. The oracle-gen output above is the direct pipeline proof the artifact can't show.

Lower priority — GamePage.tsx counter_kind display: fixed. Replaced t(`gamePage.cost.playerCounterKind.${cost.counter_kind}`) (interpolating the raw engine value into the i18n key template) with an exhaustive switch over cost.counter_kind mapping each literal to its key explicitly — a future PlayerCounterKind variant now fails to compile here instead of silently missing a translation at runtime.

Also fixed (from an earlier round, not repeated in your last review but still open): the Ward severity-pricing block in anti_self_harm.rs was nested inside the creature-only branch, so a Ward-bearing noncreature permanent (artifact/enchantment/planeswalker) was never priced by the AI's judgment layer at all — only the hard lethal-poison reject gate covered it. Moved the pricing block to run for every permanent type.

Verification this round: cargo check -p engine --lib, cargo check -p phase-ai --lib, cargo clippy -p engine --lib -- -D warnings, cargo clippy -p phase-ai --lib -- -D warnings, cargo fmt --all all clean; frontend tsc -b --noEmit clean and the full vitest run suite green (270 files / 2366 tests). As before, cargo test/cargo check --tests reliably OOMs in my sandbox on this workspace's size, so full-suite Rust proof relies on this PR's own CI shards.

@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: 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/phase-ai/src/policies/strategy_helpers.rs`:
- Around line 324-329: Update can_pay_ward_cost’s lethal-poison check to
calculate the effective poison counter increase using the same player-counter
replacement logic as add_player_counter_with_replacement, rather than
total_poison_from_ward_cost alone. Compare the post-replacement count against
LETHAL_POISON, and add a regression test covering a poison Ward cost modified by
a replacement effect.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0ff40a5f-e6d1-4e95-84a4-a352a6e55287

📥 Commits

Reviewing files that changed from the base of the PR and between 4af0dd7 and d5d0f44.

📒 Files selected for processing (5)
  • client/src/pages/GamePage.tsx
  • crates/engine/src/parser/oracle_keyword.rs
  • crates/phase-ai/src/policies/anti_self_harm.rs
  • crates/phase-ai/src/policies/strategy_helpers.rs
  • crates/phase-ai/src/tactical_gate.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • client/src/pages/GamePage.tsx
  • crates/engine/src/parser/oracle_keyword.rs

Comment thread crates/phase-ai/src/policies/strategy_helpers.rs Outdated
@matthewevans matthewevans self-assigned this Jul 26, 2026

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

Changes requested — the new noncreature Poison/Rad Ward behavior reaches target scoring without a production-seam regression.

🟡 Non-blocking

[MED] Missing AntiSelfHarmPolicy target-selection coverage for payable noncreature player-counter Ward costs. Evidence: crates/phase-ai/src/policies/anti_self_harm.rs:845-939 scores payable Ward costs on every permanent type, while the new noncreature Ward cases in crates/phase-ai/src/tactical_gate.rs:1137-1269 only assert GateDecision affordability/rejection. Why it matters: a change can preserve the tactical gate yet omit or mis-rank the Poison/Rad Ward penalty in the production target scorer. Suggested fix: add a target-selection regression through AntiSelfHarmPolicy using a noncreature permanent with a payable, nonlethal GetPlayerCounters Poison or Rad Ward and an unwarded equivalent; assert the Ward target receives the intended lower score/ranking.

Recommendation: request changes — add the production target-scoring regression, then request another review.

@matthewevans

Copy link
Copy Markdown
Member

Current-head review is clean at ccf2845fd074ac483e7d11ec508c9781c8a2e50c: required Rust and frontend checks are green, the parser parse-diff is current (and records no coverage-schema-visible change), and the remaining CodeRabbit threads are stale/false-positive concerns rather than implementation defects. In particular, static loop analysis deliberately leaves all player-counter costs unmodeled, and the frontend exhaustively maps the typed counter enum to locale-owned display text.

I cannot approve or enqueue this PR yet because open #6844 independently implements the same Serpent Society / #6640 Ward fix. The merge queue must not receive two PRs for one issue. #6662 contains the more complete replacement-aware cost-payment and replacement-choice continuation work, but the duplicate must be reconciled/closed before this PR can be approved and queued.

@matthewevans matthewevans removed the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Aug 6, 2026
@matthewevans matthewevans self-assigned this Aug 7, 2026
…-ccf2845

# Conflicts:
#	crates/engine/src/game/printed_cards.rs
@matthewevans

Copy link
Copy Markdown
Member

Maintainer port review is clean at 72550b2c417440b515750a0a909d399f0839ee3d; resolving the update conflict retained main's shared ability-visitor authority and made no Ward behavior change.

Hold: #6844 remains OPEN and independently covers the same #6640 / The Serpent Society Ward fix. Do not approve or enqueue either PR until reconciliation selects one canonical PR and closes or supersedes the other.

CI and the current-head parse-diff are in progress for this new head. Both must finish before reconsideration.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer update for current head 774a5dc6b1c12aee59478e9ae60733e309bfa8a5:

The safe compatibility fix adds PendingTrigger { provenance: None } to one phase-ai test fixture after the current-main API change. It does not change Ward behavior or the Ward payment path.

Replacement CI and the current-head parse-diff result must complete before this can move forward.

Hold remains: #6844 is still OPEN and independently covers the same #6640 / The Serpent Society Ward issue. Neither PR may be approved or enqueued until canonical reconciliation selects one PR and closes or supersedes the other.

@matthewevans

Copy link
Copy Markdown
Member

Maintainer update for current head 8d47671edb6f5e26740a73312e76838413871167:

The safe port fix updates only a stale stage2 census source coordinate after a +13 line shift. It makes no production or Ward behavior change.

Replacement CI and the current-head parse-diff result are pending.

Hold remains: #6844 is OPEN and independently covers the same #6640 / The Serpent Society Ward issue. Neither PR may be approved or enqueued until reconciliation selects one canonical PR and closes or supersedes the other.

# Conflicts:
#	crates/engine/src/game/engine.rs
@matthewevans

Copy link
Copy Markdown
Member

Maintainer reconciliation update — current head 78ae37012f4959a9b7fc3abfb966d6b47639addc

The stale-branch conflict has been maintainer-ported onto current main. The merge had one textual conflict, confined to the stage2 prompt-census test coordinate in crates/engine/src/game/engine.rs; the combined tree's producer is verified at line 11834 (main's -7 shift plus this branch's independent +13 shift). No Ward production behavior was changed by the reconciliation.

Fresh CI is now running on this head. Auto-merge and the merge queue remain disabled.

Reconciliation selects #6662 as the canonical candidate: it contains the later replacement-aware payment, prevention, continuation, parser, and AI corrections, while #6844 remains an older duplicate on 5bfca06e5c5162ba24f552e1b206b641c2688c05 with unresolved requested changes. The visible hold therefore remains in force: neither PR is approved or enqueued until #6662 receives fresh head-bound checks and review; #6844 should then be superseded rather than progressed in parallel.

@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 changes requested — parse evidence is stale

Reviewed 78ae37012f4959a9b7fc3abfb966d6b47639addc.

The sole <!-- coverage-parse-diff --> receipt is the existing sticky comment, and it explicitly identifies old head 8d47671edb6f5e26740a73312e76838413871167. This PR changes engine/parser behavior and now has a new merged head, so that receipt cannot support approval of the current commit.

Regenerate a current-head parse-diff receipt/artifact and reconcile its zero-card status with current-head actual-card pipeline evidence for The Serpent Society's Ward conversion. The evidence must be bound to this head, not inferred from the prior artifact or earlier local output.

CI is presently in progress on this head; completion of checks is required but is not a substitute for the current-head parse artifact. The earlier functional blockers are not being re-litigated in this review.

@matthewevans matthewevans removed their assignment Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants