fix(parser): swallow audit — key-anchor the Effect and ActivationRestriction probes#5713
Merged
Conversation
…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
enabled auto-merge
July 13, 2026 03:15
Contributor
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Parse changes introduced by this PR✓ No card-parse changes detected. |
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.
Two of the 33 unanchored
evidence.any::<T>()probes match a variant name sharedwith 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 internallytagged + 69 adjacently tagged
content = "data"— a distinction the old map did notrecord, 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::CastFromZonehas NINE fields and every one carries#[serde(default)], so itdeserializes from the
typetag ALONE — a unit variant in all but syntax. So a cast-zoneCONDITION ("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
CastFromZonenodes sit under non-effect keys (
conditionx25,innerx9,conditionsx2) and 34RequiresConditionnodes undercasting_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
CastFromZonecards 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):
carrier?" — and EVERY variant it matches has
ThisTurnin its name. A collision IS nameidentity, so a colliding node still bears a
ThisTurnname and is still a turn-scopedcarrier. The disjunction is closed under its own collision set.
ManaCost::SelfManaValuevsQuantityRef::SelfManaValue: both ARE a value derived fromthe card's own mana cost (CR 106.1).
Not: the two condition readings are already OR'd at the call site;FilterProp::Notand
TargetFilter::Notuse different field names, so those collisions are structurallyimpossible.
Anchoring these would be actively WRONG:
ReplacementConditionownsYouAttackedThisTurnand
DealtDamageThisTurnBySourceand has no leg in that detector, so 16 nodes across ~13cards are seen today ONLY via the collision — right answer, wrong reason (the
ManaProductiontrap 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.