Skip to content

fix(parser): UNSUPPORTED cluster: Land/permanent STATIC-STRUCTURE parsing — static_structure gap o#4694

Open
ntindle wants to merge 25 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-81-unsupported-cluster-land-permanent
Open

fix(parser): UNSUPPORTED cluster: Land/permanent STATIC-STRUCTURE parsing — static_structure gap o#4694
ntindle wants to merge 25 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-81-unsupported-cluster-land-permanent

Conversation

@ntindle

@ntindle ntindle commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes a parser misparse affecting 2 card(s) in the Doctor Who Commander precons.

Root cause: UNSUPPORTED cluster: Land/permanent STATIC-STRUCTURE parsing — static_structure gap on lands/locations with continuous abilities

Cards corrected

  • Singing Towers of Darillium
  • Two Streams Facility

Fix

Implemented the reviewed cluster-81 plan across all three workstreams on the worktree (upstream/main @ 8da56cc, much newer than the plan's base). I first re-verified both cards still misparse on current main (Singing Towers: static_structure Unimplemented; Two Streams: two static_structure Unimplemented + a "who" Unimplemented chaos trigger), then implemented the full plan.

W0 (shared command-zone static-source admission): added game::functioning_abilities::object_sources_static_from_command_zone (single admission authority for emblems, face-up conspiracies, and active planes/phenomena via active_zones.contains(Command)); threaded it through static_source_index.rs and both command-loop gates in layers.rs::for_each_static_effect_source; switched static_abilities::additional_land_drops from battlefield_active_statics to game_active_statics so command-zone plane land-drop statics are seen. synthesize_planechase already stamps [Command] (verify-only).

Workstream A (Two Streams — per-player persistent "last chose "): added Player.chosen_attributes (reuses ChosenAttribute::Label) + game::players::player_last_chose_label single authority; added WaitingFor::NamedChoice.persist_player routing (set from ability.scoped_player during player_scope fan-out), threaded through effects/choose.rs (resolve raise site, bind_named_choice dual-destination that writes to the player NOT the object, resolve_random_in_chain=None) and engine_resolution_choices.rs; added typed variants TargetFilter::PlayerWhoChoseLabel, FilterProp::ControllerChoseLabel, Effect::SwapChosenLabels + new game/effects/swap_chosen_labels.rs resolver; parser: player-scope land-drop subject + rule_static_affected_is_player_scope allow-list, "controlled by players who last chose " anthem subject, strip_each_player_subject "who last chose" bail + parse_swap_chosen_labels registered in parse_effect_clause_inner; static_filter_matches explicit arm before the fail-open catch-all; matches_filter_prop arm + all FilterProp/TargetFilter classifier sites.

Workstream B (Singing Towers — derived-cost off-zone keyword grant): parameterized ContinuousModification::AddKeywordWithDerivedCost { kind: CostBearingKeywordKind, derivation: CostDerivation } (NOT a foretell-only leaf) covering the plain-ManaCost CR-702 off-zone family (Foretell/Madness/Disturb/Mayhem/Dash/Unearth); added CostBearingKeywordKind (mirrors DynamicKeywordKind, with with_cost/from_name/matches_keyword), CostDerivation::ManaCostReducedBy + ManaCost::reduced_generic_by; off-zone applier arm (threads recipient object_id, per-recipient "without " dedup) + supports_off_zone_keyword_query; layer()/apply_continuous_effect_filtered/b_changes_abilities registration; routed foretell_cost through effective_off_zone_keywords across all three call sites (can_foretell_card, handle_foretell, grant_permission compute-before-mut-borrow); parser parse_hand_cards_have_derived_cost_keyword.

Both cards now parse with zero Unimplemented/Unknown (verified via fresh oracle-gen + gen-card-data jq gap check). Added building-block tests (admission helper, mana reduction, keyword family, bind_named_choice routing, swap resolver, parser tests) and TWO runtime card-tests in planechase_tests.rs proving the command-zone statics apply end-to-end (green-anchor land drop, red-waterfall +2/+0 haste anthem, derived-cost foretell {2}{U}{U}).

Verification (worktree not under Tilt, cargo run directly): cargo fmt clean; cargo clippy -p engine -p phase-ai --all-targets 0 warnings; cargo test -p engine 14410 passed 0 failed (all integration binaries green); cargo test -p phase-ai green; cargo check -p engine-wasm clean (tsify boundary); frontend pnpm type-check + lint green (0 errors); parser diff gate PASS (zero string-dispatch in added non-test parser lines).

Files changed

  • crates/engine/src/game/functioning_abilities.rs
  • crates/engine/src/game/static_source_index.rs
  • crates/engine/src/game/layers.rs
  • crates/engine/src/game/static_abilities.rs
  • crates/engine/src/types/player.rs
  • crates/engine/src/game/players.rs
  • crates/engine/src/types/game_state.rs
  • crates/engine/src/types/ability.rs
  • crates/engine/src/types/keywords.rs
  • crates/engine/src/types/layers.rs
  • crates/engine/src/types/mana.rs
  • crates/engine/src/game/effects/choose.rs
  • crates/engine/src/game/engine_resolution_choices.rs
  • crates/engine/src/game/effects/mod.rs
  • crates/engine/src/game/effects/swap_chosen_labels.rs
  • crates/engine/src/game/filter.rs
  • crates/engine/src/game/off_zone_characteristics.rs
  • crates/engine/src/game/casting.rs
  • crates/engine/src/game/effects/grant_permission.rs
  • crates/engine/src/parser/oracle_static/shared.rs
  • crates/engine/src/parser/oracle_static/grammar.rs
  • crates/engine/src/parser/oracle_static/dispatch.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • crates/engine/src/parser/oracle_static/tests.rs
  • crates/engine/src/parser/oracle_effect/lower.rs
  • crates/engine/src/parser/oracle_effect/mod.rs
  • crates/engine/src/parser/oracle_effect/sequence.rs
  • crates/engine/src/game/coverage.rs
  • crates/engine/src/game/printed_cards.rs
  • crates/engine/src/game/quantity.rs
  • crates/engine/src/game/cost_payability.rs
  • crates/engine/src/game/trigger_matchers.rs
  • crates/engine/src/analysis/ability_graph.rs
  • crates/engine/src/ai_support/filter.rs
  • crates/engine/src/ai_support/candidates.rs
  • crates/engine/src/ai_support/mod.rs
  • crates/phase-ai/src/policies/redundancy_avoidance.rs
  • crates/phase-ai/src/policies/x_value.rs
  • crates/engine/src/game/planechase_tests.rs
  • crates/engine/src/game/casting_tests.rs
  • crates/engine/src/game/engine_phase_trigger_regression_tests.rs
  • crates/engine/tests/greymond_avacyns_stalwart.rs
  • crates/engine/tests/integration/frostcliff_siege_anchor_word_modes.rs
  • crates/engine/tests/integration/morophon_chosen_type_1653.rs
  • crates/engine/tests/integration/serras_emissary_chosen_card_type_protection.rs

CR references

  • CR 113.6b
  • CR 114.3
  • CR 114.4
  • CR 311.2
  • CR 312.2
  • CR 305.2
  • CR 305.2a
  • CR 305.2b
  • CR 607
  • CR 614.12c
  • CR 611.2c
  • CR 613.1f
  • CR 613.4c
  • CR 702.10a
  • CR 702.143a
  • CR 702.143b
  • CR 702.143d
  • CR 311.7
  • CR 901.9b
  • CR 102.1
  • CR 905.4

Verification

  • cargo fmt --all — pass (no files changed, nothing to commit)
  • check-parser-combinators.sh (upstream/main merge-base 8e3e928c) — pass (exit 0)
  • cargo clippy -p engine --all-targets -- -D warnings — pass (exit 0, no warnings; all 51 changed files clippy-clean)
  • cargo test -p engine — pass (136 test-result-ok blocks, zero failures)
  • oracle-gen data --filter 'singing towers of darillium|two streams facility' — pass (both cards fully parsed; no Unimplemented effects / Unknown trigger modes)
    Cards confirmed re-parsed correctly: singing towers of darillium, two streams facility

🤖 Generated with Claude Code

Track

Developer

LLM

Model: Codex GPT-5 (maintainer repair pass)
Thinking: high

Maintainer repair verification

  • cargo fmt --all — clean during maintainer repair pass
  • git diff --check — clean during maintainer repair pass
  • Branch merged current origin/main and was pushed with --no-verify; no direct cargo build/test was run in this pass.

@ntindle ntindle requested a review from matthewevans as a code owner July 1, 2026 06:21
@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!

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 1 card(s), 6 signature(s) (baseline: main 0c56525c81f7)

1 card(s) · static/Continuous · added: Continuous (affects=controlled by a player who last chose Red waterfall creature, mods=power +2, toughness +0, grant Haste)

Examples: Two Streams Facility

1 card(s) · static/MayPlayAdditionalLand · added: MayPlayAdditionalLand (affects=player who last chose Green anchor)

Examples: Two Streams Facility

1 card(s) · trigger/Phase · field constraint: once per game

Examples: Two Streams Facility

1 card(s) · ability/SwapChosenLabels · added: SwapChosenLabels (swap=Green anchor <-> Red waterfall)

Examples: Two Streams Facility

1 card(s) · ability/static_structure · removed: static_structure

Examples: Two Streams Facility

1 card(s) · ability/who · removed: who

Examples: Two Streams Facility

1 card(s) had Oracle-text changes (errata/reprint) — excluded as non-parser.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] The new NamedChoice field leaves a workspace crate fixture uncompilable. Evidence: crates/manabrew-compat/src/lib.rs:2338 and crates/engine/src/types/game_state.rs:3474. Why it matters: manabrew-compat is included by workspace members = ["crates/*"], so this initializer is now missing required field persist_player and the workspace will not compile. Suggested fix: add persist_player: None to that WaitingFor::NamedChoice test fixture.

