Skip to content

ship/ai perf 6826 targeted exchange - #7049

Merged
matthewevans merged 3 commits into
mainfrom
ship/ai-perf-6826-targeted-exchange
Aug 6, 2026
Merged

ship/ai perf 6826 targeted exchange#7049
matthewevans merged 3 commits into
mainfrom
ship/ai-perf-6826-targeted-exchange

Conversation

@matthewevans

@matthewevans matthewevans commented Aug 5, 2026

Copy link
Copy Markdown
Member
  • perf: gate targeted-exchange preview behind a clone-free shape precondition
  • fix(ai): rail the targeted-exchange guard against synthesized activation abilities

Summary by CodeRabbit

  • Improvements
    • Improved AI decision-making for targeted exchanges, including alternate card faces, copied abilities, synthesized effects, and damage-all interactions.
    • AI now skips unnecessary exchange evaluation when an action cannot produce an adverse exchange, improving search efficiency.
    • Added safeguards to prevent uncertain or unsafe exchange previews from being treated as valid decisions.
  • Bug Fixes
    • Improved detection of effects that may create adverse exchanges across nested ability structures.
    • Increased reliability when evaluating layered effects and activation-related actions.

…dition

The AI decision-cost regression bisected to #6826: `root_action_is_allowed`
re-ran a full `validated_candidate_actions_for_semantic_owner` pass per action
over a list that was itself that enumeration's output, `replay_exact_candidate`
enumerated a second time, and the cheap shape guard ran after the clone, so it
saved nothing.

Add `root_may_yield_adverse_exchange`: a clone-free, allocation-free predicate
read from live `GameState` before any enumeration or clone. It is deliberately
over-approximating — `true` means "cannot prove non-rejection", and Step 5
wires it as an early-out, so it can only skip preview work and can never lose
a `Reject`. Its entry set is the union of four ability authorities
(`base_abilities`, `abilities`, `back_face`, `cleave_variant`) so a cast-time
face or text swap cannot install a shape the guard did not already walk
(CR 601.2a, CR 712.11b / 715.3a / 720.3a, CR 702.148b + CR 612). A
`layers_dirty` rail falls open on a pending layer-6 grant (CR 613.1f, CR 704.3).

Reorder `root_action_is_allowed` to consult the guard before enumerating.

Extract the wildcard-free `AbilityDefinition`/`Effect` traversal out of
`game/printed_cards.rs` into `types/ability_visit.rs`, parameterized by a
visitor, so the conjure-name walk and the shape predicate share one authority
and a newly added carrier fails both fixtures instead of silently escaping one.
…ion abilities

`root_may_yield_adverse_exchange` answers from the source object's STORED ability
lists, but the binder composes STORED ⊕ SYNTHESIZED.
`casting::activation_ability_definition` resolves an `ability_index` at or past
`obj.abilities.len()` from four families that are synthesized on read
(`runtime_granted_cycling_abilities` and its three siblings) and written into no
`GameObject` field, so the guard was answering an activation from lists that
provably cannot contain the definition being bound. The contract is that `false`
PROVES the verdict cannot be `Reject`; it held only because none of those four
families carries `Effect::Fight` or target-sourced damage today — a payload
accident, not a proof.

Fall open when `ability_index >= source.abilities.len()` (CR 602.2b). The rail
holds regardless of what those families come to synthesize, which is what the
Activation branch needs: the payload is card-data-driven through
`database/synthesis.rs`, so it cannot be discharged by reading one function.

Tighten `RootBinding::matches_pending`'s `Cast` arm to require
`activation_ability_index.is_none()`, mirroring the `Activation` arm. Without it a
cast root authenticated against an activation-sourced `PendingCast` and was judged
against an activated ability's spine.

Document the basis mismatch as falsifier clause (b2), enumerating all three
synthesis seams with the disposition of each, and state the payload-dismissal
criterion that separates them: dismissing a seam on its payload is admissible only
when that payload is closed by reading one bounded, non-data-driven function in
full (awaken rider), never over a data-driven surface where the only available
negative is a grep (the runtime-granted families). Replace the audit instrument —
"what do these functions RETURN" cannot see an append that happens ~950 lines
after the return — with "what does the ability look like where `PendingCast` is
constructed". Add a back-reference on `build_awaken_rider` so the coupling is
discoverable from the site whose payload would have to change.

Restate the leaf/judge superset invariant at all three sites: a new `Effect`
variant cannot create a `Reject` (both judges hard-match named variants), but
widening either judge without widening the leaf silently narrows the guard below
the reject set.

Tests pin the index boundary rather than a card, because no current runtime-granted
payload is adverse and a card fixture would stay green with the rail deleted.
@matthewevans
matthewevans enabled auto-merge August 5, 2026 23:51
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@matthewevans, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0dd8fab7-a106-4383-ba00-f888738ea4f5

📥 Commits

