fix(deck-builder): payoff-FP cleanup + measured recall widening in synergy axes (E139)#1309
Open
georgepapagapitos wants to merge 14 commits into
Open
fix(deck-builder): payoff-FP cleanup + measured recall widening in synergy axes (E139)#1309georgepapagapitos wants to merge 14 commits into
georgepapagapitos wants to merge 14 commits into
Conversation
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.
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.
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 trailingstyle:commit applies prettier formatting (mechanical only, run once at the end — see note below).AXESpredicate output forclassifyCard(), which feedstallyAxisInvestment(package boost),isUnsupportedSynergyPayoff(dependency gate), andisLoadBearing(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
Artifact — Treasure, no "artifact"/"Treasure" word in rules text), Hired Hexblade (Treasure-mana rider)affinity for artifactsrequires the literal phrase, not just the bare keywordGY_FUEL_KEYWORDS = ['delve','escape']replaces the 10-keyword list;selfReturnOnlywidened to also catch "put this card…" (DFC self-transform), not just "return"OPPONENT_MILLsubject list (also used bygraveyard's self-mill check — first-match-wins per clause, consistent with the existing pattern)\bmove\bword-boundaryhasCreatureAnthemnarrowed to "creature tokens you control get +…" onlyScope decisions
Test counts
classify.test.ts: 415 tests (was 386 in the base corpus + 27 new fixture cards, all real oracle text fetched live fromapi.scryfall.com/cards/named), all passing, aggregate precision/recall ≥90% gate intact.npm run typecheck/npm run lint(0 errors) /npm run format:check/npm run test:coverage(both packages) all green.Deviations from the spec
style:commit (prettier--write) beyond the 13 named families —git commit --amendandgit rebase -iare 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-cleanupper the process spec. No auto-merge enabled.