Skip to content

fix(deckbuilder): gate copy-limit affordances on engine canonical counts - #6713

Open
wondercreatemaster wants to merge 1 commit into
phase-rs:mainfrom
wondercreatemaster:fix/6659-copy-limit-affordance-v2
Open

fix(deckbuilder): gate copy-limit affordances on engine canonical counts#6713
wondercreatemaster wants to merge 1 commit into
phase-rs:mainfrom
wondercreatemaster:fix/6659-copy-limit-affordance-v2

Conversation

@wondercreatemaster

@wondercreatemaster wondercreatemaster commented Jul 27, 2026

Copy link
Copy Markdown

Summary

Fixes #6659: two copy-limit affordance gaps. (1) Client combinedCopyCounts keyed on raw entry names, so alias spellings (Nazgul/Nazgûl, case, DFC forms) each got their own ceiling. Expose the engine’s existing canonical_deck_count_key via WASM and aggregate on that key. (2) Search CardGrid had no canAddCard gate (unlike DeckStack); wire canIncrement so at-ceiling search results disable visibly.

Neither gap can produce an illegal playable deck — copy_limit_violations already canonicalizes — these are stale-affordance fixes.

Supersedes closed #6712 (same change plus CardGrid test typing that failed CI type-check).

Files changed

  • crates/engine/src/game/deck_validation.rspub fn canonical_deck_count_key + unit test
  • crates/engine/src/game/mod.rs — re-export
  • crates/engine-wasm/src/lib.rscanonicalDeckCountKey WASM binding
  • client/src/services/engineRuntime.ts / client/src/wasm/engine_wasm.d.ts — TS surface
  • client/src/components/deck-builder/useDeckBuilder.ts — canonical aggregation; include search names in limit fetch
  • client/src/components/deck-builder/CardGrid.tsx + DeckBuilder.tsxcanAddCard affordance
  • client/src/components/deck-builder/__tests__/CardGrid.test.tsx — disabled/enabled coverage (full ScryfallCard + Modern filter)
  • client/src/i18n/locales/*/deck-builder.json — copy-limit strings

Track

Developer

LLM

Model: Composer
Tier: Frontier
Thinking: high

Implementation method (required)

Method: not-applicable — exposes the existing deck_validation::canonical_deck_count_key authority for deck-builder affordance gating; no rules/resolver/parser behavior change. Legality still enforced solely by copy_limit_violations.

CR references

  • CR 100.2a / CR 100.4a — copy limit across construction zones (existing annotations on max_deck_copies / combined_copy_counts)
  • CR 201.3 — name identity / canonical face name (existing annotation on canonical_deck_count_key)

Verification

  • Gate A output below is for the current committed head.
  • Both anchors cite existing analogous code at the same seam.
  • Local pnpm run type-check — pass
  • Local vitest run src/components/deck-builder/__tests__/CardGrid.test.tsx — 2 passed
  • scripts/check-parser-combinators.sh origin/mainGate A PASS (no parser files touched).

Gate A

Gate A PASS head=fb30c845c8ff826f0c9fa853fffb2fd61f8fde1d base=e9a268a4d5ef4db00dd903ed2e1c1f6ceebcd95a

Anchored on

  • crates/engine/src/game/deck_validation.rscombined_copy_counts / max_deck_copies already use canonical_deck_count_key; WASM now exposes that same key
  • client/src/components/deck-builder/DeckStack.tsx — existing canAddCard={canIncrement} pattern mirrored onto CardGrid

Final review-impl

Final review-impl PASS head=fb30c845c8ff826f0c9fa853fffb2fd61f8fde1d — no name folding in the client; ceilings and keys come from the engine; search grid uses the same canIncrement gate as deck rows; CardGrid test fixtures match ScryfallCard / BrowserLegalityFilter.

Claimed parse impact

None.

Scope Expansion

None.

Validation Failures

None.

CI Failures

None.

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Deck Builder now enforces card copy limits consistently, including alternate spellings and card names that refer to the same card.
    • Cards at their copy limit are clearly marked, with disabled add buttons and explanatory tooltips.
    • Copy-limit messaging is now available in English, German, Spanish, French, Italian, Polish, and Portuguese.
  • Tests

    • Added coverage for enabled and disabled card-add behavior at copy limits.

Expose canonical_deck_count_key via WASM so the deck builder aggregates
alias spellings into one bucket, and disable CardGrid search-add when
canIncrement says the ceiling is reached. CardGrid tests use a full
ScryfallCard fixture and BrowserLegalityFilter "Modern".

Fixes phase-rs#6659
@github-actions github-actions Bot added the needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps) label Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The deck builder now uses engine-canonical card keys for copy-limit counting and passes eligibility into search-card tiles. Disabled tiles show copy-limit messaging, localized strings support the new states, and tests cover enabled and disabled add behavior.

Changes

Deck builder copy-limit affordances

Layer / File(s) Summary
Engine canonical key surface
crates/engine/src/game/deck_validation.rs, crates/engine/src/game/mod.rs, crates/engine-wasm/src/lib.rs
The engine’s canonical deck-count key is public, exported through WASM, and tested for alias spellings.
Client canonical copy counting
client/src/services/engineRuntime.ts, client/src/components/deck-builder/useDeckBuilder.ts
The client fetches canonical keys, aggregates combined counts by those keys, and refreshes limits for commanders, companions, signature spells, and search results.
Card-grid gating and localized states
client/src/components/deck-builder/CardGrid.tsx, client/src/components/deck-builder/DeckBuilder.tsx, client/src/components/deck-builder/__tests__/CardGrid.test.tsx, client/src/i18n/locales/*/deck-builder.json
Card-grid additions require both format legality and copy-limit eligibility; disabled states, overlays, tooltips, translations, and behavioral tests were added.

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

Sequence Diagram(s)

sequenceDiagram
  participant DeckBuilder
  participant CardGrid
  participant useDeckBuilder
  participant EngineWASM

  DeckBuilder->>CardGrid: pass canIncrement as canAddCard
  CardGrid->>useDeckBuilder: evaluate card name
  useDeckBuilder->>EngineWASM: request canonical deck-count key
  EngineWASM-->>useDeckBuilder: return canonical key
  useDeckBuilder-->>CardGrid: return copy-limit eligibility
  CardGrid-->>DeckBuilder: add card only when legal and eligible
Loading

Possibly related PRs

Suggested labels: bug

Suggested reviewers: matthewevans, kiannidev

🚥 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 is concise and accurately summarizes the main change: gating deck-builder copy-limit affordances using engine canonical counts.
Linked Issues check ✅ Passed The changes implement both linked fixes: canonical count aggregation and disabling search-result adds at the copy ceiling.
Out of Scope Changes check ✅ Passed No clearly unrelated code changes appear beyond the deck-builder copy-limit fix, tests, and supporting engine/i18n plumbing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/src/components/deck-builder/useDeckBuilder.ts`:
- Around line 348-360: Remove the local combinedCopyCounts aggregation from the
deck-builder hook and use the engine’s add-eligibility/count result for the
current deck partition and candidate instead. Update the consuming logic to rely
on the engine’s copy-limit evaluation, keeping canonicalization and counted-zone
selection centralized in the engine so it remains consistent with
copy_limit_violations.

In `@crates/engine/src/game/deck_validation.rs`:
- Around line 2622-2625: Add a verified Comprehensive Rules citation in the
documentation for canonical_deck_count_key, using the required “CR <number>:
<description>” format. Describe how that rule supports canonicalizing card
identities for copy-limit aggregation and shared UI affordance validation,
without changing the helper’s behavior.
- Around line 4000-4013: Expand canonical_deck_count_key_merges_alias_spellings
to cover a double-faced card fixture, asserting both its combined name and
front-face name resolve to the documented canonical key. Keep the existing
Nazgul spelling and case checks, and exercise the reusable canonicalization
behavior across the relevant DFC name forms rather than only comparing two
returned values.
🪄 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: 0e05be70-13c0-4e31-ae9d-ab29f53507c2

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • client/src/wasm/engine_wasm.d.ts is excluded by !client/src/wasm/**, !**/*.d.ts
📒 Files selected for processing (15)
  • client/src/components/deck-builder/CardGrid.tsx
  • client/src/components/deck-builder/DeckBuilder.tsx
  • client/src/components/deck-builder/__tests__/CardGrid.test.tsx
  • client/src/components/deck-builder/useDeckBuilder.ts
  • client/src/i18n/locales/de/deck-builder.json
  • client/src/i18n/locales/en/deck-builder.json
  • client/src/i18n/locales/es/deck-builder.json
  • client/src/i18n/locales/fr/deck-builder.json
  • client/src/i18n/locales/it/deck-builder.json
  • client/src/i18n/locales/pl/deck-builder.json
  • client/src/i18n/locales/pt/deck-builder.json
  • client/src/services/engineRuntime.ts
  • crates/engine-wasm/src/lib.rs
  • crates/engine/src/game/deck_validation.rs
  • crates/engine/src/game/mod.rs

Comment on lines 348 to +360
const combinedCopyCounts = useMemo(() => {
const counts = new Map<string, number>();
const add = (name: string, n: number) =>
counts.set(name, (counts.get(name) ?? 0) + n);
const add = (name: string, n: number) => {
const key = canonicalKeys.get(name) ?? name;
counts.set(key, (counts.get(key) ?? 0) + n);
};
for (const entry of deck.main) add(entry.name, entry.count);
for (const entry of deck.sideboard) add(entry.name, entry.count);
for (const name of commanders) add(name, 1);
for (const name of deck.signature_spell ?? []) add(name, 1);
if (deck.companion) add(deck.companion, 1);
return counts;
}, [deck, commanders]);
}, [deck, commanders, canonicalKeys]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Keep copy-limit aggregation and eligibility in the engine.

This hook independently selects counted zones and derives copy-limit state. Expose an engine add-eligibility/count result for the deck partition and candidate instead; otherwise this UI logic can drift from copy_limit_violations as format rules evolve.

🤖 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/components/deck-builder/useDeckBuilder.ts` around lines 348 - 360,
Remove the local combinedCopyCounts aggregation from the deck-builder hook and
use the engine’s add-eligibility/count result for the current deck partition and
candidate instead. Update the consuming logic to rely on the engine’s copy-limit
evaluation, keeping canonicalization and counted-zone selection centralized in
the engine so it remains consistent with copy_limit_violations.

Sources: Coding guidelines, Path instructions

Comment on lines +2622 to +2625
///
/// Public so the deck-builder WASM surface can share the same authority the
/// legality checker uses (affordance gating must not re-derive folding in JS).
pub fn canonical_deck_count_key(db: &CardDatabase, name: &str) -> String {

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

Add the required verified Comprehensive Rules citation.

This public helper now defines card identity for copy-limit validation and UI affordances, but its documentation has no verified CR <number>: <description> citation. Add the applicable rule citation and describe how it supports canonical copy aggregation.

As per path instructions, rules-touching code in crates/engine/** must carry a verified CR <number>: <description> annotation.

🤖 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/deck_validation.rs` around lines 2622 - 2625, Add a
verified Comprehensive Rules citation in the documentation for
canonical_deck_count_key, using the required “CR <number>: <description>”
format. Describe how that rule supports canonicalizing card identities for
copy-limit aggregation and shared UI affordance validation, without changing the
helper’s behavior.

Source: Path instructions

Comment on lines +4000 to +4013
#[test]
fn canonical_deck_count_key_merges_alias_spellings() {
// Public surface for the deck-builder affordance (#6659): alias
// spellings must share one key so client-side counts match validation.
let db = CardDatabase::from_json_str(&test_db_json()).unwrap();
assert_eq!(
canonical_deck_count_key(&db, "Nazgul"),
canonical_deck_count_key(&db, "Nazgûl")
);
assert_eq!(
canonical_deck_count_key(&db, "Nazgul"),
canonical_deck_count_key(&db, "nazgûl")
);
}

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

Cover DFC/front-face canonicalization in the regression test.

This test only exercises the Nazgul spelling and case variants. The exposed contract also promises that double-faced combined names and front-face names share a key; add a DFC fixture and assert the expected canonical key, not only equality between two calls.

As per path instructions, reusable building blocks must be tested across their parameter range rather than through a single card case.

🤖 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/deck_validation.rs` around lines 4000 - 4013, Expand
canonical_deck_count_key_merges_alias_spellings to cover a double-faced card
fixture, asserting both its combined name and front-face name resolve to the
documented canonical key. Keep the existing Nazgul spelling and case checks, and
exercise the reusable canonicalization behavior across the relevant DFC name
forms rather than only comparing two returned values.

Source: Path instructions

@matthewevans matthewevans self-assigned this Jul 28, 2026
@matthewevans matthewevans added the bug Bug fix label Jul 28, 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

Reviewed current head fb30c845c8ff826f0c9fa853fffb2fd61f8fde1d.

This superseding PR corrects the CardGrid fixture types, but it retains the two material #6712 defects.

  1. Keep copy-limit eligibility and counted-zone selection in the engine. useDeckBuilder reconstructs the count in React and compares it with separate maxDeckCopies/canonicalDeckCountKey calls. That has already diverged from the validator: the hook counts the dedicated deck.companion, while engine construction_deck_cards deliberately excludes it because it is outside the starting deck. It also counts sideboard entries for Commander/Brawl variants, while the engine strips those entries before copy-limit validation because they are not in the loaded deck. Please expose an engine/WASM candidate eligibility result derived from the same DeckCompatibilityRequest and copy_limit_violations authority, and have the UI render that result.

  2. Do not fail open while eligibility is stale or unavailable. The effect retains the prior maps while a format/search request is pending, clears them on rejection, and canIncrement returns true when no entry exists. A newly returned search card (including a canonical alias) can therefore be added before its request resolves; a failed request leaves it addable. handleAddCard has no independent eligibility guard. Make unresolved/stale eligibility unavailable until the authoritative response for the current deck, format, and candidate is available.

  3. Add a discriminating runtime test through the engine → WASM → deck-builder search-grid path. It must fail if the eligibility binding or canonicalization is removed, cover the alias ceiling and the companion/Commander-sideboard zone selection, and cover an unresolved or rejected request. The present CardGrid tests only verify an injected boolean. The public canonical-key test should also cover the documented DFC combined-name/front-face contract.

The frontend CI is green, but Rust/card-data checks are still pending and this branch is behind current main. CodeRabbit independently raised the engine-authority and missing-DFC-coverage findings. Its CR-annotation thread is not actionable: the existing helper already has the project's allowed compound CR 201.3 + CR 100.2a annotation.

@matthewevans matthewevans removed their assignment Jul 28, 2026
@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix needs-maintainer AI-contribution PR requires human triage (Non-dev track or unresolved gaps)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants