Skip to content

feat(deck-builder): ROLE_EVIDENCE recall widening — removal/boardwipe/ramp/cardDraw (E136)#1307

Open
georgepapagapitos wants to merge 4 commits into
mainfrom
e136-role-evidence
Open

feat(deck-builder): ROLE_EVIDENCE recall widening — removal/boardwipe/ramp/cardDraw (E136)#1307
georgepapagapitos wants to merge 4 commits into
mainfrom
e136-role-evidence

Conversation

@georgepapagapitos

Copy link
Copy Markdown
Owner

E136 — ROLE_EVIDENCE recall widening

Widens the four ROLE_EVIDENCE oracle-text regexes in validateCardRole
(frontend/src/deck-builder/services/tagger/client.ts) — the single
choke-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 REJECTS
crowd-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 that
validated 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)

  • Library-tuckput 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."
  • Fight, widened past exact adjacency — real cards insert a word
    between "fights" and "target creature" ("fights ANOTHER target
    creature", "fights up to one target creature"), which the old
    fights? target creature literal missed.
    Blood Feud: "Target creature fights another target creature."
  • Tap-lockdoesn'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)
  • Sacrifice-edict subject list gains target opponent alongside the
    existing 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)

  • Scoped damage, lenient join — the old branch required literal
    damage to each creature adjacency, missing player/type-scoped one-sided
    wipes 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."
  • -N/-N widened — singular noun + X magnitude, not just plural
    "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 - (not gets -) verb-agreement quirk intact, which
is exactly what excludes this from the MASS-wipe branch.

ramp (+3 patterns)

  • Land-recursion-to-battlefield accepts "to the battlefield", not
    just "onto".
    Splendid Reclamation: "Return all land cards from your graveyard to
    the battlefield tapped."
  • Untap-lands burst — new branch, no cost-reduction/search/treasure
    text of its own to trip any other branch.
    Wilderness Reclamation: "...untap all lands you control."
  • Convoke/improvise GRANTED to other spells via a "have"/"has" verb —
    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)

  • Monarch grantyou 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."
  • Investigate — frequently printed WITHOUT its Clue-token reminder
    text in modern templating, so the existing draws? a branch (which only
    catches 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)
  • Mass graveyard-to-hand via "put" as well as "return".
    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 against
the 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.json snapshot — once on
this branch, once with client.ts swapped to origin/main — so the delta
below isolates the regex change from the ~9-day tagger-snapshot drift
since the original 2026-07-14 audit numbers.

role before gate-blind after gate-blind rate before → after cards fixed
removal 1,397 / 5,217 1,186 / 5,217 26.8% → 22.7% -211
boardwipe 190 / 896 152 / 896 21.2% → 17.0% -38
ramp 278 / 2,189 239 / 2,189 12.7% → 10.9% -39
cardDraw 294 / 5,309 219 / 5,309 5.5% → 4.1% -75

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 full deckgen-eval-gate live 15-deck A/B
before 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 changed
    files); 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 (13
    positive-evidence + 6 named negative-guard/FP tests, one per new branch
    with a genuine subject/precision risk).

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.
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