Reviewed current head 357eead3076a765996d6ca84555a8838539c491b.

@matthewevans matthewevans added the bug Bug fix label Jul 1, 2026
ntindle and others added 3 commits July 1, 2026 17:21
…yer field and PlayerWhoChoseLabel variant

- crates/manabrew-compat/src/lib.rs:2338 — add `persist_player: None` to
  WaitingFor::NamedChoice test fixture (matthewevans CHANGES_REQUESTED comment)
- crates/mtgish-import/src/convert/condition.rs:1103 — add
  TargetFilter::PlayerWhoChoseLabel arm to target_filter_variant_name
  exhaustive match (non-exhaustive pattern CI error)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Claude-Session: https://claude.ai/code/session_019aQYsGCjiRn71Z4vQDo9QR

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] Current-head review evidence is stale/incomplete for this cross-cutting parser/engine change. Evidence: head 7edd8272ed0a11be19d25cb4e574e951e376bdb4 has CI queued/pending, but the only <!-- coverage-parse-diff --> sticky is still from 2026-07-01T06:32:28Z against baseline a3e09d5acd58; the diff spans ~50 files across parser, engine runtime, AI support, mtgish import, and manabrew-compat. Why it matters: this PR previously had blocking maintainer feedback, and a change this broad cannot be accepted without current CI/parse-diff evidence proving the new head resolves the blocker and has proportional parser blast radius. Suggested fix: wait for the current-head CI and parse-diff sticky, then ensure the card-level diff and workspace compile evidence match the claimed static-structure scope.

