fix(parser): scope count-form "would draw N or more" draw replacements (Alms Collector #5678) - #5867
fix(parser): scope count-form "would draw N or more" draw replacements (Alms Collector #5678)#5867Yurii214 wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request improves the draw replacement parser to recognize count-form antecedents (e.g., "would draw or more cards") and dynamically determine the replacement scope (IndividualDraw vs. InstructionCount) based on the substitute's behavior rather than the antecedent's grammatical number. Feedback focuses on ensuring that the newly added comments strictly adhere to Rule R6's formatting requirements for CR annotations to prevent breaking automated regex verification.
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.
Parse changes introduced by this PR · 1 card(s), 2 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Verdict: changes requested — the parser recognizes the count form, but the resulting definition cannot enforce its N or more threshold at runtime.
🔴 Blocker
crates/engine/src/parser/oracle_replacement.rs:426-443 parses the numeric antecedent but retains only IndividualDraw; :526-542 then derives the scope solely from the substitute. No field on ReplacementDefinition carries the parsed N, and draw_scope only selects instruction versus individual-draw matching. Consequently, Alms Collector's two or more condition is discarded: an individual-draw matcher sees one card at a time, while an instruction matcher has no typed >= 2 predicate to enforce. This would either never apply to the intended two-card instruction or apply to the wrong draw class.
The earlier #5678 investigation reached the same implementation boundary: issue comment. CR 121.2a applies the relevant replacement before individual draws; CR 121.6b then governs resuming the individual sequence. The current parser-only test proves AST recognition and chosen scope, not that a pending two-card draw is gated correctly or that a one-card draw is left alone.
Please carry the antecedent through a typed replacement condition (for example, EventContextAmount >= N), teach the census classifier to recognize that typed signal as InstructionCount, and verify the replacement matcher resolves it against the pending instruction count. Add an engine-pipeline regression that proves: opponent draw 1 is unchanged; opponent draw 2 is replaced; and the replacement's two draws occur for the correct players.
🟡 Non-blocking
The fixed-substitute versus count-modifier distinction is real, but it does not eliminate the antecedent threshold. It decides what the substitute does; the typed antecedent still decides whether the instruction is eligible.
✅ Clean
parse_number is the right reusable combinator for the open-ended N or more grammar axis, rather than a two or more special case.
Recommendation: rework this through the draw-replacement model/runtime path, then request re-review on the new head.
|
Thanks for the detailed review — the blocker is right: the parsed
So an I'm happy to build it end-to-end: parser ( |
|
@Yurii214 yes, we just refactored how draw works in the engine. If you rebase you should be able to leverage the new approach to correctly complete this PR
|
…ments Recognize the count-form draw antecedent "would draw <N> or more cards" (parse_number -- build for the class, not a "two or more" special case) and, for N >= 2, retain N as a typed ReplacementCondition::OnlyIfQuantity over the event's draw count (EventContextAmount >= N) -- reusing the existing condition, no new variant -- composed (And) with any as-long-as / while gate. Scope is InstructionCount. Teach the census classifier that a Draw whose condition references EventContextAmount is InstructionCount (the instruction-count signal lives in the antecedent threshold, not just the execute count); re-freeze the corpus (+1: Alms Collector). The instruction-stage runtime enforcement (an InstructionCount replace_event offer at the draw seam, the draw_scope match-gate, and EventContextAmount threading into condition evaluation) is a separate core-draw change tracked in the PR discussion. Refs phase-rs#5678 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4e541d7 to
c88f2b6
Compare
|
Rebased onto the new draw engine and pushed the parser + condition + census layer:
What's left is the instruction-stage runtime, and I'd like to match your intended design before touching the core seam. Tracing the new flow: Since you just refactored this and it's high blast radius, how would you prefer the instruction offer wired — e.g. a |
matthewevans
left a comment
There was a problem hiding this comment.
Verdict: changes requested — the parser/census work now models the threshold correctly, but it advertises a supported instruction-stage replacement before the runtime has the required instruction-stage authority.
🔴 Blocker
crates/engine/src/parser/oracle_replacement.rs:424-460 lowers Alms Collector to InstructionCount with OnlyIfQuantity(EventContextAmount >= N), while crates/engine/src/game/effects/draw.rs:248-307 still decomposes the frame and consults only ProposedEvent::Draw { count: 1 }. The full parse-diff consequently reports Alms Collector as added support, but no candidate can see its count-two instruction and the condition cannot be true in the per-unit consultation. This is coverage-incorrect rather than a parser-only intermediate.
The intended runtime shape is the Plan 03 draw authority: keep DrawReplacementScope as the definition-side classifier, add a separate explicit event-side DrawEventStage (instruction-count versus individual draw), and consult scope-compatible definitions at the matching stage. The instruction consultation must happen once against the full count before units are created; each surviving unit then gets its own individual-draw consultation. Thread that stage event's amount into condition quantity resolution, rather than adding a post-filter or inferring scope from the execute body. This is the design needed for both Alms Collector and Quantum Riddler and preserves CR 121.2a's ordering.
03-draw-and-zone-authority.md is also clear that this belongs in the centralized, pause-safe draw sequence and should follow its Plan-02 preflight, not as a parser-only acceptance change. Please either complete that scoped runtime work with one-card/two-card end-to-end GameScenario coverage (including the correct replacement draws), or keep Alms Collector honestly unsupported until the Plan-03 implementation lands.
✅ Clean
The new antecedent lowering is at the right parser seam: parse_number plus a typed OnlyIfQuantity condition represents the full N or more class without a card-specific branch.
Recommendation: retain the typed parser direction, but land it only with the planned staged runtime authority and discriminating runtime coverage; do not add a generic candidate post-filter.
|
Clarification to my prior review: you do not have access to the internal remediation plan I referenced, and I am not asking you to implement a hidden core-pipeline design. Your diagnosis is correct: the current engine has no public, complete instruction-stage replacement contract, so this PR cannot safely finish the runtime portion from the available contributor context alone. Please do not add a speculative candidate post-filter or a new ad-hoc draw discriminator. We will own the centralized staged draw work and publish the relevant contract before requesting contributor changes in that area. For this PR, the actionable state is simply that parser/census acceptance cannot merge while the runtime is absent, because it would mark Alms Collector supported without enforcing its threshold. We will follow up after the shared runtime lands; no additional implementation is expected from you now. |
matthewevans
left a comment
There was a problem hiding this comment.
Changes requested — parser coverage claims a runtime capability that the draw pipeline does not implement.
🔴 Blocker
crates/engine/src/parser/oracle_replacement.rs:408-580parses Alms Collector asInstructionCountand recordsEventContextAmount >= 2;scripts/draw_replacement_census.py:304-326then freezes that parser classification. But the draw execution authority is still the per-unit sequence driver incrates/engine/src/game/effects/draw.rs:138-221; this PR adds no instruction-stage consult/apply path forDrawReplacementScope::InstructionCount. The new test atoracle_replacement.rs:17076-17120asserts only the AST/coverage shape, so it cannot prove that an opponent's two-card draw becomes the required one-card-for-each substitute (or that a one-card draw remains unchanged).
Please hold this parser/coverage change until the core draw-replacement contract has a real instruction-stage authority, then land it with registered runtime regressions for 1-card and 2+-card opponent draws.
✅ Clean
- The antecedent grammar is compositional and the threshold is typed rather than a card-name special case.
Recommendation: request changes — do not mark Alms Collector supported before the instruction-count runtime path exists.
|
Understood, and thanks for the clear guidance — I'll hold on the runtime and won't add a speculative post-filter or draw discriminator. Glad the threshold modeling (parser + Happy to keep this PR open so that layer is ready to combine once the shared staged-draw runtime + contract land, or to close it and re-open against the published contract — whichever keeps your queue cleaner. Just let me know. Appreciate the detailed engagement. |
|
Held — the shared draw-replacement authority is still absent, so no contributor implementation is requested. 🔴 Blocker
✅ CleanYour follow-up correctly avoids a speculative post-filter or ad-hoc draw discriminator. Recommendation: keep this PR held until the centralized staged draw-replacement contract lands; no further contributor change is expected now. |
|
Expiry warning — this PR remains held on Unless the requested changes are addressed within 7 days, this PR will be automatically closed to keep the review queue current. A new head or material contributor follow-up will stop that expiry and return the PR to review. |
|
Thanks for the heads-up. Confirming state so the queue stays accurate: per your 2026-07-16 / 07-17 notes this is held for the shared staged draw-replacement authority, and you asked that no contributor implementation be attempted from the available context — so the head is unchanged at Happy to keep it open until the shared runtime lands, or to close and reopen a fresh head once the instruction-stage replacement contract is available — whichever keeps your queue cleanest. Otherwise this note should hold off the auto-expiry. |
|
Held — confirmation received. The current head |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughChangesInstruction-count draw replacements
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant DrawSequence
participant ReplacementResolver
participant GameState
DrawSequence->>ReplacementResolver: replace_draw_instruction(count, applied)
ReplacementResolver->>GameState: set Instruction consultation scope
ReplacementResolver-->>DrawSequence: Proceed with updated count/applied or Replaced
DrawSequence->>DrawSequence: split surviving count into individual draws
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Any pending work mentioned for draw/replacement should now be implemented for this PR to use. |
matthewevans
left a comment
There was a problem hiding this comment.
Request changes — current head b4e24382.
[HIGH] Instruction-count replacements still never reach a runtime authority. oracle_replacement.rs now lowers the count form to DrawReplacementScope::InstructionCount plus OnlyIfQuantity { EventContextAmount >= N }, but the current draw path in game/effects/draw.rs only invokes replacement from resume_draw_sequence through per-card ProposedEvent::Draw { count: 1 }. A source scan of the current head finds InstructionCount only in the parser/type declarations and tests, not in a runtime resolver. Consequently an Alms Collector-style "would draw two or more cards" replacement cannot apply to a two-card instruction.
The branch history contains the needed pattern in ab673bc104 (replace_draw_instruction before the per-card split), but that authority is not present on this head after the merge. Please restore/adapt that instruction-stage consult before splitting the draw sequence, and add a production-path regression showing draw-one is unaffected while draw-two is replaced.
matthewevans
left a comment
There was a problem hiding this comment.
Request changes — current head b0cea689.
The intervening merge changes only dependency/version files; it does not restore the missing instruction-stage draw replacement authority. InstructionCount remains parser/type-only on this head while the production draw path applies replacements only to count: 1 per-card events. The prior requested runtime-seam fix and draw-one/draw-two production-path regression are still required.
|
The current head remains blocked by the requested instruction-stage draw-replacement fix and runtime regression. Please update the branch before requesting another review. |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/engine/src/game/replacement.rs`:
- Around line 8442-8453: Restore state.draw_consult_scope to prev_scope
immediately after replace_event and before calling
apply_pending_post_replacement_effect. Keep the post-replacement drain
conditional unchanged, ensuring substitute draws execute under the normal scope
and can use the IndividualDraw seam.
In `@crates/engine/src/parser/oracle_replacement.rs`:
- Around line 597-619: Preserve threshold_n for every count-form branch by
composing its OnlyIfQuantity condition into def.condition before any as-long-as
or draw-skip branch returns. Ensure all early-return paths retain the N-or-more
gate while preserving existing condition composition. Add a regression test
confirming a gated “draw two or more cards” replacement does not apply to a
one-card draw.
- Around line 604-612: Update the threshold construction in the threshold_n
handling to convert n with i32::try_from instead of a wrapping cast. If
conversion overflows, fail closed by skipping or rejecting the replacement
condition rather than creating a negative rhs value.
In `@crates/engine/src/types/ability.rs`:
- Around line 19950-19954: Update the documentation for DrawReplacementScope to
remove the CR 121.6b reference from this consult-scope description, retaining
only CR 121.2a and its explanation of the pre-split whole-instruction consult.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 225c9624-979a-45ef-86e4-d81b157327b1
⛔ Files ignored due to path filters (1)
scripts/draw-replacement-corpus.tsvis excluded by!**/*.tsv
📒 Files selected for processing (6)
crates/engine/src/game/effects/draw.rscrates/engine/src/game/replacement.rscrates/engine/src/parser/oracle_replacement.rscrates/engine/src/types/ability.rscrates/engine/src/types/game_state.rsscripts/draw_replacement_census.py
| let result = replace_event(state, instruction, events); | ||
| // CR 614.6 + CR 121.6: a full substitution (Alms Collector: "instead you and | ||
| // that player each draw a card") is pre-zeroed by `apply_single_replacement` | ||
| // and its substitute stashed as a post-replacement continuation. Drain it in | ||
| // the same resolution step, mirroring `draw_through_replacement`'s Execute | ||
| // arm, so the substitute runs before the (now zero-count) instruction below. | ||
| if !matches!(result, ReplacementResult::NeedsChoice(_)) && state.has_post_replacement_drain() { | ||
| let _ = crate::game::engine_replacement::apply_pending_post_replacement_effect( | ||
| state, None, None, None, events, | ||
| ); | ||
| } | ||
| state.draw_consult_scope = prev_scope; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Trace whether the post-replacement drain can run a Draw substitute synchronously.
rg -nP --type=rust -C4 'fn apply_pending_post_replacement_effect' crates/engine/src
# Look for draw dispatch reachable from the drained continuation.
rg -nP --type=rust -C3 '\bstart_draw_sequence(_with_[a-z_]+)?\s*\(' crates/engine/src/game/engine_replacement.rsRepository: phase-rs/phase
Length of output: 874
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the post-replacement drain implementation and the draw replacement seam.
sed -n '1981,2065p' crates/engine/src/game/engine_replacement.rs
printf '\n---\n'
sed -n '8418,8460p' crates/engine/src/game/replacement.rs
printf '\n---\n'
rg -n --type=rust 'draw_consult_scope|DrawConsultScope::|IndividualDraw|Instruction' crates/engine/src/game -C 3Repository: phase-rs/phase
Length of output: 36244
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the drain body and any helper it calls that can schedule draw events.
sed -n '1981,2045p' crates/engine/src/game/engine_replacement.rs
printf '\n---\n'
rg -n --type=rust 'start_draw_sequence|draw_through_replacement|apply_single_replacement|apply_pending_post_replacement_effect' crates/engine/src/game -C 4Repository: phase-rs/phase
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Print the relevant function body.
awk 'NR>=1981 && NR<=2065 {print NR ":" $0}' crates/engine/src/game/engine_replacement.rs
printf '\n---\n'
awk 'NR>=8438 && NR<=8460 {print NR ":" $0}' crates/engine/src/game/replacement.rs
printf '\n---\n'
rg -n --type=rust -C 4 'IndividualDraw|DrawConsultScope::Instruction|DrawConsultScope::Individual' crates/engine/src/gameRepository: phase-rs/phase
Length of output: 28948
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the consult-scope branch and the drain helper implementation.
rg -n --type=rust -C 5 'draw_consult_scope|DrawConsultScope::Instruction|IndividualDraw|return false' crates/engine/src/game
printf '\n---\n'
sed -n '1981,2065p' crates/engine/src/game/engine_replacement.rsRepository: phase-rs/phase
Length of output: 50371
Restore draw_consult_scope before draining the substitute. apply_pending_post_replacement_effect runs the continuation immediately, and any draw it issues while state.draw_consult_scope == DrawConsultScope::Instruction will skip IndividualDraw shields because that seam only accepts InstructionCount there. Move the scope reset above the drain so the substitute’s per-card draws still see the normal IndividualDraw seam.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/engine/src/game/replacement.rs` around lines 8442 - 8453, Restore
state.draw_consult_scope to prev_scope immediately after replace_event and
before calling apply_pending_post_replacement_effect. Keep the post-replacement
drain conditional unchanged, ensuring substitute draws execute under the normal
scope and can use the IndividualDraw seam.
| // CR 121.2a: a "draw N or more cards" antecedent (N >= 2) gates the | ||
| // replacement on the pending draw *instruction* being for at least N | ||
| // cards. Carry N as a typed `OnlyIfQuantity` over the event's draw count | ||
| // (`EventContextAmount`), evaluated at the instruction stage before the | ||
| // draw decomposes into individual card draws — composed (And) with any | ||
| // as-long-as / while / except-first gate already set. Alms Collector: | ||
| // "If an opponent would draw two or more cards, ...". | ||
| if let Some(n) = threshold_n { | ||
| let threshold = ReplacementCondition::OnlyIfQuantity { | ||
| lhs: QuantityExpr::Ref { | ||
| qty: QuantityRef::EventContextAmount, | ||
| }, | ||
| comparator: Comparator::GE, | ||
| rhs: QuantityExpr::Fixed { value: n as i32 }, | ||
| active_player_req: None, | ||
| }; | ||
| def.condition = Some(match def.condition.take() { | ||
| Some(existing) => ReplacementCondition::And { | ||
| conditions: vec![existing, threshold], | ||
| }, | ||
| None => threshold, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve N on every count-form branch.
Line 568 returns after applying an as long as condition, and the draw-skip branches return even earlier, so they bypass Lines 604-618. A gated would draw two or more cards replacement can therefore lose its threshold and apply to a one-card draw. Compose the threshold through the shared condition path before any branch returns; add a gated count-form regression test.
Also applies to: 18804-18838
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/engine/src/parser/oracle_replacement.rs` around lines 597 - 619,
Preserve threshold_n for every count-form branch by composing its OnlyIfQuantity
condition into def.condition before any as-long-as or draw-skip branch returns.
Ensure all early-return paths retain the N-or-more gate while preserving
existing condition composition. Add a regression test confirming a gated “draw
two or more cards” replacement does not apply to a one-card draw.
Source: Path instructions
| if let Some(n) = threshold_n { | ||
| let threshold = ReplacementCondition::OnlyIfQuantity { | ||
| lhs: QuantityExpr::Ref { | ||
| qty: QuantityRef::EventContextAmount, | ||
| }, | ||
| comparator: Comparator::GE, | ||
| rhs: QuantityExpr::Fixed { value: n as i32 }, | ||
| active_player_req: None, | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant parser section.
sed -n '560,640p' crates/engine/src/parser/oracle_replacement.rs
echo
echo '--- parse_number references ---'
rg -n "parse_number|threshold_n|ReplacementCondition::OnlyIfQuantity|QuantityExpr::Fixed" crates/engine/src -S
echo
echo '--- parse_number definition candidates ---'
rg -n "fn parse_number|type .*Number|parse_number\(" crates/engine/src/parser -SRepository: phase-rs/phase
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- oracle_replacement relevant region ---'
sed -n '596,620p' crates/engine/src/parser/oracle_replacement.rs
echo
echo '--- parse_number definition and nearby types ---'
rg -n "fn parse_number|type ParseNumber|parse_number\(" crates/engine/src/parser crates/engine/src -S --max-count 20
echo
echo '--- QuantityExpr::Fixed definition ---'
rg -n "enum QuantityExpr|Fixed \{" crates/engine/src/types crates/engine/src -S --max-count 20Repository: phase-rs/phase
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- parse_number file candidates ---'
fd -a 'parse_number' crates/engine/src/parser crates/engine/src || true
echo
echo '--- QuantityExpr definition candidates ---'
fd -a 'ability.rs' crates/engine/src/types crates/engine/src || true
rg -n "pub enum QuantityExpr|pub struct QuantityExpr|Fixed \{" crates/engine/src/types crates/engine/src/types crates/engine/src -S --max-count 20Repository: phase-rs/phase
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read only the exact code around the reviewed cast.
sed -n '604,612p' crates/engine/src/parser/oracle_replacement.rs
echo
echo '--- parse_number references in parser files ---'
rg -n "parse_number" crates/engine/src/parser -S --max-count 50
echo
echo '--- QuantityExpr enum definition ---'
rg -n "enum QuantityExpr" crates/engine/src -S --max-count 20Repository: phase-rs/phase
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the exact parse_number definition and the QuantityExpr type with minimal output.
rg -n "fn parse_number|pub fn parse_number|type ParseNumber" crates/engine/src/parser -S --max-count 20
echo
rg -n "pub enum QuantityExpr" crates/engine/src -S --max-count 10
echo
# If QuantityExpr is in a different file, show the exact match file names only.
rg -l "pub enum QuantityExpr" crates/engine/src -SRepository: phase-rs/phase
Length of output: 3930
🏁 Script executed:
#!/bin/bash
set -euo pipefail
set +e
rg -n -C 3 "fn parse_number|pub fn parse_number|type ParseNumber" crates/engine/src/parser -S
status1=$?
echo "--- status1=$status1 ---"
echo
rg -n -C 3 "pub enum QuantityExpr" crates/engine/src -S
status2=$?
echo "--- status2=$status2 ---"
exit 0Repository: phase-rs/phase
Length of output: 30058
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- threshold_n assignment ---'
rg -n -C 4 "threshold_n" crates/engine/src/parser/oracle_replacement.rs -S
echo
echo '--- parse_number in parser files with context ---'
rg -n -C 3 "parse_number" crates/engine/src/parser -S --max-count 30Repository: phase-rs/phase
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n crates/engine/src/parser/oracle_replacement.rs | sed -n '540,620p'Repository: phase-rs/phase
Length of output: 5247
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 3 "threshold_n" crates/engine/src/parser/oracle_replacement.rs -SRepository: phase-rs/phase
Length of output: 968
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd -a 'parse*' crates/engine/src/parserRepository: phase-rs/phase
Length of output: 27577
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the exact file that defines QuantityExpr and the nearby definition.
file=$(rg -l "pub enum QuantityExpr" crates/engine/src -S | head -n 1)
echo "FILE=$file"
if [ -n "$file" ]; then
cat -n "$file" | sed -n '1,120p'
fiRepository: phase-rs/phase
Length of output: 5571
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 2 "parse_number\(" crates/engine/src/parser -S --max-count 50Repository: phase-rs/phase
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
log=/tmp/coderabbit-shell-logs/shell-output-FZrUV8
echo '--- lines mentioning parse_number in the previous log ---'
rg -n "parse_number|threshold_n|QuantityExpr::Fixed" "$log" -S | head -n 80Repository: phase-rs/phase
Length of output: 8659
🏁 Script executed:
#!/bin/bash
set -euo pipefail
cat -n crates/engine/src/parser/oracle_replacement.rs | sed -n '560,620p'Repository: phase-rs/phase
Length of output: 3963
🏁 Script executed:
#!/bin/bash
set -euo pipefail
log=/tmp/coderabbit-shell-logs/shell-output-FZrUV8
# Pull only the exact snippets needed from the previous large output.
rg -n -C 2 "parse_number|threshold_n" "$log" -S | head -n 120Repository: phase-rs/phase
Length of output: 13133
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the shared numeric primitive implementation and any overflow handling.
cat -n crates/engine/src/parser/oracle_nom/primitives.rs | sed -n '1,240p'
echo
echo '--- digit-number helpers ---'
rg -n -C 3 "parse_digit_number|parse_english_number|u32::MAX|i32::MAX|checked" crates/engine/src/parser/oracle_nom/primitives.rs -SRepository: phase-rs/phase
Length of output: 14154
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Focus on the numeric parser body only.
cat -n crates/engine/src/parser/oracle_nom/primitives.rs | sed -n '1,220p'Repository: phase-rs/phase
Length of output: 10516
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show just the code paths that construct u32 numbers.
rg -n -C 5 "parse_digit_number|parse_english_number|parse_number\(" crates/engine/src/parser/oracle_nom/primitives.rs -SRepository: phase-rs/phase
Length of output: 9185
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the relevant portion of the shared numeric primitive implementation.
sed -n '1,220p' crates/engine/src/parser/oracle_nom/primitives.rsRepository: phase-rs/phase
Length of output: 8976
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Extract the parse_digit_number and parse_english_number sections if present.
rg -n -A 40 -B 10 "fn parse_digit_number|fn parse_english_number|pub fn parse_number" crates/engine/src/parser/oracle_nom/primitives.rs -SRepository: phase-rs/phase
Length of output: 7375
Use a fallible conversion for the threshold.
threshold_n is u32, so n as i32 can wrap above i32::MAX and flip this gate negative; use i32::try_from(n) and fail closed on overflow.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/engine/src/parser/oracle_replacement.rs` around lines 604 - 612,
Update the threshold construction in the threshold_n handling to convert n with
i32::try_from instead of a wrapping cast. If conversion overflows, fail closed
by skipping or rejecting the replacement condition rather than creating a
negative rhs value.
Source: Path instructions
| /// CR 121.2a + CR 121.6b: which [`DrawReplacementScope`] the in-progress draw | ||
| /// replacement consult is eligible to match. A draw instruction resolves in two | ||
| /// seams — the whole-instruction consult that runs *before* the instruction | ||
| /// splits into individual card draws, and the per-card consult that runs for | ||
| /// each individual draw — and a shield is scoped to exactly one of them. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the relevant code and documentation.
git ls-files 'crates/engine/src/types/ability.rs' 'docs/MagicCompRules.txt' | cat
echo
echo '--- ability.rs excerpt ---'
sed -n '19930,19970p' crates/engine/src/types/ability.rs
echo
echo '--- relevant CR search ---'
rg -n -C 2 '121\.2a|121\.6b|draw replacement|replacement' docs/MagicCompRules.txtRepository: phase-rs/phase
Length of output: 2471
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the exact rules text around the cited sections.
python3 - <<'PY'
from pathlib import Path
path = Path('docs/MagicCompRules.txt')
text = path.read_text()
for needle in ['121.2a', '121.6b']:
idx = text.find(needle)
print(f'=== {needle} @ {idx} ===')
if idx != -1:
start = max(0, idx - 500)
end = min(len(text), idx + 1200)
print(text[start:end])
print()
PYRepository: phase-rs/phase
Length of output: 702
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find nearby parser/test code that may explain the intended seam.
rg -n -C 3 'DrawConsultScope|DrawReplacementScope|instruction.*draw|individual.*draw' crates/engine/src/types/ability.rs crates/engine/src -g '!target'Repository: phase-rs/phase
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the file around the cited lines and any nearby comments.
sed -n '19945,19960p' crates/engine/src/types/ability.rs | cat -nRepository: phase-rs/phase
Length of output: 1105
🌐 Web query:
Magic Comprehensive Rules 121.2a 121.6b draw replacement text
💡 Result:
In the Magic: The Gathering Comprehensive Rules, rules 121.2a and 121.6b address how replacement effects interact with drawing cards [1][2]. Rule 121.2a states: An instruction to draw multiple cards can be modified by replacement effects that refer to the number of cards drawn. This modification occurs before considering any of the individual card draws [1][2]. Rule 121.6b states: If an effect replaces a draw within a sequence of card draws, the replacement effect is completed before resuming the sequence [1][2].
Citations:
Drop CR 121.6b from this consult-scope doc. It covers completing a replacement before resuming a draw sequence; the pre-split consult seam here is supported by CR 121.2a, not 121.6b.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/engine/src/types/ability.rs` around lines 19950 - 19954, Update the
documentation for DrawReplacementScope to remove the CR 121.6b reference from
this consult-scope description, retaining only CR 121.2a and its explanation of
the pre-split whole-instruction consult.
Source: Path instructions
…per-card split The count>=min threshold added in the prior commit only ran after the draw pipeline had split a multi-card draw into single-card events, so Alms Collector's min:2 was never applicable in production. Move the instruction-scope replacement consult ahead of the per-card split at the draw-instruction seam so the printed threshold is evaluated against the full draw count. Add an end-to-end test through the production draw path proving a two-card draw is replaced and a one-card draw is not. (cherry picked from commit ab673bc) Restore/adapt the instruction-stage draw-replacement authority onto the current draw-sequence stack (start_draw_sequence_with_origin + push_draw_sequence_with_origin), per the maintainer's requested-changes review. Co-authored-by: nghetienhiep <13849419+nghetienhiep@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
679c3cf to
fd86d17
Compare
|
Pushed The structural merge compiles clean — the field/
So the count-gate needs the instruction-stage contract you mentioned owning: either resolve |
|
Request changes — head Thanks for pushing the instruction-stage seam rather than a post-filter — the seam choice is right, and your diagnosis of the 🔴 Blocker1. The head does not compile — 2. The threshold is modeled twice, in two incompatible ways. 3. The contract you're blocked on already exists — the pending event is in scope at condition-evaluation time.
So the pending 4. The consult scope is restored after the substitute drains, so the substitute's own draws run under the wrong seam. 🟡 Non-blocking5. The threshold is dropped on three early-return paths. 6. CR 121.6b does not describe consult-scope eligibility. 7. Parse-diff evidence is stale for this head. The Refuting one CodeRabbit item: ✅ Clean
Recommendation: rework on a single threshold authority — keep |
|
Correction to my review above — one credit I gave you was stronger than my evidence supports. In the ✅ Clean section I wrote that What I should have written: the test is shaped correctly — it drives the real Nothing else in the review changes. The two blockers and the recommendation stand as written, and the other three ✅ items — the right seam at Once the rework lands and the test compiles, please confirm it red-first against the reverted |
|
Supplement to my 20:32 review on the same head 🔴 BlockerThe pre-split consult changes Quantum Riddler's runtime semantics, untested and unclaimed.
Today a three-card draw meets the per-card seam three times; after this change it is modified once at the instruction seam. That is very likely the CR 121.2a-correct outcome — which is exactly why it needs a regression rather than silence. The parse-diff sticky cannot surface it, because Quantum Riddler's parse signature is unchanged. Please add a Quantum Riddler multi-card regression alongside the Alms Collector one. Consult scope is restored after the substitute drains. 🟡 Non-blocking — three CR citations that do not describe the code they annotateEach verbatim from
The threshold is dropped on three early returns. The PR description no longer matches the diff. The body argues Alms Collector should be ✅ Clean — re-verified independently on this head
Recommendation: fold these into the same rework as the threshold-authority fix. Move the scope restore above the drain, add the Quantum Riddler regression, hoist the threshold composition above the three early returns, and drop the three CR citations at |
|
Correcting the record on where the red comes from, because your last comment is built on the opposite premise and that will cost you another round. Reviewed at head The compile error is on this branch, not on
|
matthewevans
left a comment
There was a problem hiding this comment.
Changes requested — the current head does not compile.
🔴 Blocker
[HIGH] Keep the draw threshold in one typed authority. Evidence: crates/engine/src/game/replacement.rs:5983,5994 destructures DrawReplacementScope::InstructionCount { min }, but crates/engine/src/types/ability.rs:19932 declares the variant fieldless. This explains the current required-check cascade. Preserve the scope as a category and carry N through the existing quantity condition path; then regenerate current-head parse evidence.
Recommendation: request changes; restore compilation and current-head card-data evidence before re-review.
matthewevans
left a comment
There was a problem hiding this comment.
Request changes — current head fd86d177f770f2f4fc976fa8ef918ce34a11b81c is not safely portable as a maintainer fixup.
- It currently fails to compile:
crates/engine/src/game/replacement.rs:5987and:5996pattern-matchDrawReplacementScope::InstructionCount { min }, whilecrates/engine/src/types/ability.rs:19932declares the variant as fieldless. The threshold must remain in the typed replacement-condition authority, not be split into an undeclared scope payload. - This head is
DIRTYagainst currentmain. An isolated merge attempt produces broad conflicts across the draw sequence, replacement matcher, state, and parser/census surfaces. Those are not a mechanical test-module conflict: the branch’s instruction-stage model has to be reconciled with the current draw pipeline as one reviewed design. - Required CI is red, so the only parse-diff artifact is from the earlier baseline rather than evidence for this head.
Please rebase onto current main, resolve the threshold/scope model coherently at the centralized start_draw_sequence_with_origin → instruction-consult seam, and return with green CI, current parse evidence, and discriminating draw-one/draw-two runtime coverage. I have not pushed an unsafe conflict resolution.
|
Current-head recheck for The branch remains unsafe to transplant. Its instruction-stage work conflicts in the central draw-sequence path, and it does not compile as written: Please return with a fresh rebase that reconciles the whole instruction-consult path with the current draw sequence, plus green current-head CI/parse evidence and production-path one-card versus two-card coverage. This is a reviewed design change, not a safe mechanical conflict resolution. |
|
Rechecked current head The route stated in the current-head review remains unchanged. The branch is |
Closes #5678.
Problem
parse_replacement_line's draw-antecedentaltmatched only"would draw a card"and a hardcoded"would draw one or more cards". Alms Collector's antecedent is the count-form "would draw two or more cards", so it matched neither and the line produced no replacement at all — the card silently did nothing.Fix
Two parts, both at the parser seam (
crates/engine/src/parser/oracle_replacement.rs):Recognize the count-form antecedent as a class. Replace the hardcoded
"one or more cards"tag with"would draw " + parse_number + " or more cards", so every<N> or morethreshold (one, two, three, …) is recognized — build-for-the-class rather than a per-card tag.parse_numberalready covers digit and word forms.Derive
draw_scopefrom the substitute, not the antecedent. This is the subtle part, and it corrects the issue's stated expectation. The issue predictedInstructionCount, but that contradictsDrawReplacementScope's own doc andscripts/draw_replacement_census.py::classify_scope:InstructionCountis reserved for count-modifier substitutes that read the replaced instruction's own count ("draw that many cards plus one instead" — Quantum Riddler, aQuantityRef::EventContextAmount; the only such card). Alms Collector's substitute is a fixed substitution ("instead you and that player each draw a card") — the Notion Thief / Hullbreacher class the enum doc explicitly scopes asIndividualDraw(CR 121.6b: cards are drawn one at a time).So the scope is finalized after the execute chain is parsed. Both antecedent forms seed the
IndividualDrawdefault;execute_draw_reads_replaced_countthen inspects the execute's top-levelDraweffect — the exact surfaceclassify_scopereads (execute["effect"]) — for a count that referencesEventContextAmount, and promotes toInstructionCountonly then. Emitting the scope from the same signal the census reads keeps producer and cross-check in agreement by construction: Alms Collector →IndividualDraw, Quantum Riddler →InstructionCount, and no existing card's scope changes.Why the corpus baseline moves
scripts/draw-replacement-corpus.tsvgains exactly one row (alms collector … IndividualDraw) now that the card produces a Draw replacement. Re-frozen withscripts/draw_replacement_census.py --corpus --writein this commit;--corpus --checkis green (51 rows), and the diff is that single added row — no existing row moved.Verification
count_form_draw_antecedent_is_recognized_and_scope_follows_the_substitutedrives the real entry (parse_replacement_line) and asserts scope follows the substitute: Alms Collector + a fixed "three or more" →IndividualDraw; a "that many … plus one" count-modifier →InstructionCount; singular "a card" →IndividualDraw.cargo test -p engine --lib(the new test) — green.draw_replacement_census.py --corpus --check— green after re-freeze (against a freshly regeneratedcard-data.json).cargo clippy -p engine --lib -- -D warnings— clean.docs/MagicCompRules.txt.Model: claude-opus-4-8[1m]
Summary by CodeRabbit
New Features
Bug Fixes
Tests