ship/restriction grammar - #5719
Conversation
matthewevans
commented
Jul 13, 2026
- refactor(parser): make parse_inner_condition the primary restriction grammar
- test(parser): pin the restriction-only fallback and prove the commit is atomic
…grammar
CR 601.3 / CR 602.5: a casting/activation restriction is an ordinary game-state
condition, so it must be recognized by the same combinators that recognize the
identical phrase in an "as long as" static. Invert the ordering:
parse_restriction_condition now runs all_consuming(parse_inner_condition) FIRST
and converts, and the bespoke restriction grammar is a narrow fallback.
- Tri-state SharedRestrictionParse {NoMatch, Converted, Unsupported}. A phrase the
shared grammar UNDERSTOOD but ParsedCondition cannot hold exactly fails the parse
outright; it is never re-read by the fallback, which would produce a weaker
reading of the same words.
- static_condition_to_restriction_condition is exhaustive over all 57 StaticCondition
variants with no wildcard arm, so a new variant is a compile error rather than a
silently permissive restriction. Counter bands are rejected, never widened into an
"at least" that drops the maximum (CR 122.1 / CR 711.2a).
- Compound conditions move into parse_inner_condition as one parameterized connective
combinator; the legacy " and "/" or " raw-string splitting is deleted. A string split
cannot see a connector sitting INSIDE an atomic leaf; requiring both sides to parse
as complete conditions is what makes the decomposition safe. n-ary chains nest
instead of leaving an unconsumed — and silently swallowed — tail.
- CR 903.3d: "you control a commander" converts to an ObjectCount over the IsCommander
filter. It previously became subtype "commander" — a subtype no permanent has — so
Deflecting Swat's free-cast condition could never be satisfied.
- An unparsed condition must never be stored as RequiresCondition { condition: None }.
That evaluates permissively (Option::is_none_or -> true) AFTER consuming the source
clause, so the ability reported as fully supported while being activatable in exactly
the situations the card forbids. All six activation sites now route through one
atomic commit_requires_condition authority that mutates nothing on failure, leaving
the clause for the ordinary Effect::Unimplemented fallback.
Full-pool dual export (population 35,396 faces): RequiresCondition{condition: null}
goes 50 occurrences / 48 faces -> 0. 42 faces become honestly unsupported: 35 were
that permissive lie (no restriction enforced at all), 3 were confidently-wrong parses
(a bare-subtype dump that could never be satisfied, a QuantityVsEachOpponent comparing
a value to itself, an "attacking or blocking" narrowed to "attacking"), and 4 are true
regressions in two named classes tracked as follow-ups. 1 gain, 15 dead parsers
deleted (-611 lines net).
…is atomic
Enforcement for the P02-U3 ruling. Both gates were watched failing before being
accepted; reverting the commit order reproduces the original defect verbatim as
[RequiresCondition { condition: None }, OnlyOnceEachTurn].
- restriction_only_fallback_dispatches_exactly_the_pinned_families: a STRUCTURAL pin,
not prose. The fallback's doc comment tells contributors not to add a phrase there;
a comment stops nobody. This reads the module's own source, extracts the dispatcher's
body, and rejects any parse_*(text) call outside the pinned six — each tagged with WHY
it is retained (restriction-context referent / vocabulary gap / phrasing gap pending
P02-U3b). Adding a seventh arm turns it red with the question the author must answer:
is the new phrase referentially special, genuinely inexpressible, or merely spelled
differently? Only the first two belong there.
- failed_activation_condition_commits_no_cadence_and_leaves_the_clause_unimplemented:
the witness for commit_requires_condition. An activation gate whose condition fails to
parse must commit NOTHING — not the cadence restriction sharing the sentence, not a
permissive RequiresCondition { condition: None }, and not the source text, which must
survive as Effect::Unimplemented.
- parsed_activation_condition_commits_both_cadence_and_condition: the positive
reach-guard, so the atomicity test cannot pass merely because the peeling branch never
ran. It stays green under the revert while the atomicity test goes red.
The source-scanning `.find` calls carry allow-noncombinator annotations: they parse this
module's own bytes, not Oracle text.
Test-only; parse output unchanged.
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Two PlayerFilter imports lost their last user when #5677's HasCounters arm replaced the bespoke lowering, and the describe() test helper lost its callers when SharedRestrictionParse::Unsupported dropped its payload. Local 'cargo test' passes with warnings; CI's -D warnings does not.
Parse changes introduced by this PR · 56 card(s), 26 signature(s) (baseline: main
|
… parity rows it exposes (CR 603.3b + CR 603.4) (phase-rs#5732) `integration_cards.json` is a cached subset of the card-data export, but it was last FULLY regenerated at b9685cc (phase-rs#5695). Nineteen parser/types PRs merged since then; every fixture touch in between was surgical (phase-rs#5672 +1, phase-rs#5679 +2, phase-rs#5727 2 entries), so the parse values silently drifted. Regenerated from the export at 8c35dc5 (oracle-gen, MTGJSON 5.3.0+20260629). Population (json deep-equality, not line counts — the file is one line): committed 2658 entries -> 2726. 70 added, 2 removed, 44 changed values. Attribution of the 44 changed (causal: exports built at phase-rs#5720 / phase-rs#5717 / phase-rs#5730 and compared, NOT shape-guessing): phase-rs#5723 (P02-U3b shared condition grammar) ...... 3 archive trap, temple of civilization, thaumaton torpedo (all gained the comparator/lhs/rhs/qty/scope condition shape) phase-rs#5721 + phase-rs#5719 (where-X quantity channel + ..... 21 restriction grammar; both merged BEFORE phase-rs#5717 — merge order != PR-number order) bellowsbreath ogre, cryptex, deadly rollick, deflecting swat, desert, dread wanderer, esquire of the king, flesh, fraying sanity, gloomlake verge, great desert hellion, gutterbones, officious interrogation, once upon a time, potioner's trove, ribald shanty, rock jockey, second little pig, shifting woodland, snuff out, starport security phase-rs#5695..phase-rs#5720 no-regen window (bloc) ........... 20 Stale already at phase-rs#5720, so attributable to the 19-PR window above the b9685cc anchor, not to any single PR: alrund god of the cosmos, animal friend, approach of the second sun, cavernous maw, fblthp the lost, from father to son, hour of revelation, increasing vengeance, jodah the unifier, mana reflection, misty salon, puca's eye, ram through, reidane god of the worthy, secrets of the key, sevinne's reclamation, temple of the dead, the dining car, unleash the flux, valgavoth terror eater The 70 added keys are new test-source card references the generator collects; phase-rs#5729 (tests-only) contributed zero parse delta, as expected. Corrected premise: 44 entries are truly stale, not 7. Six of the seven originally reported reproduce; `osteomancer adept` is NOT stale (committed == fresh). The regen turns `ordering_parity_sweep` red, so the gate's evidence rows ship ATOMICALLY with it. Both rows are population entries, not ordering regressions: the sweep skips Unimplemented-bearing triggers, so a card only enters it once its parse binds. great desert hellion -> BATCH_GENUINE_ROWS. Its LTB Draw was Unimplemented until phase-rs#5721/phase-rs#5719 bound Intensity{Source}. Each co-departing Hellion draws off its OWN intensity but discards the SHARED hand, so the second trigger discards the cards the first just drew: with intensities a != b the final hand, graveyard and library differ by order. The members are not identical functions, so commutation genuinely fails and the new prompt is the CR 603.3b choice the legacy serde walk wrongly auto-ordered (CR 603.5: each "may" is chosen on resolution). planar collapse -> DOCUMENTED_OVER_PROMPT (L8-held family). New fixture key. Upkeep ObjectCount(Creature) >= 4 intervening-if x DestroyAll + self-Sacrifice: the first copy's sweep drives the census to 0, so the sibling's CR 603.4 re-check is false and it does nothing. Monotone and self-limiting — identical siblings commute up to relabeling, so the prompt is conservative, fail-closed and rules-correct. Neither row weakens the gate: both are direction-gated over-prompts (an under-prompt is never suppressible), and both are consumed by the ledger's exact-set asserts (over_prompt_hit 18->19, batch_genuine_hit 1->2), so a misclassification still trips the STRICT PROOF-GATE. Verification: engine lib 16481/16481 pass (was 16480 + 1 red); integration 2929/2929 pass. Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>