fix(parser): parse Gilgamesh dig-from-among and active-voice reflexive attach#3743
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces parsing support for active-voice reflexive trigger conditions (e.g., 'when you put one or more [type] onto the battlefield this way') and refactors the 'dig from among' parser to avoid misrouting. The review feedback points out two violations of the repository's hard architectural rules: using a substring scan (scan_contains) for parser dispatch in sequence.rs (violating R1), and using verbatim string comparison (prefix == "when ") alongside a missing CR annotation in conditions.rs (violating R6).
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // Must be checked BEFORE the "from among" path for bare "of them" forms, but MUST NOT | ||
| // fire when "from among them/those cards" is present — that substring also contains | ||
| // " of them" and would mis-route (Gilgamesh, Collected Company). | ||
| if !nom_primitives::scan_contains(lower, "from among") { |
There was a problem hiding this comment.
Violation of R1: Substring scan used for parser dispatch. Using scan_contains for parser dispatch violates the hard architectural rule R1, which mandates using nom combinators on the first pass. Additionally, compound phrases like 'from among' should be decomposed into modular, reusable parsers for constituent parts rather than matched as a single verbatim string.
Suggested fix: Use a composed nom parser to check for 'from' followed by 'among' in a robust way.
let mut parser = tuple((tag("from"), space1, tag("among")));References
- R1. Nom combinators on the first pass — no exceptions. Every new parser dispatch under crates/engine/src/parser/ must use nom 8.0 combinators or delegate to existing helpers. Findings: any new .contains("...") used for parsing dispatch in non-test parser code. (link)
- Avoid verbatim string equality for parsing Oracle phrases as it bypasses the robust nom-based parser and creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts (e.g., subjects, conjunctions) and compose them using idiomatic combinator aggregates (like nested
altandtagsequences) to prevent combinatorial explosion and improve maintainability.
| if prefix == "when " { | ||
| if let Ok((after_clause, (filter, _negated))) = |
There was a problem hiding this comment.
Violation of R6: Missing CR annotation. Every rules-touching parser implementation must carry a verified CR annotation. Additionally, avoid verbatim string equality like prefix == "when " for parsing Oracle phrases as it bypasses the robust nom-based parser. Instead, use modular, reusable parsers for constituent parts.
Suggested fix: Replace the verbatim string check with a nom-based parser (e.g., using tag) and add the CR 603.12 annotation.
// CR 603.12: Parse active-voice reflexive trigger condition ("when you put ... this way")
let (after_clause, (filter, _negated)) = tag("when")(input)?References
- R6. CR annotations are mandatory and verified. Every rules-touching line of engine code must carry a comment of the form CR : . (link)
- Avoid verbatim string equality for parsing Oracle phrases as it bypasses the robust nom-based parser and creates fragile matches. Instead, decompose compound phrases into modular, reusable parsers for constituent parts (e.g., subjects, conjunctions) and compose them using idiomatic combinator aggregates (like nested
altandtagsequences) to prevent combinatorial explosion and improve maintainability.
matthewevans
left a comment
There was a problem hiding this comment.
Reviewed current head 194a554e03df98815d31add3ef14b5aff5fad5b9.
Gemini's parser-gate concern is confirmed on the current head, and there is also a behavioral failure in the new Gilgamesh regression.
Findings:
[MED] The new Gilgamesh full-chain parse still does not produce the advertised attach target. Evidence: cargo test -p engine gilgamesh -- --nocapture fails parser::oracle_effect::tests::attach_just_moved_gilgamesh_any_number_equipment_reflexive_attach at crates/engine/src/parser/oracle_effect/mod.rs:49883, with expected Samurai you control attach target, got ParentTarget. Why it matters: Gilgamesh needs attachment = ParentTarget for the Equipment moved this way and target = Samurai you control; as parsed now, the attach step does not target the Samurai class the Oracle text requires. Suggested fix: fix the attach-clause split for attach one of them to a Samurai you control so only the attachment side binds to ParentTarget, and keep the full-chain regression strict.
[MED] The new parser branch fails the repository's nom-combinator gate. Evidence: scripts/check-parser-combinators.sh fails on added lines in crates/engine/src/parser/oracle_effect/conditions.rs:593-596 for unannotated strip_prefix chaining in the active-voice condition branch. Why it matters: parser dispatch in this repo must be expressed with nom combinators or a justified structural escape hatch, and this branch is part of the newly-added Oracle parser surface. Suggested fix: consume the separator after this way with a small nom combinator, or add a valid allow-noncombinator annotation only if this is purely structural punctuation cleanup after the clause has already parsed.
I also want the parse_dig_from_among change kept out of substring-dispatch territory: the new !scan_contains(lower, "from among") guard at crates/engine/src/parser/oracle_effect/sequence.rs:3267 is controlling which grammar arm runs. Prefer structuring the of them arm and the from among arm as explicit nom alternatives/prefix splits so the Gilgamesh fix does not depend on a broad clause-level scan.
matthewevans
left a comment
There was a problem hiding this comment.
Thanks for the quick follow-up. The parser-combinator gate issue looks addressed on this head, but the substantive Gilgamesh parser failure is still present.
I reran the targeted engine test locally at 370ef438f28159f853cf11536e9ee3eb43dfe1cb:
cargo test -p engine gilgamesh -- --nocapture
It still fails in parser::oracle_effect::tests::attach_just_moved_gilgamesh_any_number_equipment_reflexive_attach with:
expected Samurai you control attach target, got ParentTarget
So the full active-voice reflexive attach chain is still not producing the Samurai target required by the Gilgamesh wording. Please fix that parser path and keep the discriminating test strict.
matthewevans
left a comment
There was a problem hiding this comment.
Approved after re-review of the current head: the Gilgamesh full-chain parser regression now passes, the parser-combinator gate is clean, GitHub checks are green, and the required bug label is present.
Summary
Gilgamesh, Master-at-Arms was reported in Discord as fully parsed with zero coverage gaps, but the trigger AST dropped most of the Oracle text. The card only retained a bare
Dig(6)withfilter: Anyand no reflexive attach step.This change fixes two parser building blocks:
DigFromAmongdisambiguation — the bare"put N of them"arm inparse_dig_from_amongincorrectly matched the substring" of them"inside"from among them", so"put any number of Equipment cards from among them"lowered tofilter: Anyinstead of an Equipment filter withPutCount::AnyNumber."When you put one or more Equipment onto the battlefield this way"(CR 603.12 active voice), not the passive"If an Equipment is put onto the battlefield this way"pattern Armored Skyhunter uses. Addedparse_you_put_onto_battlefield_this_way_clauseand wired it throughstrip_if_you_do_conditionalso the attach step gets aZoneChangedThisWay { Equipment }condition andforward_resulton the parentDig.Closes #3286
Root cause
Coverage marked the card supported because a typed
Dignode existed for the trigger, but AST faithfulness checks were never run (classifier:pending). Comparing Oracle text toparse_detailsshowed:You may put any number of Equipment cards from among them onto the battlefieldDigFromAmong { AnyNumber, Equipment, Battlefield }Dig,filter: AnyPut the rest on the bottom … in a random orderrest_destination: LibraryWhen you put one or more Equipment … this way, you may attach one of them to a Samurai you controlAttach+ZoneChangedThisWay { Equipment }, optionalFiles changed
crates/engine/src/parser/oracle_effect/sequence.rs— guard the"of them"early arm when"from among"is present; regression testscrates/engine/src/parser/oracle_nom/condition.rs— newparse_you_put_onto_battlefield_this_way_clausecombinator + unit testcrates/engine/src/parser/oracle_effect/conditions.rs— recognize"when you put … this way"asZoneChangedThisWaycrates/engine/src/parser/oracle_effect/mod.rs— full-chain regression test (attach_just_moved_gilgamesh_any_number_equipment_reflexive_attach)Expected AST after fix
Trigger
EntersOrAttacksexecute chain:Test plan
cargo fmt --allcargo test -p engine attach_just_moved_gilgamesh_any_number_equipment_reflexive_attachcargo test -p engine from_among_any_number_equipment_not_misrouted_as_of_themcargo test -p engine strip_if_you_do_conditional_gilgamesh_you_put_equipment_this_waycargo test -p engine test_you_put_onto_battlefield_this_way_equipmentcargo test -p engine put_two_of_them_into_hand_still_uses_of_them_armparse_detailsshows Equipment filter + Attach child