Skip to content

fix(deck-builder): payoff-FP cleanup + measured recall widening in synergy axes (E139)#1309

Open
georgepapagapitos wants to merge 14 commits into
mainfrom
e139-payoff-cleanup
Open

fix(deck-builder): payoff-FP cleanup + measured recall widening in synergy axes (E139)#1309
georgepapagapitos wants to merge 14 commits into
mainfrom
e139-payoff-cleanup

Conversation

@georgepapagapitos

Copy link
Copy Markdown
Owner

Summary

E139 fix slice from the E107 classifier audit (docs/e107-classifier-audit-2026-07-14.md, waves 2+3): payoff-FP cleanup on 6 axes' precision findings + measured recall widening on 8 axes, per the audit's mechanically-verified recall lists. 13 commits, one per axis family, so a gate-found regression can drop one family without losing the slice. A trailing style: commit applies prettier formatting (mechanical only, run once at the end — see note below).

⚠️ COMPOSITION-AFFECTING — ship gate pending. This changes AXES predicate output for classifyCard(), which feeds tallyAxisInvestment (package boost), isUnsupportedSynergyPayoff (dependency gate), and isLoadBearing (trim protection) in the deck generator. Per the task spec, I have not run the 15-deck live panel or enabled auto-merge — the orchestrator runs the ship gate after this PR is delivered.

Per-family breakdown