Reviewing files that changed from the base of the PR and between 2146d79 and c034e44.

📒 Files selected for processing (2)
  • crates/engine/src/ai_support/targeted_exchange.rs
  • crates/engine/src/game/effects/awaken.rs
📝 Walkthrough

Walkthrough

The change adds complete ability traversal, clone-free adverse-exchange detection, bounded targeted-exchange previews, and AI search prefiltering. It also replaces printed-card traversal code and adds coverage for nested abilities, exchange shapes, and preview budgets.

Changes

Adverse exchange evaluation

Layer / File(s) Summary
Complete ability traversal
crates/engine/src/types/ability_visit.rs, crates/engine/src/types/mod.rs, crates/engine/src/game/printed_cards.rs
Adds reusable pre-order visitors for nested ability and effect carriers. Printed-card discovery now uses these visitors and retains local Conjure and Meld extraction.
Adverse-shape precheck
crates/engine/src/ai_support/targeted_exchange.rs, crates/engine/src/ai_support/mod.rs, crates/engine/src/game/effects/awaken.rs
Adds root_may_yield_adverse_exchange, validates cast roots, and scans base, layered, alternative-face, cleave, and nested ability sources.
Bounded preview accounting
crates/engine/src/ai_support/targeted_exchange.rs, crates/engine/tests/integration/*
Adds TargetedExchangeBudget counters and threads them through candidate enumeration, replay, branching, and Fight or damage previews. Integration tests cover zero-budget, self-damage, and Fight cases.
AI search prefilter
crates/phase-ai/src/search.rs
Skips candidate recovery and targeted-exchange verdict evaluation for cast or activation roots without adverse-exchange shapes.

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

Sequence Diagram(s)

sequenceDiagram
  participant phase_ai as phase-ai search
  participant guard as root_may_yield_adverse_exchange
  participant state as GameState
  participant exchange as targeted_exchange_verdict

  phase_ai->>guard: inspect cast or activation root
  guard->>state: traverse ability definitions and effect carriers
  guard-->>phase_ai: adverse-exchange shape result
  phase_ai->>exchange: evaluate targeted exchange when shape exists
  exchange->>state: enumerate candidates and replay bounded previews
  exchange-->>phase_ai: targeted exchange verdict
Loading

Possibly related PRs

  • phase-rs/phase#6826: Extends the targeted-exchange verdict system with prechecks, budgeting, traversal, and AI search integration.

Suggested labels: enhancement

Suggested reviewers: lgray, andriypolanski, ntindle

🚥 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 identifies the targeted exchange performance change, which matches the main pull request objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ship/ai-perf-6826-targeted-exchange

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.

🧹 Nitpick comments (2)
crates/engine/src/game/printed_cards.rs (1)

842-863: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep a compile-time check on the name-extraction leaf.

The traversal moved to types::ability_visit, which keeps its Effect match wildcard-free. This leaf now uses _ => {}. A future Effect variant that carries a static card name will therefore compile silently and never be seeded into card_face_registry. The paired fixture walker_covers_every_nested_carrier pins carriers, not name-bearing leaves, so nothing fails.

Group the current no-name variants explicitly, or add a small exhaustive helper that classifies "does this effect name a card", so the compiler forces the decision here as it does in ability_visit.

As per coding guidelines: "wildcard _ match arms where the enum is known and an exhaustive match would let the compiler catch missing variants".

🤖 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/printed_cards.rs` around lines 842 - 863, Update
collect_conjure_names so its Effect match explicitly lists every current no-name
variant instead of using the wildcard arm, preserving the existing Conjure and
Meld handling. Keep the match exhaustive so adding a future name-bearing Effect
variant produces a compile-time error requiring it to be classified and seeded.

Source: Coding guidelines

crates/engine/src/ai_support/targeted_exchange.rs (1)

104-298: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record the guard coupling by symbol, not by file position. Both documents encode a load-bearing cross-file contract as file.rs:<line> citations. An unrelated edit in casting.rs, zones.rs, splice.rs, stickers.rs, overload.rs, or layers.rs silently retargets those pointers, and the audit procedure the guard doc prescribes then reads the wrong code. Cite the enclosing function or item name in both places, and keep a line range only where the exact span matters.

  • crates/engine/src/ai_support/targeted_exchange.rs#L104-L298: replace the roughly thirty positional citations (casting.rs:497, casting.rs:6938-6942, casting.rs:11028-11039, zones.rs:482, zones.rs:508-513, stickers.rs:490, game/layers.rs:2028, splice.rs:145, overload.rs:99-108, and the rest) with the function or item names they refer to.
  • crates/engine/src/game/effects/awaken.rs#L49-L50: replace casting.rs:6938-6942 with the name of the casting function that appends this rider.
🤖 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/ai_support/targeted_exchange.rs` around lines 104 - 298,
Replace positional file-and-line citations in the targeted_exchange_verdict
guard documentation with the enclosing function, method, or item names they
identify, retaining line ranges only when an exact span is essential; update
every cited reference throughout targeted_exchange.rs, including casting, zones,
splice, stickers, overload, layers, and related symbols. In awaken.rs, replace
the casting.rs line citation with the name of the casting function that appends
the rider. Apply the documentation-only change at both specified sites and
preserve the existing cross-file contract details.
🤖 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/ai_support/targeted_exchange.rs`:
- Around line 104-298: Replace positional file-and-line citations in the
targeted_exchange_verdict guard documentation with the enclosing function,
method, or item names they identify, retaining line ranges only when an exact
span is essential; update every cited reference throughout targeted_exchange.rs,
including casting, zones, splice, stickers, overload, layers, and related
symbols. In awaken.rs, replace the casting.rs line citation with the name of the
casting function that appends the rider. Apply the documentation-only change at
both specified sites and preserve the existing cross-file contract details.

In `@crates/engine/src/game/printed_cards.rs`:
- Around line 842-863: Update collect_conjure_names so its Effect match
explicitly lists every current no-name variant instead of using the wildcard
arm, preserving the existing Conjure and Meld handling. Keep the match
exhaustive so adding a future name-bearing Effect variant produces a
compile-time error requiring it to be classified and seeded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 46679e6c-d798-45b1-8c29-c6dd4c3a03ac

📥 Commits

Reviewing files that changed from the base of the PR and between c44a451 and 2146d79.

📒 Files selected for processing (9)
  • crates/engine/src/ai_support/mod.rs
  • crates/engine/src/ai_support/targeted_exchange.rs
  • crates/engine/src/game/effects/awaken.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/types/ability_visit.rs
  • crates/engine/src/types/mod.rs
  • crates/engine/tests/integration/main.rs
  • crates/engine/tests/integration/targeted_exchange_preview_budget.rs
  • crates/phase-ai/src/search.rs

The falsifier and audit-instrument prose encodes a load-bearing cross-file
contract. Bare `file.rs:<line>` citations silently retarget when an unrelated
edit shifts lines in `casting.rs`, `overload.rs`, `specialize.rs`, or
`game_object.rs`, and the audit procedure then reads the wrong code — the same
failure mode the comment itself dissects two paragraphs earlier.

Replace the bare positional citations with the enclosing symbol
(`prepare_spell_cast_with_variant_override_inner`,
`overload::transform_effect_in_place`, `specialize::specialize_permanent`,
`printed_cards::apply_back_face_to_object`, `CleaveFormState`,
`GameObject::specialize_faces`, `awaken::append_awaken_rider`). Citations that
already named their symbol keep their line as a hint. Raised by CodeRabbit on
PR #7049.

Documentation only; no behavior change.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Generated for head c034e443772901b357739a0a3fe2a6339678b4c4.

Parse changes introduced by this PR

✓ No card-parse changes detected.

@matthewevans
matthewevans added this pull request to the merge queue Aug 6, 2026
@matthewevans
matthewevans removed this pull request from the merge queue due to a manual request Aug 6, 2026
@matthewevans
matthewevans merged commit 7f5f0b4 into main Aug 6, 2026
18 checks passed
@matthewevans
matthewevans deleted the ship/ai-perf-6826-targeted-exchange branch August 6, 2026 00:27
lgray pushed a commit to lgray/phase that referenced this pull request Aug 6, 2026
…#6826 cost (phase-rs#7053)

phase-rs#7044 refreshed the baseline onto the phase-rs#6826 regression itself
(layers_full_eval 3495 -> 15877). Once phase-rs#7049 fixed that regression the
gate carried ~3x slack -- real cost 5464 against a 15877 baseline under a
~6% band -- so it could no longer catch a new decision-cost regression.

Re-measured on a clean worktree against an immutable card-data snapshot
(scoped card_data_hash 670a4a14, covering 46/46 scenario deck cards):

  counter                             | old    | new    | threshold
  layers_full_eval                    |  15877 |   5464 |      5801
  state_clone_for_legality            |  19342 |  11099 |     11717
  restriction_static_mode_gate_scans  | 155747 | 110891 |    116499
  sba_battlefield_snapshot_builds     |  27462 |  19626 |     20671

scripts/validate-ai-perf-reproducibility.sh: PASSED (margin+band).
25/25 band runs clean, "0 OVER-MARGIN of 29 counters", and
worst_current == baseline on every counter across 125 cold processes.

CI budget: T_run_max 90s * 2.5 + T_build ~630s = ~14.3 min < 25 min.
(The script reported T_build=1s from a warm cache; the 630s figure is the
cold isolated server-release build measured separately, which is the
conservative ceiling the check intends.)

git_sha stamps 7f5f0b4, where the 25-run validation ran. Re-confirmed
against f26f4e3 (current main, including phase-rs#7051 and phase-rs#7017):
0 FAIL, 29 PASS, +0 on every counter.

Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant