Skip to content

fix(parser): swallow audit — key-anchor the Effect and ActivationRestriction probes#5713

Merged
matthewevans merged 1 commit into
mainfrom
ship/swallow-anchor
Jul 13, 2026
Merged

fix(parser): swallow audit — key-anchor the Effect and ActivationRestriction probes#5713
matthewevans merged 1 commit into
mainfrom
ship/swallow-anchor

Conversation

@matthewevans

Copy link
Copy Markdown
Member

Two of the 33 unanchored evidence.any::<T>() probes match a variant name shared
with another tagged enum in a way that CHANGES the detector's answer. Both are now
key-anchored (CR 601.2 / CR 602.5), pinned by differential guard tests observed RED
before green.

REGENERATED THE COLLISION MAP FIRST — the committed one was wrong by omission. It was
computed against 91 enums; the true population is 162 (tag = "type": 93 internally
tagged + 69 adjacently tagged content = "data" — a distinction the old map did not
record, and the two have DIFFERENT deserialize semantics). Same probes, recomputed: 19
colliding (site, variant) pairs across 10 sites, not 13. All 13 previously listed
reproduce; 6 were missed. A collision table is a claim about a population — if the
population is short, its ABSENCES are worthless.

ANCHORED (answer differs across the collision):

any:: CastFromZone also: AbilityCondition, ReplacementCondition
any:: RequiresCondition also: CastingRestriction (field-identical)

Effect::CastFromZone has NINE fields and every one carries #[serde(default)], so it
deserializes from the type tag ALONE — a unit variant in all but syntax. So a cast-zone
CONDITION ("if you cast this spell from your graveyard", CR 601.2a) was read as a one-shot
cast-permission EFFECT and used as proof that the card's "this turn" is intrinsic. A struct
variant is NOT automatically safe: check for a REQUIRED field before believing otherwise.

MEASURED, full 35,396-face pool. The collision surface is POPULATED: 36 CastFromZone
nodes sit under non-effect keys (condition x25, inner x9, conditions x2) and 34
RequiresCondition nodes under casting_restrictions — 70 nodes read as the WRONG enum.
Swallow delta is nevertheless 0 gains / 0 losses (export byte-identical), because both are
MASKED by an upstream text gate, not by any property of the probe: 32 of the 35
CastFromZone cards contain no "this turn" at all, and the activation gate requires every
"this turn" to sit on an "activate only" line, which a casting restriction never does.
That is luck, not safety. Anchoring makes it correct by construction at zero cost.

ANSWER-EQUIVALENT (8 sites / 17 pairs left unanchored, each with a checkable reason):

  • the turn-scope disjunction (14 pairs) asks one existential question — "any turn-scoped
    carrier?" — and EVERY variant it matches has ThisTurn in its name. A collision IS name
    identity, so a colliding node still bears a ThisTurn name and is still a turn-scoped
    carrier. The disjunction is closed under its own collision set.
  • ManaCost::SelfManaValue vs QuantityRef::SelfManaValue: both ARE a value derived from
    the card's own mana cost (CR 106.1).
  • Not: the two condition readings are already OR'd at the call site; FilterProp::Not
    and TargetFilter::Not use different field names, so those collisions are structurally
    impossible.

Anchoring these would be actively WRONG: ReplacementCondition owns YouAttackedThisTurn
and DealtDamageThisTurnBySource and has no leg in that detector, so 16 nodes across ~13
cards are seen today ONLY via the collision — right answer, wrong reason (the
ManaProduction trap from the quantity anchoring). Filed as a follow-up.

GATES (on this base): fmt clean; clippy -D warnings exit 0; 16,281 lib + 2,735 integration
pass, 0 failed; full-pool export BYTE-IDENTICAL vs origin/main — 0 gains / 0 losses over
35,396 faces.

…riction probes

Two of the 33 unanchored `evidence.any::<T>()` probes match a variant name shared
with another tagged enum in a way that CHANGES the detector's answer. Both are now
key-anchored (CR 601.2 / CR 602.5), pinned by differential guard tests observed RED
before green.

REGENERATED THE COLLISION MAP FIRST — the committed one was wrong by omission. It was
computed against 91 enums; the true population is 162 (`tag = "type"`: 93 internally
tagged + 69 adjacently tagged `content = "data"` — a distinction the old map did not
record, and the two have DIFFERENT deserialize semantics). Same probes, recomputed: 19
colliding (site, variant) pairs across 10 sites, not 13. All 13 previously listed
reproduce; 6 were missed. A collision table is a claim about a population — if the
population is short, its ABSENCES are worthless.

ANCHORED (answer differs across the collision):

  any::<Effect>                CastFromZone       also: AbilityCondition, ReplacementCondition
  any::<ActivationRestriction> RequiresCondition  also: CastingRestriction (field-identical)

`Effect::CastFromZone` has NINE fields and every one carries `#[serde(default)]`, so it
deserializes from the `type` tag ALONE — a unit variant in all but syntax. So a cast-zone
CONDITION ("if you cast this spell from your graveyard", CR 601.2a) was read as a one-shot
cast-permission EFFECT and used as proof that the card's "this turn" is intrinsic. A struct
variant is NOT automatically safe: check for a REQUIRED field before believing otherwise.

MEASURED, full 35,396-face pool. The collision surface is POPULATED: 36 `CastFromZone`
nodes sit under non-effect keys (`condition` x25, `inner` x9, `conditions` x2) and 34
`RequiresCondition` nodes under `casting_restrictions` — 70 nodes read as the WRONG enum.
Swallow delta is nevertheless 0 gains / 0 losses (export byte-identical), because both are
MASKED by an upstream text gate, not by any property of the probe: 32 of the 35
`CastFromZone` cards contain no "this turn" at all, and the activation gate requires every
"this turn" to sit on an "activate only" line, which a *casting* restriction never does.
That is luck, not safety. Anchoring makes it correct by construction at zero cost.

ANSWER-EQUIVALENT (8 sites / 17 pairs left unanchored, each with a checkable reason):
  * the turn-scope disjunction (14 pairs) asks one existential question — "any turn-scoped
    carrier?" — and EVERY variant it matches has `ThisTurn` in its name. A collision IS name
    identity, so a colliding node still bears a `ThisTurn` name and is still a turn-scoped
    carrier. The disjunction is closed under its own collision set.
  * `ManaCost::SelfManaValue` vs `QuantityRef::SelfManaValue`: both ARE a value derived from
    the card's own mana cost (CR 106.1).
  * `Not`: the two condition readings are already OR'd at the call site; `FilterProp::Not`
    and `TargetFilter::Not` use different field names, so those collisions are structurally
    impossible.

Anchoring these would be actively WRONG: `ReplacementCondition` owns `YouAttackedThisTurn`
and `DealtDamageThisTurnBySource` and has no leg in that detector, so 16 nodes across ~13
cards are seen today ONLY via the collision — right answer, wrong reason (the
`ManaProduction` trap from the quantity anchoring). Filed as a follow-up.

GATES (on this base): fmt clean; clippy -D warnings exit 0; 16,281 lib + 2,735 integration
pass, 0 failed; full-pool export BYTE-IDENTICAL vs origin/main — 0 gains / 0 losses over
35,396 faces.
@matthewevans
matthewevans enabled auto-merge July 13, 2026 03:15
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@matthewevans
matthewevans added this pull request to the merge queue Jul 13, 2026
@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

Merged via the queue into main with commit 5042f1b Jul 13, 2026
13 checks passed
@matthewevans
matthewevans deleted the ship/swallow-anchor branch July 13, 2026 03:43
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