feat(deck-builder): ROLE_EVIDENCE recall widening — removal/boardwipe/ramp/cardDraw (E136)#1307
Open
georgepapagapitos wants to merge 4 commits into
Open
feat(deck-builder): ROLE_EVIDENCE recall widening — removal/boardwipe/ramp/cardDraw (E136)#1307georgepapagapitos wants to merge 4 commits into
georgepapagapitos wants to merge 4 commits into
Conversation
Audit (E107, 2026-07-14) found removal gate-blind at 26.7% (1,370 cards tagged removal but rejected by the oracle-evidence gate). Adds four verified real-card patterns to validateCardRole's removal evidence: library-tuck, fight past the exact "fights target creature" adjacency, a doesn't/don't-untap tap-lock anchored to third-person "its/their controller's" (never "your", so a card's own self-tap-down cost can't false-positive), and a "target opponent" sacrifice-edict subject. Every fixture (positive and negative) is live-verified oracle text fetched from Scryfall, not written from memory.
Audit (E107, 2026-07-14) found boardwipe gate-blind at 21% (186 cards tagged boardwipe but rejected by the oracle-evidence gate). Two verified real-card fixes: a lenient damage-to-each-creature join for player/type-scoped one-sided wipes whose object isn't literally "each creature" right after "to" (Flame Wave), and a -N/-N branch widened to a singular noun and X magnitude (Virulent Plague: "Creature tokens get -2/-2.") while keeping the "get" (not "gets") verb-agreement guard that already excludes a single-target spot-removal spell from the mass-wipe branch (Battle at the Bridge). Every fixture (positive and negative) is live-verified oracle text fetched from Scryfall, not written from memory.
Audit (E107, 2026-07-14) found ramp gate-blind at 11% (226 cards tagged ramp but rejected by the oracle-evidence gate). Three verified real-card fixes: land-recursion-to-battlefield accepts "to the battlefield" (not just "onto", Splendid Reclamation), an untap-lands burst branch (Wilderness Reclamation), and convoke/improvise GRANTED to other spells via a "have"/"has" verb (Chief Engineer, Inspiring Statuary) — scoped to the granting template so a card that merely has convoke/improvise as its own printed keyword (Aerial Boost) doesn't trip it. Every fixture (positive and negative) is live-verified oracle text fetched from Scryfall, not written from memory.
Audit (E107, 2026-07-14) found cardDraw gate-blind at 5.6% (288 cards
tagged card-advantage but rejected by the oracle-evidence gate) —
monarch alone was 7/30 of the judged draw misses. Three verified
real-card fixes: monarch grant ("you become the monarch" — Palace
Jailer), investigate (frequently printed without its Clue-token
reminder text in modern templating — Duggan, Private Detective), and
mass graveyard-to-hand recursion phrased with "put" instead of
"return" (Campfire).
Both new branches carry a subject guard verified against a real
wrong-subject card each: monarch excludes a preceding "whenever" so an
opponent-facing grant (Jared Carthalion, True Heir: "target opponent
becomes the monarch") can't false-positive off its own unrelated
"Whenever you become the monarch" trigger text; investigate excludes
an opponent subject so removal-compensation investigate (Declaration
in Stone: "That player investigates...") isn't credited as your own
card advantage.
Every fixture (positive and negative) is live-verified oracle text
fetched from Scryfall, not written from memory.
georgepapagapitos
force-pushed
the
e136-role-evidence
branch
from
July 23, 2026 16:47
20166b1 to
1ecbcc6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
E136 — ROLE_EVIDENCE recall widening
Widens the four
ROLE_EVIDENCEoracle-text regexes invalidateCardRole(
frontend/src/deck-builder/services/tagger/client.ts) — the singlechoke-point for role validation shared by the live generation gate and
report
roleCounts. Per the E107 full-corpus audit(
docs/e107-classifier-audit-2026-07-14.md), this gate correctly REJECTScrowd-tag over-breadth, but also had a genuine recall hole: ~1,000 cards
tagged with a role by the crowd tagger whose real oracle text describes
that role in a shape the regex didn't recognize yet ("gate-blind").
Method: every fixture below (positive AND negative) is oracle text
fetched live from
api.scryfall.com/cards/named?exact=…at build time —none written from memory. Every widening is a provable regex superset:
each change either extends an existing alternative (e.g.
onto the battlefield→(?:onto|to) the battlefield) or adds a brand-new|-alternative — never narrows or removes one. That means no card thatvalidated under the old regex can newly fail under the new one; the
same-corpus sweep below confirms this held for real (zero regressions
across 31,622 cards).
One commit per role family (removal/boardwipe/ramp/cardDraw) so a
ship-gate regression traced to one role can be reverted independently.
removal (+4 patterns)
put target creature on top/bottom of its owner's library. No destroy/exile/counter/bounce branch caught this shape.Anchor to the Aether: "Put target creature on top of its owner's
library. Scry 1."
between "fights" and "target creature" ("fights ANOTHER target
creature", "fights up to one target creature"), which the old
fights? target creatureliteral missed.Blood Feud: "Target creature fights another target creature."
doesn't/don't untap during its/their controller's [next] untap step, de facto tempo removal. Anchored to third-person"its"/"their" (never "your") so a card's own self-tap-down cost can't
false-positive.
Icefall Regent: "...tap target creature an opponent controls. That
creature doesn't untap during its controller's untap step..."
Frost Breath: "Tap up to two target creatures. Those creatures don't
untap during their controller's next untap step." (plural verb form)
target opponentalongside theexisting
target player/each opponent/etc.Tribute to Hunger: "Target opponent sacrifices a creature of their
choice."
FP guard (negative test): Basalt Monolith — "This artifact doesn't
untap during your untap step" — a mana rock's own self-downside cost,
correctly excluded by the third-person anchor. Brainstorm — "put two
cards from your hand on top of your library" — putting your OWN cards
back is not a tuck (no "target", no "owner's").
boardwipe (+2 patterns)
damage to each creatureadjacency, missing player/type-scoped one-sidedwipes where another clause sits between "to" and "each creature".
Flame Wave: "deals 4 damage to target player or planeswalker and
each creature that player...controls."
"creatures" + a literal digit.
Virulent Plague (current Oracle text): "Creature tokens get -2/-2."
FP guard (negative test): Battle at the Bridge — "Target creature
gets -X/-X until end of turn" (single-target spot removal). The widen
keeps the literal
get -(notgets -) verb-agreement quirk intact, whichis exactly what excludes this from the MASS-wipe branch.
ramp (+3 patterns)
just "onto".
Splendid Reclamation: "Return all land cards from your graveyard to
the battlefield tapped."
text of its own to trip any other branch.
Wilderness Reclamation: "...untap all lands you control."
a real cost-reduction engine, scoped so a card that merely HAS
convoke/improvise as its own keyword doesn't trip it.
Chief Engineer: "Artifact spells you cast have convoke."
Inspiring Statuary: "Nonartifact spells you cast have improvise."
FP guard (negative test): Aerial Boost — bare "Convoke" reminder
text on its own cost, no granting clause — correctly excluded.
cardDraw (+3 patterns)
you become the monarch, excluded when preceded by"whenever" so a trigger CONDITION referencing an already-active monarch
isn't mistaken for a grant. Monarch alone was 7/30 (23%) of the
judged draw misses in the original audit.
Palace Jailer: "When this creature enters, you become the monarch."
text in modern templating, so the existing
draws? abranch (which onlycatches the reminder's "Draw a card") missed it outright. Excluded when
the investigating subject is an opponent, not you.
Duggan, Private Detective: "Whenever Duggan enters or attacks,
investigate." (no reminder text printed)
Campfire: "Put all commanders you own from the command zone and from
your graveyard into your hand."
FP guards (negative tests, both subject-discipline): Jared
Carthalion, True Heir — "target opponent becomes the monarch" / "You
can't become the monarch this turn" — neither is the literal "you
become the monarch" grant; correctly excluded. Declaration in Stone —
"That player investigates for each nontoken creature exiled this way"
compensates the removed creature's controller (typically an opponent), not
your own card advantage; correctly excluded.
Audit-harness deltas (same-day, same corpus, same tagger snapshot)
Ran the committed harness
(
E107_AUDIT=1 E107_CORPUS=… ./node_modules/.bin/vitest run src/deck-builder/services/tagger/e107-audit.live.test.ts) twice againstthe same freshly-pulled Scryfall oracle-cards bulk corpus
(31,622 commander-legal cards, matching the original audit's corpus size
exactly) and the same worktree
tagger-tags.jsonsnapshot — once onthis branch, once with
client.tsswapped toorigin/main— so the deltabelow isolates the regex change from the ~9-day tagger-snapshot drift
since the original 2026-07-14 audit numbers.
363 cards newly recognized corpus-wide, zero rate increases. Spot-check
of the flip list turned up real cards beyond the hand-picked fixtures
validating the same patterns generically — e.g. removal newly catches
Waterknot (tap-lock aura) and Cruel Ultimatum; boardwipe newly catches
Goblin Chainwhirler (scoped damage); ramp's flip list includes the exact
Chief Engineer/Splendid Reclamation/Hoarding Broodlord fixtures used
above, confirming they're representative, not cherry-picked.
Also verified zero regressions: no card that validated under the old
regex failed to validate under the new one, and no validated card's role
changed — exactly what the provable-superset property predicts, confirmed
against the full 31,622-card corpus programmatically (not sampled).
Composition-affecting — ship gate pending
This changes which cards role caps/counts see (generation gate + report
roleCounts), so it needs the fulldeckgen-eval-gatelive 15-deck A/Bbefore merge, per the board note. Not requesting that here — this PR
is implementation + rigorous real-text unit tests + the audit-harness
delta above. No auto-merge enabled; worktree kept at
/Users/georgepapagapitos/dev/spellcontrol-e136-role-evidence.Verification
npm run typecheck— clean (frontend + backend).npm run lint— 0 errors (37 pre-existing warnings, none in changedfiles); stylelint clean.
npm run format:check— clean.npm run test:coverage— frontend 487/489 files (2 pre-existing skips),6,816/6,833 tests, coverage thresholds met; backend 66/66 files,
1,049/1,049 tests, coverage thresholds met (dev Postgres up).
client.test.ts— 244/244 passing, including 19 new E136 tests (13positive-evidence + 6 named negative-guard/FP tests, one per new branch
with a genuine subject/precision risk).