Family Fix Example (real oracle text, fetched live) Guard
artifacts Precision: "Affinity for X" only counts when X is artifacts. Recall: type-line-only Treasure permanents; "if mana from a Treasure was spent" payoff rider Negative: Angelic Observer ("Affinity for Citizens"). Positive: Buried Treasure (Artifact — Treasure, no "artifact"/"Treasure" word in rules text), Hired Hexblade (Treasure-mana rider) affinity for artifacts requires the literal phrase, not just the bare keyword
graveyard Precision: flashback/disturb/jump-start/aftermath/unearth/embalm/eternalize/encore only ever recur THEMSELVES — dropped from payoff. Delve/escape kept (they genuinely need a stocked yard as a cost) Negative: Think Twice (flashback), Faithless Looting, Increasing Confusion, Startled Awake, Chronomancer (all corrected). Positive: Treasure Cruise (delve), Uro (escape) GY_FUEL_KEYWORDS = ['delve','escape'] replaces the 10-keyword list; selfReturnOnly widened to also catch "put this card…" (DFC self-transform), not just "return"
tribal Type-kind confusion, wrong-subject, inverse-payoff Plague Engineer ("creatures … your OPPONENTS control get -1/-1" — hate, not reward), Volo ("doesn't share a creature type" — rewards diversity, the regex matched the bare substring ignoring the negation) "of the chosen type" bounded to a 25-char gap from "creature(s)" in-clause (Deification's unrelated "creature" mention elsewhere in the clause no longer bridges to a different noun's "of the chosen type")
mill Recall: "each player mills" subject; trigger-on-mill payoff Chill of Foreboding (each-player mill), Glowing One ("whenever a player mills a nonland card") "each player" added to the shared OPPONENT_MILL subject list (also used by graveyard's self-mill check — first-match-wins per clause, consistent with the existing pattern)
poison Recall: "gets N poison counter(s)" grant; Corrupted payoff Fynn (poison-counter grant), Bonepicker Skirge (Corrupted). Corrects Vraska, Betrayal's Sting (-9 grant)
spellslinger Recall: recursion producer; Storm + graveyard-instant/sorcery-count payoff Archaeomancer (recursion), Grapeshot (Storm), Cryptic Serpent (graveyard-count, same shape as Affinity)
cycling Recall: "you cycled…this turn" trigger; graveyard counts vs. recursion Spellpyre Phoenix (trigger + recursion), Abandoned Sarcophagus (recursion). Corrects Zenith Flare (count-damage) "from your graveyard" (recursion) vs. "in your graveyard" (count) — same phrase, split by preposition
energy Recall: "whenever you get one or more {E}" payoff Territorial Gorger
equipment Precision (47%): tutor/recursion/attach re-bucketed producer-side. Recall: equip-cost reduction, Affinity for Equipment, count scaling Armed Response (count scaling), Bureau Headmaster (equip-cost), Oxidda Finisher (Affinity). Corrects Stoneforge Mystic, Sigarda's Aid (producer↔payoff) ETB-attach text excluded from payoff (enabler, not reward) — clause-scoped so it can't false-exclude an unrelated reward clause on the same card
vehicles Recall: Pilot type-line; crew-support body text Aeronaut Admiral, Cloudspire Captain. Corrects Depala, Kotori, Greasefang, Mu Yanling (all Pilot-typed, previously zero producer signal)
superfriends Precision (20%): protection grants + loyalty-counter-growth double-bucketing Shalai, Deification (hexproof, opposite word order from the existing guard). Corrects Oath of Gideon (loyalty-growth == the producer's own text) Deliberately conservative — see scope decision below
counters Precision: counter-REMOVAL (Aether Snap) was reading as "moves/banks counters" Aether Snap ("reMOVE" — the old regex unintentionally substring-matched "move" inside "remove") \bmove\b word-boundary
tokens Precision (47%): generic (non-token) anthems no longer count Glorious Anthem. Corrects 9 fixtures whose only tokens-payoff signal was a generic anthem, + 4 downstream test fixtures (deckSynergy/suggest/card-fit/intelligent-cuts) that used Mirror Entity / a synthetic "Anthem Lord" as their worked example hasCreatureAnthem narrowed to "creature tokens you control get +…" only

Scope decisions

  • 'Modified' mechanic (equipment): NOT touched. Per the ship spec this was an open scope call — took the narrow answer, no modified-subsystem added.
  • Superfriends precision fix is partial by design. Wave-2 named four root causes ("protects walkers, loyalty growth, extra activations, tutors"). I fixed the two I could ground unambiguously in real cards (protection grants, loyalty-counter-growth double-bucketing). I deliberately left the "extra activations" / tutor-trigger boundary alone — Chain Veil, Oath of Teferi, and Carth the Lion keep their existing payoff tags, since narrowing further without the original wave-2 judged sample risked false-negativing canonical walker-count payoffs (Chain Veil is the namesake superfriends payoff). This is a smaller precision gain than a full rewrite, but it's the well-evidenced slice.
  • Sacrifice/discard/counters PRODUCERS, spellslinger payoff precision, extraCombat, and all keyword-anchored clean cohorts (energy/mill/landfall/monarch/discard/enchantress/venture) are untouched, per the DO-NOT-TOUCH list.

Test counts

  • classify.test.ts: 415 tests (was 386 in the base corpus + 27 new fixture cards, all real oracle text fetched live from api.scryfall.com/cards/named), all passing, aggregate precision/recall ≥90% gate intact.
  • Both-direction coverage per changed predicate: every precision fix has a named real negative (the audit's own FP-shaped card or an equivalent); every recall addition has a named real positive.
  • Full frontend suite: 6825 passing / 17 skipped (487 files). Full backend suite: 1049 passing (66 files, untouched by this PR — one transient auth-test failure on the first run was a shared-dev-Postgres collision, confirmed by a clean retry, unrelated to this change).
  • npm run typecheck / npm run lint (0 errors) / npm run format:check / npm run test:coverage (both packages) all green.

Deviations from the spec

  • Could not fetch a second "Affinity for artifacts" positive fixture (Frogmite) — Scryfall's API rate-limited this session's IP persistently beyond its own stated 60s cooldown window partway through research. Every other family has both-direction real-card coverage; this one branch (pre-existing, only narrowed by this PR) has a real negative (Angelic Observer) but relies on the pre-existing aggregate corpus for its positive direction rather than a dedicated new fixture.
  • One extra style: commit (prettier --write) beyond the 13 named families — git commit --amend and git rebase -i are both off-limits per standing instructions, and prettier's line-wrap/quote-style pass after the full 13-commit reconstruction touched lines across 6 different families' regions. Splitting a purely-mechanical, non-semantic reformat across families added complexity for zero benefit, so it's a single trailing commit instead.

Worktree kept at /Users/georgepapagapitos/dev/spellcontrol-e139-payoff-cleanup per the process spec. No auto-merge enabled.

Precision: "Affinity for X" cost-reduction only counts as an artifacts
payoff when X is actually artifacts — matching the bare Affinity keyword
regardless of what it's affinity FOR was reading Affinity for
Knights/Gates/Cats/etc as artifact synergy (wave-1: 7/7 FPs).

Recall: a printed (nontoken) Treasure permanent's oracle text never says
"artifact" or "Treasure" in the rules text (only the type line does), so
it fell through every text-based producer check — now caught via
type-line membership. Added the "if mana from a Treasure was spent"
payoff rider (Hired Hexblade-class).

Fixtures: Angelic Observer (Affinity for Citizens, real negative),
Hired Hexblade (Treasure-mana rider), Buried Treasure (type-line-only
Treasure, also a genuine sacrifice outlet).
Flashback/disturb/jump-start/aftermath/unearth/embalm/eternalize/encore
only ever recur or cast THEMSELVES with no dependency on anything else
being in the yard — self-contained resilience, not a graveyard-value
engine. Crediting the whole keyword list as a payoff was reading that
self-recursion as caring about the graveyard (wave-1: 9/25 FPs).

Split into GY_FUEL_KEYWORDS (delve, escape) — these genuinely want a
stocked yard, since they exile OTHER cards from it as a cost — vs the
rest, which are dropped from payoff entirely. Also widened the existing
selfReturnOnly guard to catch "put this card from your graveyard" (a
transforming DFC's own template), not just "return this card".

Fixtures: Think Twice (flashback, negative), Treasure Cruise (delve,
still a payoff), Uro (escape, still a payoff — recurs nothing but
itself yet needs a stocked yard to fuel its own recursion). Corrects
Faithless Looting, Increasing Confusion, Startled Awake, and
Chronomancer, whose graveyard payoff tag came solely from the removed
keyword branch.
… (E139)

Three FP guards on the "of the chosen type" / "shares a creature type"
payoff:

- Type-kind confusion: "of the chosen type" was unscoped, so a
  land-type or card-type selector wearing similar wording could read as
  tribal. Now bounded to co-occur with "creature(s)" within the same
  clause (a tight 25-char gap, not a bare wildcard, so an unrelated
  "creature" mention elsewhere in a long clause can't bridge to a
  DIFFERENT noun's "of the chosen type" — see the Deification fixture).
- Wrong-subject: Plague Engineer's "creatures of the chosen type your
  OPPONENTS control get -1/-1" is a hate piece, not a reward for your
  own typal deck — now excluded by an opponents-control guard.
- Inverse-payoff: Volo, Guide to Monsters rewards creatures that
  DON'T share a type (deck diversity) — the regex matched the bare
  "shares a creature type" substring regardless of the "doesn't"
  negation preceding it. Now excluded.

Fixtures: Plague Engineer (wrong-subject negative), Volo (inverse
negative) — both real oracle text, fetched live.
… (E139)

Recall: "each player mills" (Chill of Foreboding-class symmetric
decking) fell through both mill (subject list only had "each
opponent", not "each player") and graveyard (self-mill needs a bare
"you" subject) — a real card produced zero signal on either axis.
Added to the shared OPPONENT_MILL subject list; still distinct from
the `grouphug` "each player draws" wheel, which stays untouched.

Also added a trigger-on-mill payoff: "whenever a player mills a
[nonland] card" (Glowing One) rewards the mill engine running, a
shape the payoff side didn't have at all (only the doubler pattern did).

Fixtures: Chill of Foreboding (each-player mill, also cross-checks the
graveyard flashback fix from the prior commit), Glowing One
(trigger-on-mill).
…139)

Recall (wave-3: ~51/61 pool uncovered): the producer only knew the
infect/toxic keywords, missing the direct "gets N poison counter(s)"
grant that's an alternate delivery engine (Fynn, the Fangbearer;
Vraska, Betrayal's Sting's -9 ultimate). The payoff side had no
Corrupted branch at all — the March of the Machine ability word for
"an opponent has three or more poison counters", templated with an
em-dash so it survives reminder-stripping.

Fixtures: Fynn (poison-counter grant), Bonepicker Skirge (Corrupted).
Corrects Vraska, Betrayal's Sting, whose -9 grant was previously
invisible to the axis.
…(E139)

Recall (wave-3: ~67% of 681-card pool ≈ 450): the producer only knew
literal "spells you cast cost" cost-reduction, with no recursion
branch (Archaeomancer-class: "return target instant or sorcery card
from your graveyard"). The payoff had no Storm branch and no
graveyard-instant/sorcery-count scaling ("...for each instant and
sorcery card in your graveyard" — the same self-cost-reduction shape
Affinity already gets credited elsewhere).

Fixtures: Archaeomancer (recursion producer), Grapeshot (Storm),
Cryptic Serpent (graveyard-count scaling).
…cursion (E139)

Three complete-pool misses (wave-3: 6 named): "if/whenever you cycled
... this turn" delayed triggers (Spellpyre Phoenix); recursion of
cards WITH a cycling ability FROM the graveyard (Spellpyre Phoenix,
Abandoned Sarcophagus); and counting cards with a cycling ability
sitting IN the graveyard (Zenith Flare's count-damage) — distinguished
from the recursion shape by preposition (from vs in).

Fixtures: Spellpyre Phoenix (this-turn trigger + recursion, also
exercises the new spellslinger recursion producer from two commits
back), Abandoned Sarcophagus (recursion). Corrects Zenith Flare, whose
count-damage was previously invisible to the axis.
Recall (wave-3: 3 complete misses). The payoff only knew spending
energy ("Pay {E}"); "whenever you get one or more {E}" (Territorial
Gorger, Fabrication Module) rewards BANKING it instead — a distinct
trigger shape the axis had no branch for at all.

Fixtures: Territorial Gorger.
…ing (E139)

Precision (wave-2: 47%, "tutors/recursion/attach misfiled as payoff").
Re-bucketed producer-shaped text out of payoff: tutoring/recurring
Equipment DEPLOYS the engine (now a producer, mirroring how
auras/superfriends treat their own tutors), and a free-attach ETB
("whenever an Equipment enters, you may attach it") is an ENABLER, not
a reward — only an ETB that does something FOR you still counts as a
payoff.

Recall: equip-cost reduction on non-Equipment cards is the same
enabling shape as spellslinger/enchantress/auras' "X you cast cost
less" producers; Affinity for Equipment is the same "threshold/cost
payoff" shape the artifacts axis already grants Affinity for
artifacts; and "for each/equal to the number of Equipment you
control" scaling (Kemba-class) had no branch at all.

Fixtures: Armed Response (count scaling), Bureau Headmaster (equip-cost
reduction), Oxidda Finisher (Affinity for Equipment). Corrects
Stoneforge Mystic and Sigarda's Aid, whose tags move producer<->payoff.

'Modified' mechanic: explicitly NOT touched — open scope decision per
the ship spec, no modified-subsystem added.
…text (E139)

Recall (wave-3: ~39% of 71-card pool ≈ 28). The producer keyed only on
the Vehicle type line / Crew keyword — Pilot creatures (which crew the
engine without being Vehicles themselves) and crew-support body text
on non-Pilot cards ("crews Vehicles as though its power were N
greater") both fell through entirely.

Fixtures: Aeronaut Admiral, Cloudspire Captain (Pilot type). Corrects
Depala, Kotori, and Greasefang (all Pilot-typed) and Mu Yanling (also
Pilot-typed), which previously had zero vehicles producer signal.
…y-growth (E139)

Precision (wave-2: 20%, "producer-shaped text credited as payoff").
Two unambiguous root causes fixed: pure protection grants (Shalai,
Deification's "have hexproof" / "can't be the target" — the opposite
word order from the existing creature-and/or-planeswalker guard) and
loyalty-counter growth (Oath of Gideon's "enters with an additional
loyalty counter", text the producer ALREADY credits — tagging it as
payoff too was double-bucketing the same grant).

Deliberately conservative: left the "extra activations" / tutor-trigger
boundary untouched (Chain Veil, Oath of Teferi, Carth the Lion keep
their existing payoff tags) — those genuinely scale with walker count
and the wave-2 sample can't be re-verified against the original judged
cards, so narrowing further risked false-negativing canonical
walker-count payoffs without concrete evidence.

Fixtures: Shalai (protection negative, opposite word order), Deification
(protection negative, planeswalker-TYPE selector). Corrects Oath of
Gideon, whose payoff tag was pure double-bucketing.
Precision. Aether Snap ("Remove all counters from all permanents...")
was reading as a counters payoff ("moves/banks counters") and getting
trim protection it shouldn't get — the culprit was an unanchored
substring match: "move" is a substring of "reMOVE", so the bare
/move all counters/ regex fired on removal effects too. Word-boundary
fix; "moves/banks" cards (The Ozolith) still match correctly.

Fixtures: Aether Snap (the named FP card).
Precision (wave-2: 47%, "generic anthems credited without token-
specificity"). "Creature tokens you control get +…" is token-scoped;
"creatures you control get/gain/have +…" (Glorious Anthem, Craterhoof
Behemoth, Beastmaster Ascension) helps a two-creature board exactly as
much as a wide one, so it's not a go-wide signal. Narrowed
hasCreatureAnthem to the token-scoped branch only.

Fixtures: Glorious Anthem (the textbook generic-anthem negative).
Corrects nine existing fixtures whose ONLY tokens-payoff signal was a
generic (non-token) anthem: Cryptolith Rite, Craterhoof Behemoth,
Beastmaster Ascension, Mirror Entity, Angel of Invention, Elspeth
Sun's Champion, Nadaar Selfless Paladin, Triumph of the Hordes, Vito
Thorn of the Dusk Rose. Also updates four downstream test fixtures
(deckSynergy, suggest, card-fit, intelligent-cuts) that used Mirror
Entity / a generic "Anthem Lord" as their worked example of a tokens
payoff — swapped for still-valid token-scoped examples so their own
intent (payoff-starved warnings, load-bearing cut guards) stays
correctly exercised.
Purely mechanical (quote-style + line-wrap) — prettier --write output
across all 13 family commits, run once at the end so CI's format:check
passes. No semantic change; the full suite is unaffected.
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