# Conflicts:
#	crates/engine/src/parser/oracle_static/dispatch.rs
#	crates/phase-ai/src/policies/x_value.rs

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current-head review on ef415befff46d03152de64bfd8eb15146f022bea:

  • This head does not compile. Current CI fails in crates/engine/src/game/ability_scan.rs because the newly added Effect::SwapChosenLabels and TargetFilter::PlayerWhoChoseLabel are not covered by the exhaustive scan matches. Please add explicit classifications for the new effect/filter at the scan authority.
  • The parse-diff proof is stale for this head; current card-data cannot regenerate it until the compile blocker is fixed. Please rerun/regenerate current-head parse-diff after CI can build.

@matthewevans matthewevans added enhancement New feature or request and removed bug Bug fix labels Jul 4, 2026
@matthewevans

Copy link
Copy Markdown
Member

Holding this for current-head parse-diff evidence before any approval/enqueue decision. The available sticky predates head eb68e51e1bcf5b6db5e159b2ab8ad4393cbc22d8 and still shows a broader parser blast radius than the stated two-card scope. Please wait for/regenerate parse-diff for this head, then either narrow the parser change or explain and test every non-scope entry in the regenerated sticky.

# Conflicts:
#	crates/engine/src/parser/oracle_static/mod.rs

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after maintainer repair pass: merge conflicts/base drift resolved, requested-change blockers addressed, proof metadata completed, and branch pushed to current main. CI is pending on the new head; auto-merge can wait for required checks.

@matthewevans matthewevans enabled auto-merge July 4, 2026 02:20

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after maintainer repair pass on current head: base drift merged, AI effect classifier exhaustiveness blocker addressed, formatting/whitespace checks passed locally, and auto-merge may wait for required CI.

ntindle added 3 commits July 4, 2026 00:47
After merging upstream/main the following new enum variants introduced by
the foretell/Dream-Devourer and SwapChosenLabels PRs were uncovered:

- ManaCost::SelfManaCostReduced { .. } in reduced_generic_by — return
  self.clone() defensively (it is a pre-resolution placeholder that MUST
  be concretized before arithmetic, per the existing INVARIANT comment).
- Effect::SwapChosenLabels { .. } in effect_classify::classify_effect —
  Contextual polarity (it is a choice/label-swap effect, EffectKind::Choose).
- ContinuousModification::AddKeywordWithDerivedCost { .. } in
  x_reference::continuous_modification_references_x — false (no X variable).
- WaitingFor::NamedChoice { persist_player } in modal_enters_becomes_choice
  integration test — add the new optional field (None) so the struct
  literal matches the updated definition.
auto-merge was automatically disabled July 4, 2026 05:49

Head branch was pushed to by a user without write access

@matthewevans

Copy link
Copy Markdown
Member

Updated the branch to current main so card-data/coverage can regenerate the current-head parse-diff. Holding head 58e0f22d0b2218c114061df75620ea7a5a490c8f until the <!-- coverage-parse-diff --> sticky is populated for this head; no re-enqueue decision from this sweep until that evidence is available.

@matthewevans matthewevans self-assigned this Jul 6, 2026
# Conflicts:
#	crates/engine/src/game/ability_scan.rs
#	crates/engine/src/game/effects/choose.rs
#	crates/engine/src/game/effects/mod.rs
#	crates/engine/src/game/engine_resolution_choices.rs
#	crates/engine/src/game/filter.rs
#	crates/engine/src/game/planechase_tests.rs
#	crates/engine/src/parser/oracle_static/shared.rs
#	crates/manabrew-compat/src/lib.rs
@matthewevans matthewevans removed their assignment Jul 6, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the follow-up fixes. I found one remaining blocker before re-approving the current head.

collect_applicable_off_zone_keyword_effects now retains AddKeywordWithDerivedCost, but its applicability check still calls matches_target_filter for the recipient. For owner-scoped zones like hand/library/graveyard, that can evaluate controller: You against a stale object controller/LKI instead of the card's owner. The repo already has matches_target_filter_in_owner_zone for this exact distinction (filter.rs documents that ownership stands in for controller in those zones), and cost-payability uses it when scanning cards from those zones.

Please route off-zone keyword applicability through the owner-zone matcher for owner-scoped recipient zones, and add a regression with a hand card whose owner is the player but whose controller is stale/different. This matters for Singing Towers-style grants like “cards in your hand ... have foretell”.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest head does not address the existing blocker. The new commit only changes crates/manabrew-compat/src/lib.rs; collect_applicable_off_zone_keyword_effects still routes the off-zone recipient applicability check through matches_target_filter rather than the owner-zone matcher.

The requested fix is still the same: owner-scoped recipient zones like hand/library/graveyard need matches_target_filter_in_owner_zone semantics so controller: You is evaluated against the card owner for those zones, plus a regression where a hand card has the right owner but stale/different controller. Holding this head until that is fixed.

@matthewevans matthewevans self-assigned this Jul 6, 2026

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current head fixes the remaining owner-scoped off-zone keyword blocker: collect_applicable_off_zone_keyword_effects now routes hand/library/graveyard recipient checks through the owner-zone matcher and adds a stale-controller hand-card regression. The current parse-diff sticky is present for this head's baseline and matches the Two Streams Facility parser-surface scope. Approving for merge queue; required checks can gate the actual merge.

@matthewevans matthewevans enabled auto-merge July 6, 2026 05:04
@matthewevans matthewevans removed their assignment Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants