You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TargetSpec's permanent arms become the shared permanent filter, so a card that targets a
class nobody has targeted before needs no Rust — and the English the server poses for that
slot is generated from the filter rather than hand-written per phrasing.
Context and evidence
Parent: #819, §2.1. Second half; #824 does the non-targeting selectors and introduces the
filter this consumes.
TargetSpec (ability/target.rs:196) has ~29 variants. Counting the target / dealer / dealt_to values actually used across the 299 catalog files: 20 of the 36 distinct
selectors in the catalog are used by exactly one card. On the targeting side that includes
AnyArtifactEnchantmentOrCreatureWithFlying — Vivien Reid's −3, and nothing else
Nearly all of them are a product of axes the engine already models: controller relation
(you / opponent / any / another), card type, a keyword read through the computed set,
a combat or tap state, colour or colourlessness, and mana value. That is #824's PermanentFilter plus a mana-value bound.
Cost of one new variant today — every consuming match is wildcard-free, which is the design
working as intended and also the bill:
sage-server/src/view/prompt.rs — the posed English
29
sage-server/src/rules_text/words.rs
86
plus arms in card/attachment.rs, card/helpers.rs, ability/effect/targets.rs, and catalog/effects.rs. Two crates, six exhaustive matches, per phrasing.
This does not change the wire contract.TargetRequirement (sage-protocol/src/action.rs)
carries prompt: String and a candidate list; the enum never crosses the socket. What it does
change is where that string comes from. Today prompt.rs:163 is a table of hand-written
phrases — "Choose target player", "Choose target nonland permanent" — one per variant.
Generating an equally natural phrase from a filter is the genuinely hard half of this issue and
is the reason it is separated from #824, not the type plumbing.
exclusions.json names one limit that is a limit of this shape: "Target specs that name a
blocker, or the planeswalker a source is attacking." The blocking half is one field.
Scope
TargetSpec's permanent-selecting arms replaced by one arm carrying the shared PermanentFilter, extended with the axes only targeting needs (mana value, attacking,
tapped, another relative to the source).
The non-permanent arms — players, player-or-planeswalker, spells on the stack, cards in a
graveyard, AnyTarget — stay named. They select different kinds of object and a filter over
permanents cannot say them.
One legality predicate and one candidate enumerator, both reading the filter, replacing the
parallel matches in resolve.rs and actions/targeting.rs.
Prompt text and rules text generated from the filter, in the server, with the phrasing
quality of the current hand-written table as the bar.
Catalog JSON re-authored. No card's rules change.
The validator keeps refusing filters that cannot be satisfied or that pair with a payload
that means nothing.
Acceptance criteria
TargetSpec has one permanent arm carrying a filter; the ~24 named permanent variants
are gone.
Legality and candidate enumeration share one predicate — a permanent is a legal target
exactly when it is a candidate.
Every prompt string the server poses today is reproduced or improved. A snapshot test
over every targeting card in the catalog, reviewed as English, is the evidence.
Every rules-text rendering is reproduced or improved, under the same review.
A target spec naming a blocking creature is authorable, and the exclusions.json
entry naming that limit is narrowed to the part still true.
Adding a card that targets, say, an untapped enchantment an opponent controls requires no
Rust change.
Hexproof, the CR 608.2b resolution re-check, and up to N slot counts behave exactly as
before — covered by the existing suites, unchanged.
make compat regenerated; make verify green, including make e2e-smoke.
Test and validation evidence
The existing behavioural suites are the correctness net: this is behaviour-preserving.
A generated-prompt snapshot across every targeting definition in the catalog, checked in and
reviewed, so a phrasing regression is a diff rather than a discovery in play.
The same for rules text.
make e2e-smoke and make e2e-views — the prompt is the surface a player reads, and AGENTS.md makes the browser tier the gate for exactly this kind of seam.
make verify.
Dependencies and sequencing
Blocked by #824, which introduces the filter. Should follow #820 and #822 so the catalog
being re-authored is correct. Independent of #826/#827 in subject matter but overlapping in
files — sequence rather than parallelise.
Estimated scope
Large — broad change requiring a split assessment. Already split from #824; splitting further
would mean landing a state where legality and candidates read different predicates, which is
worse than the size.
Risks and constraints
The posed English is the risk, not the types. A generated phrase that reads worse than
the hand-written one is a real regression in a client AGENTS.md describes as "a dark table,
not a form", and no automated check can judge it. Per the hard rule, say plainly what could
not be verified and leave the judgment to the maintainer.
A filter with an absent field means "any". A named variant re-authored with a field forgotten
silently widens what a spell may target — the failure mode is a legal target that should not
be, which the existing tests will not all catch.
The client computes no legality (hard rule) and must keep computing none: the candidate list
stays server-authoritative and exhaustive.
Non-goals
The non-permanent target kinds. CardFilter and the graveyard selectors. The effect-verb
matrix (#826, #827).
Documentation impact
docs/card-schema.md — the target vocabulary and "How far the vocabulary reaches". docs/decisions/0004-targeting-model.md — the enumeration contract is unchanged but the
selector it enumerates from is not; check whether the ADR states the shape. exclusions.json — one entry narrowed. docs/protocol.md only if a prompt's shape changes,
which it should not.
Goal
TargetSpec's permanent arms become the shared permanent filter, so a card that targets aclass nobody has targeted before needs no Rust — and the English the server poses for that
slot is generated from the filter rather than hand-written per phrasing.
Context and evidence
Parent: #819, §2.1. Second half; #824 does the non-targeting selectors and introduces the
filter this consumes.
TargetSpec(ability/target.rs:196) has ~29 variants. Counting thetarget/dealer/dealt_tovalues actually used across the 299 catalog files: 20 of the 36 distinctselectors in the catalog are used by exactly one card. On the targeting side that includes
AnyArtifactEnchantmentOrCreatureWithFlying— Vivien Reid's −3, and nothing elseAnyColorlessCreature— Infernal ReckoningAnyTappedCreature— Take VengeanceAnyCreatureDefendingPlayerControls— Star-Crowned StagAnyPermanentWithManaValue— IsolateAnyCreatureWithFlying— PlummetAnyArtifactCreatureYouControl— Aethershield ArtificerAnotherAttackingCreature— Pegasus CourserAnyCreatureOrPlaneswalker— Nicol BolasAnyLand— Tectonic RiftNearly all of them are a product of axes the engine already models: controller relation
(
you/opponent/any/another), card type, a keyword read through the computed set,a combat or tap state, colour or colourlessness, and mana value. That is #824's
PermanentFilterplus a mana-value bound.Cost of one new variant today — every consuming match is wildcard-free, which is the design
working as intended and also the bill:
sage-engine/src/resolve.rs— legality (CR 601.2c / 608.2b)sage-engine/src/actions/targeting.rs— candidate enumerationsage-server/src/view/prompt.rs— the posed Englishsage-server/src/rules_text/words.rsplus arms in
card/attachment.rs,card/helpers.rs,ability/effect/targets.rs, andcatalog/effects.rs. Two crates, six exhaustive matches, per phrasing.This does not change the wire contract.
TargetRequirement(sage-protocol/src/action.rs)carries
prompt: Stringand a candidate list; the enum never crosses the socket. What it doeschange is where that string comes from. Today
prompt.rs:163is a table of hand-writtenphrases —
"Choose target player","Choose target nonland permanent"— one per variant.Generating an equally natural phrase from a filter is the genuinely hard half of this issue and
is the reason it is separated from #824, not the type plumbing.
exclusions.jsonnames one limit that is a limit of this shape: "Target specs that name ablocker, or the planeswalker a source is attacking." The blocking half is one field.
Scope
TargetSpec's permanent-selecting arms replaced by one arm carrying the sharedPermanentFilter, extended with the axes only targeting needs (mana value, attacking,tapped,
anotherrelative to the source).graveyard,
AnyTarget— stay named. They select different kinds of object and a filter overpermanents cannot say them.
parallel matches in
resolve.rsandactions/targeting.rs.quality of the current hand-written table as the bar.
that means nothing.
Acceptance criteria
TargetSpechas one permanent arm carrying a filter; the ~24 named permanent variantsare gone.
exactly when it is a candidate.
over every targeting card in the catalog, reviewed as English, is the evidence.
exclusions.jsonentry naming that limit is narrowed to the part still true.
Rust change.
up to Nslot counts behave exactly asbefore — covered by the existing suites, unchanged.
make compatregenerated;make verifygreen, includingmake e2e-smoke.Test and validation evidence
reviewed, so a phrasing regression is a diff rather than a discovery in play.
make e2e-smokeandmake e2e-views— the prompt is the surface a player reads, andAGENTS.mdmakes the browser tier the gate for exactly this kind of seam.make verify.Dependencies and sequencing
Blocked by #824, which introduces the filter. Should follow #820 and #822 so the catalog
being re-authored is correct. Independent of #826/#827 in subject matter but overlapping in
files — sequence rather than parallelise.
Estimated scope
Large — broad change requiring a split assessment. Already split from #824; splitting further
would mean landing a state where legality and candidates read different predicates, which is
worse than the size.
Risks and constraints
the hand-written one is a real regression in a client
AGENTS.mddescribes as "a dark table,not a form", and no automated check can judge it. Per the hard rule, say plainly what could
not be verified and leave the judgment to the maintainer.
silently widens what a spell may target — the failure mode is a legal target that should not
be, which the existing tests will not all catch.
stays server-authoritative and exhaustive.
Non-goals
The non-permanent target kinds.
CardFilterand the graveyard selectors. The effect-verbmatrix (#826, #827).
Documentation impact
docs/card-schema.md— the target vocabulary and "How far the vocabulary reaches".docs/decisions/0004-targeting-model.md— the enumeration contract is unchanged but theselector it enumerates from is not; check whether the ADR states the shape.
exclusions.json— one entry narrowed.docs/protocol.mdonly if a prompt's shape changes,which it should not.