Skip to content

feat(ai): ground deck suggestions in the deck's actual plan (#295) - #349

Merged
untraceablez merged 1 commit into
mainfrom
feat/295-ai-synergy
Aug 21, 2026
Merged

feat(ai): ground deck suggestions in the deck's actual plan (#295)#349
untraceablez merged 1 commit into
mainfrom
feat/295-ai-synergy

Conversation

@untraceablez

Copy link
Copy Markdown
Collaborator

Closes #295. With #294 merged, this closes out the Decks & EDH tooling milestone.

Why the picks were generic

The complaint in #295 is that AI suggestions return goodstuff rather than cards that fit the deck. The grounding explains it — three defects, each fixed here:

1. The commander's ability was truncated to its first line.

scan.commander_text = (oracle or "").split("\n")[0][:160]

A commander's engine is routinely on line two or three — an activated ability, a death trigger. For a large class of decks the model never saw the thing the deck is built around, then got asked to suggest cards that synergise with it. Now the full text, joined onto one line and bounded at 600 chars so a wall of reminder text can't crowd out the rest of the prompt.

2. Candidates were bare names. The model was asked to reason about synergy while being told only what the cards were called. Each candidate now arrives as Name — role | shared keywords | what it does.

3. The shortlist was arbitrary. Only so many candidates fit in a prompt, and the old code took list(pool.values())[:60] — the first 60 by oracle id. Most of the collection was never seen, and which part was seen had nothing to do with the deck. It's now the 40 best-fitting cards, scored with #294's deck_synergy. Fewer candidates, but the right ones — and the freed tokens pay for the annotations.

Prompt

Criteria are now explicitly ordered — the commander's specific ability, then the deck's themes and creature types, then thin roles — with an instruction not to suggest powerful cards that don't advance the plan, and a requirement that each reason name the specific interaction rather than calling something "solid value".

What deliberately did not change

Validation still covers the whole owned pool, not the shortlist. Narrowing what we send is exactly the change that could have accidentally narrowed what we accept, turning a legitimate pick into a "hallucination". There's a test for it: the model names a card that wasn't among the 40, and it still resolves.

llm._candidate_pool is deleted rather than left behind — the shortlist path replaced its only caller, and a second pool builder lying around is how two paths drift.

Verification

  • 1285 passed, 100% coverage, 0 missed lines (11 new tests). ruff and mkdocs --strict clean. No migration.
  • Tests assert against the actual prompt a fake client receives: that the commander's line-three ability is present, that a candidate line carries role and rules text, that a synergistic card survives into the shortlist when 60 generics compete for the slots, and that only keywords the deck cares about are spent on prompt space.
  • One test I wrote failed and was right to: a commander that merely is an Elf but never mentions Elves does not make an Elf deck. That's now pinned as a negative case.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LxrePqFWyzQTcWaroSk4sH

The AI picks read as generic goodstuff, and the grounding explains why. Three defects, each fixed:

**The commander's ability was truncated to its first line** (`oracle.split("\n")[0][:160]`). A
commander's engine is routinely on line two or three — an activated ability, a death trigger — so
for a large class of decks the model never saw the thing the deck is built around. Full text now,
bounded at 600 chars so a wall of reminder text can't crowd out the rest of the prompt.

**Candidates were bare names.** The model was asked to reason about synergy while being told only
what the cards are called. Each candidate now arrives as `Name — role | shared keywords | what it
does`, so a pick can be reasoned about rather than recognised.

**The shortlist was arbitrary.** Only so many candidates fit in a prompt, and the old code took
`list(pool.values())[:60]` — the first 60 by oracle id. Most of the collection was never seen, and
which part *was* seen had nothing to do with the deck. The shortlist is now the 40 best-fitting
cards, scored with #294's `deck_synergy` — fewer candidates, but the right ones, and the freed
tokens pay for the annotations.

The prompt now orders its criteria explicitly (commander's specific ability, then themes, then thin
roles), tells the model not to suggest powerful cards that don't advance the plan, and requires each
reason to name the specific interaction rather than "solid value". The deck's creature types are
grounded too, via the same deck-derived tribe detection.

Validation is unchanged in reach: card names are still checked against the **whole** owned pool, not
the shortlist, so a card the model knows from elsewhere still resolves if you own it. There is a
test for exactly that, because narrowing what we send is precisely the change that could have
narrowed what we accept.

`llm._candidate_pool` is gone — the shortlist path replaced its only caller, and a second pool
builder left lying around is how the two paths would drift.

11 new tests; suite 1285 passing at 100% coverage. Closes the Decks & EDH tooling milestone
alongside #294.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxrePqFWyzQTcWaroSk4sH
@untraceablez
untraceablez merged commit 129064d into main Aug 21, 2026
9 checks passed
@untraceablez
untraceablez deleted the feat/295-ai-synergy branch August 21, 2026 21:26
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.

Improve AI-grounded deck suggestions (synergy-aware prompt + grounding)

1 participant