feat(deck): add companion zone with deckbuilding-restriction validation#77
Conversation
Add a COMPANION deck zone so a deck can pin a companion card, and validate the companion's deckbuilding restriction against the deck's mainboard + commander cards. Companion restrictions are not present in Scryfall's structured data (only the `Companion` keyword is), so the ten companions' conditions are encoded as a name-keyed predicate registry in lib/deck/legality/companions.ts. companionRule runs as part of fullLegality for every format, surfacing a companion_violation LegalityIssue that flows through the existing DeckLegalityBadge. - prisma: add COMPANION to the Zone enum (+ migration) - snapshot: thread cmc/manaCost/oracleText into SnapshotCard for restrictions - UI: companion zone in the move menu, add destinations, row hotkeys, the sideboard/considering panel, and every Record<Zone> map; import/export round-trip - tests: companion legality coverage for all ten companions Closes #37 Claude-Session: https://claude.ai/code/session_01XF6NhHy3smZTYCSa9swp1B
…one in DnD/read-only views
|
Warning Review limit reached
Next review available in: 19 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds COMPANION zone support across schema, legality, deck I/O, builder UI, hotkeys, tests, and docs. It also adds companion-specific legality predicates and formats companion violations as part of full deck legality. ChangesCompanion Zone Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
lib/deck/io/serialize.ts (1)
6-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDeduplicate the zone sort contract.
lib/deck/io/adapters/_shared.ts:68-85already definesZONE_ORDER, and this copy now has to stay in lockstep with it. Centralizing the ordering in one shared module would avoid JSON serialization drifting from the adapter order the next time a zone is added.🤖 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 `@lib/deck/io/serialize.ts` around lines 6 - 12, The zone ordering contract is duplicated in serialize.ts and _shared.ts, so JSON serialization can drift from adapter ordering. Remove the local ZONE_ORDER constant in serialize.ts and reuse the shared ZONE_ORDER from the _shared module instead, keeping the ordering definition centralized and consistent across the serializer and adapter code.
🤖 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 `@lib/deck/io/adapters/arena.ts`:
- Around line 43-48: The Companion section in arena.ts is inserting its own
blank separator, which causes duplicate empty lines before Sideboard and a
trailing newline for companion-only exports. Update the section assembly around
the companion block so the Companion header and its lines are appended without
an internal empty string, and let the existing export flow handle separators
between sections consistently. Use the byZone, lineFor, and lines.push logic in
the arena adapter to locate and normalize this behavior.
In `@lib/deck/legality/companions.ts`:
- Around line 156-160: The Kaheera legality check in the companions predicate
currently rejects creatures based only on printed subtypes from
cardTypesOf/creatureSubtypesOf, so Changeling creatures are missed. Update the
check function to also inspect the card’s oracle text (or equivalent card data)
before rejecting a creature, and treat any card with Changeling as having all
creature types so it passes the KAHEERA_TYPES requirement. Use the existing
companions check logic and the cards iteration in companions.ts to keep the fix
localized.
- Around line 102-107: The hasActivatedAbility helper only checks for colon-form
abilities, so keyword activated abilities like Equip, Crew, and Reconfigure are
missed. Update hasActivatedAbility(card: JudgedCard) in companions.ts to
recognize these keyword-based activated abilities in addition to the existing
isBasicLandCard and /:\s/ checks, using the card.oracleText text so Zirda
legality correctly accepts permanents whose only activated ability is expressed
as a keyword line.
---
Nitpick comments:
In `@lib/deck/io/serialize.ts`:
- Around line 6-12: The zone ordering contract is duplicated in serialize.ts and
_shared.ts, so JSON serialization can drift from adapter ordering. Remove the
local ZONE_ORDER constant in serialize.ts and reuse the shared ZONE_ORDER from
the _shared module instead, keeping the ordering definition centralized and
consistent across the serializer and adapter code.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0bbc07e6-b66c-4ed9-80e9-8e356adc9566
📒 Files selected for processing (25)
CONTEXT.mdREADME.mdapp/_components/builder/__tests__/move-card-menu.test.tsxapp/_components/builder/card-row-sortable.tsxapp/_components/builder/move-card-menu.tsxapp/_components/builder/sideboard-considering-dnd.tsxapp/_components/builder/sideboard-considering.tsxapp/_components/deck/deck-history-list.tsxapp/_components/header-search/deck-mode-bar.tsxapp/_components/hotkeys/registry.tsdocs/adr/0002-deck-illegality-is-a-ui-state.mdlib/deck/__tests__/add-intent.test.tslib/deck/add-intent.tslib/deck/io/adapters/_shared.tslib/deck/io/adapters/arena.tslib/deck/io/serialize.tslib/deck/legality.test.tslib/deck/legality/__tests__/companions.test.tslib/deck/legality/companions.tslib/deck/legality/index.tslib/deck/legality/shared.tslib/deck/mutation/snapshot-pure.tslib/deck/mutation/types.tsprisma/migrations/20260628000000_zone_companion/migration.sqlprisma/schema.prisma
- arena.ts: fix separator logic so companion-only export has no trailing blank line and companion+sideboard gets exactly one blank between sections - companions.ts: Kaheera now accepts Changeling creatures (all creature types by oracle rule, not printed subtypes) - companions.ts: hasActivatedAbility covers keyword-shorthand activated abilities (Equip, Crew, Reconfigure) that lack a colon - serialize.ts: remove duplicate ZONE_ORDER; import from _shared
The DnD owner view always rendered the companion ZoneBlockDnd even when no companion was set. Guard it with companion.length > 0, matching the existing conditional in the read-only sibling view.
Merge-commit CI ran coverage over companions.ts (new in this PR) and
dropped total branch coverage to 98.85%, below the 99% threshold.
Five previously uncovered branches:
- Gyruda: null cmc falls back to 0 (even — legal)
- Kaheera: creature with no subtype ("Creature", no "—") flags illegal
- Umori: all-land deck short-circuits before type-sharing check
- Umori: null typeLine treated as no shared type
- Zirda: null oracleText treated as empty string (no activated ability)
Two unreachable ?? fallbacks on split()[0] and split()[1] — guaranteed
non-undefined after preceding null/includes checks — marked
/* c8 ignore next */ to exclude from branch denominator.
companions.ts now 100% branch coverage; global branches: 99.85%.
Intent
Implement issue #37 'Companions': add a COMPANION deck zone alongside mainboard/sideboard/considering/commander, let a user assign a companion card to it, and validate each companion's oracle-text deckbuilding restriction against the deck's mainboard+commander cards with a clear legal/illegal indicator reusing the existing LegalityIssue -> formatLegalityIssue -> DeckLegalityBadge machinery. Companion restrictions are NOT in Scryfall structured data (only the Companion keyword is) and the set of companions is a fixed closed set of ten, so per captain decision they are encoded as a name-keyed predicate registry in lib/deck/legality/companions.ts (option A, hardcoded), a deliberate choice. companionRule runs universally in fullLegality (not per-format), threads cmc/manaCost/oracleText as optional SnapshotCard fields, flags more than one card in the COMPANION zone, and flags unknown cards in the zone. Per captain decisions during review: companion price/tokens DO count toward the deck (like Commander, COMPANION intentionally left out of EXCLUDED_ZONES); companion color identity IS enforced in singleton formats (colorIdentityRule now scans COMPANION). All prior review findings are resolved in the current HEAD: read-only companion visibility, always-render DnD companion drop-target, single-companion check, deck-history zone ordering includes COMPANION, and companion color-identity enforcement. The zirda activated-ability heuristic, null-cmc->MV0 default, and jegantha hybrid-symbol edge are documented and accepted as bounded given the hardcoded approach. UI wiring spans Zone enum + migration, add-intent destinations, import/export adapters, builder move-card menu, hotkeys, sortable rows, owner DnD view, read-only view, deck-mode-bar and history labels, CONTEXT.md domain docs, and tests (1908 passing locally, typecheck clean).
What Changed
COMPANIONzone added to the DB schema (Zoneenum + migration); a name-keyed predicate registry inlib/deck/legality/companions.tsencodes all ten companion deckbuilding restrictions (hardcoded, not from Scryfall structured data) and runs ascompanionRuleinsidefullLegality, flagging unknown companions, multiple companions, and each card's oracle-text restriction againstMAINBOARD+COMMANDERcardscolorIdentityRuleextended to scan theCOMPANIONzone alongsideCOMMANDER, so off-color companions surface as legality issues in singleton formatsRisk Assessment
✅ Low: Well-bounded additive feature: new enum value, name-keyed predicate registry, and symmetric UI/IO wiring; legality entry point and deck-size counting verified unaffected, with only minor edge-case inaccuracies in two companion predicates.
Testing
Ran 62 unit tests covering companion restriction logic, color-identity enforcement, zone routing (add-intent), and UI menu ordering — all pass. No E2E browser run was performed as the app requires a live DB, but the critical business logic (companion predicate registry, fullLegality wiring, colorIdentityRule scanning COMPANION zone) is fully exercised by the test suite.
Evidence: Full test run output (62 tests, 4 files)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
lib/deck/legality/companions.ts:237- Umori check: if the first nonland card injudgedhas a null/unrecognised typeLine,cardTypesOfreturns [] sosharedstarts empty and the rule reports "nonland cards do not all share a card type" even when every other card does share one. Same null-typeLine input is harmless for the other predicates (isPermanent/isLand return false), but Umori produces a false violation. Seedsharedfrom the first card with a recognised type, or skip null-type cards.lib/deck/legality/companions.ts:120- formatMinimum() maps OATHBREAKER to COMMANDER_DECK_SIZE (100), but an Oathbreaker deck is a 60-card singleton, not 100. Only affects Yorion's needed-count math in Oathbreaker (a non-real combination, like Yorion-in-Commander), so impact is nil today, but it's a latent inaccuracy if companion logic is reused.lib/deck/add-intent.ts:47- COMPANION is offered as an add/move destination in every format (buildAddDestinations, move-card-menu, hotkeys) and companionRule runs universally, matching the captain's documented decision. Noting only: in formats where the companion mechanic doesn't exist the zone is still selectable. Intentional per stated intent.✅ **Test** - passed
✅ No issues found.
node_modules/.bin/vitest run lib/deck/legality/__tests__/companions.test.ts— 18 tests: zone gating (empty zone, unknown card, multiple companions, single companion), all 10 companion restrictions (Gyruda/Obosh/Keruga/Lurrus/Lutri/Jegantha/Kaheera/Umori/Yorion/Zirda), mainboard+commander scope isolation, fullLegality integration, and registry completenessnode_modules/.bin/vitest run lib/deck/legality.test.ts— 27 tests including 2 new companion color-identity cases: flags off-color companion under B/G commander, allows on-color companionnode_modules/.bin/vitest run lib/deck/__tests__/add-intent.test.ts— 14 tests, confirming COMPANION appears as add destination alongside mainboard/sideboard/consideringnode_modules/.bin/vitest run app/_components/builder/__tests__/move-card-menu.test.tsx— 3 tests confirming zone ordering logic with companion in menu✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.
Summary by CodeRabbit