Skip to content

fix(parser): UNSUPPORTED cluster: The Fourth Doctor (Blast COMMANDER) — play historic from TOP OF LI - #4341

Merged
matthewevans merged 8 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-41-unsupported-cluster-the-fourth
Jul 4, 2026
Merged

fix(parser): UNSUPPORTED cluster: The Fourth Doctor (Blast COMMANDER) — play historic from TOP OF LI#4341
matthewevans merged 8 commits into
phase-rs:mainfrom
ntindle:fix/who-misparse-41-unsupported-cluster-the-fourth

Conversation

@ntindle

@ntindle ntindle commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

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

Root cause: UNSUPPORTED cluster: The Fourth Doctor (Blast COMMANDER) — play historic from TOP OF LIBRARY once/turn + create a Food token

Cards corrected

  • The Fourth Doctor

Fix

Cluster 41 (The Fourth Doctor) implemented as a 100% parser change with zero new engine variants. The base had advanced past the plan's premise: ability-word classification (Gap-1) was already fixed upstream, but the card was still incorrect — the "cast a historic spell" branch was dropped (affected: Typed(Land+Historic) only) and the reflexive "When you do, create a Food token" rider produced no trigger. Fixed both. Step 1: hoisted the once-per-turn frequency-prefix strip to the top of try_parse_top_of_library_cast_permission so the disjunctive/Bolas branches inherit it, and threaded a frequency: CastFrequency parameter through try_parse_disjunctive_top_of_library_cast_permission, restoring the dropped spell branch -> affected: Or([Typed(Land+Historic), Typed(Card+Historic)]) with frequency OncePerTurn. Step 2: exported the helper via pub(crate) use and imported it into oracle.rs. Step 3: added a pre-Priority-7 handler that strips the ability word, splits the rider on ". when you do, ", parses the permission static, and emits BOTH the static AND a reflexive TriggerMode::PlayCard trigger with valid_target Controller, spell_cast_origin Equals(Library), valid_card cloned from the permission's own affected filter (closing the over-trigger gap), executing the Food token; self-declines if the rider payoff is Unimplemented. Step 5: three new tests + a shared disjunction assertion helper. Verified via oracle-gen single-card parse (no Unimplemented; correct static + trigger shape), 28 targeted tests pass, cargo fmt clean, cargo clippy -p engine --all-targets -D warnings exit 0, cargo test -p engine exit 0. Parser diff gate passes on production files. CR 603.12 multi-permission scoping is a documented, code-annotated approximation, not asserted correct. No commit made.

Files changed

  • crates/engine/src/parser/oracle_static/restriction.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_static/tests.rs

CR references

  • CR 207.2c
  • CR 401.5
  • CR 601.1a
  • CR 603.12
  • CR 111.10
  • CR 305.1
  • CR 400.1
  • CR 601.2a

Verification

  • cd /Users/ntindle/code/random/magic/phase-main-base && cargo fmt --all — clean (no files changed; nothing to commit)
  • cd /Users/ntindle/code/random/magic/phase-main-base && ./scripts/check-parser-combinators.sh <upstream/main merge-base 258e53607> — clean (exit 0)
  • cd /Users/ntindle/code/random/magic/phase-main-base && cargo clippy -p engine --all-targets -- -D warnings — clean (exit 0; fix's 4 changed files clippy-clean, no warnings at all)
  • cd /Users/ntindle/code/random/magic/phase-main-base && cargo test -p engine — clean (exit 0; all tests passed, 0 failed)
  • cd /Users/ntindle/code/random/magic/phase-main-base && cargo run --profile tool --features cli --bin oracle-gen -- data --filter "the fourth doctor" — confirmed correct parse (no Unimplemented/Unknown)
    Cards confirmed re-parsed correctly: The Fourth Doctor

🤖 Generated with Claude Code

Track

Developer

LLM

  • Model: Claude Code / Codex maintainer review
  • Thinking: high

@ntindle
ntindle requested a review from matthewevans as a code owner June 25, 2026 23:43

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements parsing for top-of-library play/cast permissions carrying a reflexive 'When you do, ' rider (such as on 'The Fourth Doctor') and hoists the once-per-turn frequency prefix stripping so it applies to both compound and disjunctive branches. Feedback on the changes highlights three key improvements: temporarily saving and restoring ctx.subject and ctx.actor to prevent context leakage to subsequent lines, removing a redundant scan_contains check before parsing, and aligning the text and lower strings by stripping the prefix from both to prevent downstream parser mismatches.

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.

Comment thread crates/engine/src/parser/oracle.rs Outdated
Comment on lines +3129 to +3137
// CR 603.12: reflexive trigger body — parse with a
// neutral subject so it resolves as its own ability.
ctx.subject = None;
ctx.actor = None;
let rider_def = parse_effect_chain_with_context(
rider_text.trim(),
AbilityKind::Spell,
&mut ctx,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

[HIGH] Context leakage of ctx.subject and ctx.actor to subsequent lines.

Why it matters: Mutating ctx.subject and ctx.actor to None without restoring them permanently alters the parser context for any subsequent lines of the same card, which can cause subtle parsing bugs on multi-line cards.

Suggested fix: Use Option::take to temporarily clear the context fields and restore them immediately after parsing the effect chain.

Suggested change
// CR 603.12: reflexive trigger body — parse with a
// neutral subject so it resolves as its own ability.
ctx.subject = None;
ctx.actor = None;
let rider_def = parse_effect_chain_with_context(
rider_text.trim(),
AbilityKind::Spell,
&mut ctx,
);
// CR 603.12: reflexive trigger body — parse with a
// neutral subject so it resolves as its own ability.
let orig_subject = ctx.subject.take();
let orig_actor = ctx.actor.take();
let rider_def = parse_effect_chain_with_context(
rider_text.trim(),
AbilityKind::Spell,
&mut ctx,
);
ctx.subject = orig_subject;
ctx.actor = orig_actor;

Comment thread crates/engine/src/parser/oracle.rs Outdated
Comment on lines +3121 to +3124
if scan_contains(&permission_lower, "from the top of your library") {
if let Some((perm_text, rider_text)) =
split_once_on_lower(&permission_line, &permission_lower, ". when you do, ")
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

[MEDIUM] Redundant string search check before parsing.

Why it matters: try_parse_top_of_library_cast_permission already validates that the string contains and matches the top-of-library permission format, making the pre-check with scan_contains redundant and slightly less efficient.

Suggested fix: Remove the redundant scan_contains check and directly proceed to split and parse.

Suggested change
if scan_contains(&permission_lower, "from the top of your library") {
if let Some((perm_text, rider_text)) =
split_once_on_lower(&permission_line, &permission_lower, ". when you do, ")
{
if let Some((perm_text, rider_text)) =
split_once_on_lower(&permission_line, &permission_lower, ". when you do, ")
{
References
  1. Avoid redundant string prefix checks (such as '.starts_with()') before calling parsing functions that already perform leading character validation and whitespace trimming.

Comment on lines +2278 to +2293
// CR 601.2a: Strip an optional once-per-turn frequency prefix FIRST, before
// the Bolas-compound and disjunctive branches, so each branch sees the bare
// "you may play/cast …" lead and inherits the correct frequency. "Once each
// turn, …" (Assemble the Players, The Fourth Doctor) and the longer "Once
// during each of your turns, …" synonym both lower to OncePerTurn; absence
// keeps the Unlimited shape (Realmwalker, Future Sight, Crystal Skull).
// A prefix strip removes nothing from the end, so the suffix of `text`
// (original case) stays aligned with the post-strip `lower`; the
// `text`-based alt-cost/condition rider scans below remain correct.
let (lower, frequency) = if let Some(r) = nom_tag_lower(lower, lower, "once each turn, ")
.or_else(|| nom_tag_lower(lower, lower, "once during each of your turns, "))
{
(r, CastFrequency::OncePerTurn)
} else {
(lower, CastFrequency::Unlimited)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

[MEDIUM] Mismatched text and lower string alignment after prefix stripping.

Why it matters: Stripping the frequency prefix from lower but leaving text unstripped causes them to become out of sync at the start. Any downstream parser or helper (such as try_parse_disjunctive_top_of_library_cast_permission or parse_top_of_library_alt_cost_rider) that expects text and lower to be aligned will receive mismatched offsets or incorrect substrings.

Suggested fix: Shadow text to strip the corresponding prefix length when lower is stripped.

Suggested change
// CR 601.2a: Strip an optional once-per-turn frequency prefix FIRST, before
// the Bolas-compound and disjunctive branches, so each branch sees the bare
// "you may play/cast …" lead and inherits the correct frequency. "Once each
// turn, …" (Assemble the Players, The Fourth Doctor) and the longer "Once
// during each of your turns, …" synonym both lower to OncePerTurn; absence
// keeps the Unlimited shape (Realmwalker, Future Sight, Crystal Skull).
// A prefix strip removes nothing from the end, so the suffix of `text`
// (original case) stays aligned with the post-strip `lower`; the
// `text`-based alt-cost/condition rider scans below remain correct.
let (lower, frequency) = if let Some(r) = nom_tag_lower(lower, lower, "once each turn, ")
.or_else(|| nom_tag_lower(lower, lower, "once during each of your turns, "))
{
(r, CastFrequency::OncePerTurn)
} else {
(lower, CastFrequency::Unlimited)
};
// CR 601.2a: Strip an optional once-per-turn frequency prefix FIRST, before
// the Bolas-compound and disjunctive branches, so each branch sees the bare
// "you may play/cast …" lead and inherits the correct frequency. "Once each
// turn, …" (Assemble the Players, The Fourth Doctor) and the longer "Once
// during each of your turns, …" synonym both lower to OncePerTurn; absence
// keeps the Unlimited shape (Realmwalker, Future Sight, Crystal Skull).
// A prefix strip removes nothing from the end, so the suffix of text
// (original case) stays aligned with the post-strip lower; the
// text-based alt-cost/condition rider scans below remain correct.
let (text, lower, frequency) = if let Some(r) = nom_tag_lower(lower, lower, "once each turn, ")
.or_else(|| nom_tag_lower(lower, lower, "once during each of your turns, "))
{
let stripped_len = lower.len() - r.len();
(&text[stripped_len..], r, CastFrequency::OncePerTurn)
} else {
(text, lower, CastFrequency::Unlimited)
};

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

Parse changes introduced by this PR · 4 card(s), 9 signature(s) (baseline: main 2f8bb7e537f5)

1 card(s) · static/TopOfLibraryCastPermission(Cast,freq=once_per_turn) · added: TopOfLibraryCastPermission(Cast,freq=once_per_turn) (affects=card)

Examples: Cemetery Illuminator

1 card(s) · static/TopOfLibraryCastPermission(Cast,freq=once_per_turn) · added: TopOfLibraryCastPermission(Cast,freq=once_per_turn) (affects=instant or sorcery)

Examples: Johann, Apprentice Sorcerer

1 card(s) · static/TopOfLibraryCastPermission(Cast,freq=once_per_turn) · added: TopOfLibraryCastPermission(Cast,freq=once_per_turn) (affects=power ≤2 creature)

Examples: Assemble the Players

1 card(s) · static/TopOfLibraryCastPermission(Cast,freq=once_per_turn) · removed: TopOfLibraryCastPermission(Cast,freq=once_per_turn) (affects=card)

Examples: Cemetery Illuminator

1 card(s) · static/TopOfLibraryCastPermission(Cast,freq=once_per_turn) · removed: TopOfLibraryCastPermission(Cast,freq=once_per_turn) (affects=instant or sorcery)

Examples: Johann, Apprentice Sorcerer

1 card(s) · static/TopOfLibraryCastPermission(Cast,freq=once_per_turn) · removed: TopOfLibraryCastPermission(Cast,freq=once_per_turn) (affects=power ≤2 creature)

Examples: Assemble the Players

1 card(s) · static/TopOfLibraryCastPermission(Play,freq=once_per_turn) · added: TopOfLibraryCastPermission(Play,freq=once_per_turn) (affects=historic land or historic card)

Examples: The Fourth Doctor

1 card(s) · static/TopOfLibraryCastPermission(Play,freq=once_per_turn) · removed: TopOfLibraryCastPermission(Play,freq=once_per_turn) (affects=historic land)

Examples: The Fourth Doctor

1 card(s) · trigger/when you do · added: when you do

Examples: The Fourth Doctor

2 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.

Reviewed current head ccbe83f6ac56352fd60d6fed3a3eb09102adcbbc with the maintainer bar: correct architectural seam, idiomatic/reasonable/maintainable implementation at that seam, and real proof with discriminating tests. CI is green, but this still needs changes.

  1. The Fourth Doctor is marked supported while the reflexive rider is knowingly not rules-correct in multi-permission games. In crates/engine/src/parser/oracle.rs, the new handler lowers “When you do” to a global TriggerMode::PlayCard scoped only by OriginConstraint::Equals(Zone::Library) and the permission’s affected filter. The adjacent comment explicitly says this cannot distinguish which permission authorized the play. That is not just a documentation limitation: TopOfLibraryCastPermission already selects/consumes an authorizing source for spell casts, and top_of_library_permission_source intentionally prefers an unlimited permission when one exists. The synthesized trigger bypasses that provenance, so it can still fire when another top-of-library permission, not The Fourth Doctor’s once-per-turn permission, authorized the play. If the provenance is not modeled yet, the rider should remain an honest unsupported gap rather than making The Fourth Doctor parse cleanly.

  2. Top-of-library land plays do not consume the once-per-turn library permission. The new static uses play_mode: Play with frequency: OncePerTurn, but handle_play_land only calls top_of_library_land_playable_by_permission(...).is_some_and(...) and records graveyard/exile permission usage later. There is no matching record for top_of_library_cast_permissions_used on the land path, while spell casts do record the selected top-of-library source in casting_costs.rs. That means “Once each turn, you may play a historic land ... from the top of your library” is not actually once per turn for lands. The parser tests only inspect the lowered shape, so they do not catch this runtime contract.

  3. The new parser branch mutates shared parse context without restoring it. In oracle.rs, the handler sets ctx.subject = None and ctx.actor = None before parsing the rider. If rider parsing fails, the line falls through to later parser priorities with the context already cleared; if it succeeds, the same ctx is reused for subsequent lines. Please scope this with save/restore around the rider parse, or use an isolated context if that is the intended semantics.

  4. The frequency-prefix strip desynchronizes text and lower. In try_parse_top_of_library_cast_permission, the prefix is stripped only from lower, but the unstripped text is passed to branch parsers and stored as the static description. The comment says the suffix remains aligned, but the helper API still has paired original/lower inputs and this creates exactly the class of original/lower drift Gemini called out. Strip both sides together, or use an existing paired-text helper so downstream scans/descriptions reflect the same slice.

The fix can either implement permission provenance for the reflexive trigger and land-play frequency consumption, or keep the unsupported rider honest until that runtime seam exists. As-is, the PR passes parser-shape tests while accepting a rules-bearing approximation as supported.

…and-play frequency, text/lower desync, context restore

Maintainer @matthewevans raised four issues on PR phase-rs#4341 (The Fourth Doctor
parser cluster). All four are addressed here.

1. Reflexive trigger provenance (issue #1):
   Replace the rules-incorrect `TriggerMode::PlayCard` trigger with a
   `TriggerMode::Unknown` gap marker. A global PlayCard trigger cannot
   distinguish which permission authorized a given play (CR 603.12), so
   the "When you do" rider was firing even when a different top-of-library
   permission authorized the play. The Unknown trigger keeps the gap
   visible in coverage until the casting/land-play pipeline gains
   permission-provenance tracking.

2. Land-play frequency consumption (issue phase-rs#2):
   Add `record_top_of_library_land_permission` in engine.rs and call it
   from all three completion arms of `handle_play_land`. A
   `OncePerTurn` `TopOfLibraryCastPermission { play_mode: Play }` now
   consumes the per-turn slot on land plays, mirroring how
   `finalize_cast` records this for spell casts (CR 401.5 + CR 601.2a).

3. Context mutation without restore (issue phase-rs#3 / Gemini):
   The reflexive rider is no longer parsed at all (Unknown trigger is
   emitted unconditionally), eliminating the ctx.subject/ctx.actor
   mutation that was leaking into subsequent lines.

4. text/lower desync on frequency-prefix strip (issue phase-rs#4 / Gemini):
   In `try_parse_top_of_library_cast_permission`, shadow both `text` and
   `lower` together when stripping the "once each turn, " / "once during
   each of your turns, " prefix, so downstream helpers receive aligned
   slices (Gemini phase-rs#3 suggestion, CR 601.2a).

5. Redundant scan_contains pre-check removed (Gemini phase-rs#2):
   The `scan_contains` guard before `split_once_on_lower` in oracle.rs
   was redundant — `try_parse_top_of_library_cast_permission` already
   validates the anchor. Removed for clarity.

Test: `the_fourth_doctor_full_card_parse` updated to expect
`TriggerMode::Unknown` instead of `TriggerMode::PlayCard`.

Verification: cargo clippy -p engine --all-targets -- -D warnings: clean.
cargo test -p engine: all pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

VERDICT: request-changes

Current-head recheck after the follow-up. The honest unsupported reflexive rider, parser context restore, and text/lower frequency-prefix blockers look addressed, but the land-play frequency blocker is still not fixed.

[HIGH] Top-of-library land plays still do not consume the once-per-turn permission. The new recorder recomputes top_of_library_permission_source at crates/engine/src/game/engine.rs:5433, and that helper reads the current library front at crates/engine/src/game/casting.rs:2763-2764. The normal land-play path only calls the recorder after the zone delivery has moved the land out of the library (engine.rs:5742-5753, then engine.rs:5861-5864). At that point library.front() is the next card, so top_id != object_id and the helper returns without inserting the permission source into top_of_library_cast_permissions_used. The replacement-done path has the same post-delivery ordering at engine.rs:5798-5801. This preserves the original bug: a OncePerTurn top-library land permission can be reused because the land play never spends the source.

Please capture the authorizing source/frequency before moving the land, alongside the existing in_library_with_permission check, and pass that captured source into the commit paths. This also needs a production-path regression around GameAction::PlayLand proving the used set is populated or that a second same-source top-library land play is rejected.

No approval/label/enqueue from the sweep.

@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] Top-of-library land plays still do not consume the once-per-turn slot on the normal successful land-play path. Evidence: record_top_of_library_land_permission recomputes the authorizing permission by calling top_of_library_permission_source and then requires the current library top to still equal the played object (crates/engine/src/game/engine.rs:5720-5728), but the normal path calls it only after the replacement pipeline has delivered the Library → Battlefield move and stamped the played land (crates/engine/src/game/engine.rs:6062-6067, crates/engine/src/game/engine.rs:6145-6154). At that point top_of_library_permission_source reads player.library.front() (crates/engine/src/game/casting.rs:2771-2772), so the played land is no longer the top card and the helper returns before inserting into top_of_library_cast_permissions_used. Why it matters: the previous blocker remains for ordinary top-of-library land plays; a Fourth Doctor-style once-per-turn historic land play can be used repeatedly if no choice/replacement pause keeps the object on top. Suggested fix: capture the selected top-of-library permission source/frequency before moving the land, then consume that captured source in the land-play epilogue, and add a runtime test that plays two matching lands from the top under a OncePerTurn play_mode: Play permission.

The `record_top_of_library_land_permission` helper was called in the
land-play epilogue — after `zone_pipeline::deliver` had already moved the
land from Library → Battlefield. At that point `top_of_library_permission_source`
reads `player.library.front()`, which now points to the *next* card, so
`top_id != object_id` always triggered and the slot was silently left
unconsumed. A `OncePerTurn` top-of-library `Play` permission (The Fourth
Doctor shape) could therefore be reused indefinitely for lands.

Fix: capture `(src_id, frequency)` from `top_of_library_permission_source`
*before* the replacement pipeline, alongside the existing
`in_library_with_permission` eligibility check. The simplified
`record_top_of_library_land_permission` now accepts the pre-captured pair
instead of re-deriving it post-delivery. All three epilogue paths
(Execute + `NeedsChoice` replacement prompt, `NeedsChoice` directly, and
the normal path) use the captured value.

Also adds `once_per_turn_library_land_play_consumes_slot_and_blocks_second_play`,
a production-path regression test that plays two historic lands from the
top of the library under a `OncePerTurn` `play_mode: Play` permission,
asserts the first play stamps `top_of_library_cast_permissions_used`, and
asserts the second play is rejected — the discriminating assertion that
the pre-capture bug made impossible to satisfy.

Addresses matt's round-4 review blocker (2026-06-28).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

[LOW] Land-play permission consumption cites a spell-casting rule that does not describe this path. Evidence: crates/engine/src/game/engine.rs:5710 annotates the top-of-library land-play permission recorder with CR 601.2a, but CR 601.2a is the spell-to-stack proposal rule; this path is playing a land as a special action from the library. Why it matters: this repo treats CR annotations as verified evidence, and a wrong CR citation creates false confidence around the rules authority for the implementation. Suggested fix: replace the spell-casting citation with the land/special-action/library-top rules that actually govern this path, e.g. CR 305.1 + CR 401.5, and update nearby repeated comments/tests consistently.

# Conflicts:
#	crates/engine/src/game/engine.rs
#	crates/engine/src/parser/oracle_static/tests.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.

[LOW] The land-play consumption path still cites the wrong CR rule. Evidence: crates/engine/src/game/engine.rs:5117, crates/engine/src/game/engine.rs:5251, and crates/engine/src/game/engine_tests.rs:7650 cite CR 601.2a for playing a land from the library. Why it matters: CR 601.2a is the spell-casting-to-stack procedure; land play is governed by CR 305.1, CR 116.2a, and top-library visibility by CR 401.5, so the current annotation points maintainers at the wrong authority. Suggested fix: replace the CR 601.2a annotations with verified land-play / special-action / top-library visibility rules that describe this path.

Reviewed current head c3c38baebe9413d2f367fab8f6820462376de572.

@matthewevans matthewevans added the bug Bug fix label Jul 1, 2026
…land-play paths

Matt's 2026-07-01 review (CHANGES_REQUESTED) identified that all six
CR 601.2a annotations in the top-of-library land-play path cite the
spell-casting-to-stack procedure (601.2a) instead of the rules that
actually govern this path:
  - CR 305.1: playing a land is a special action, not a spell cast
  - CR 116.2a: playing a land puts the card onto the battlefield from
    the zone it was in (zero stack involvement)
  - CR 401.5: top-of-library visibility closes after the special action

Fixed locations:
  - engine.rs: doc-comment on record_top_of_library_land_permission (×1)
  - engine.rs: inline comment in handle_play_land pre-capture block (×1)
  - engine.rs: three call-sites in the success/NeedsChoice/fallthrough
    branches of handle_play_land that record the permission slot (×3)
  - engine_tests.rs: doc-comment on the once-per-turn slot regression
    test (×1)

All six now cite CR 305.1 + CR 116.2a + CR 401.5 with explanatory text.
The two remaining mentions of CR 601.2a in the updated comments are
explicit "does not apply here" notes, which is the correct documentation.

Verification: cargo clippy -p engine --all-targets -- -D warnings (clean),
cargo test -p engine (all pass), cargo fmt --all (no changes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 parse-diff evidence is far outside the PR's claimed scope.

The current <!-- coverage-parse-diff --> sticky for head 11fdea04813ff0332c1d474b21e1b3ac1c3a862e reports 300 cards / 226 signatures, including 37 trigger/ChangesZoneAll cards changing active-zone output. The PR body says this fixes one card, The Fourth Doctor.

That is not mergeable evidence for a parser PR. Either the branch is carrying baseline contamination or the parser change is affecting hundreds of unrelated cards; in both cases, the current head needs to be rebased/updated and the parse diff reduced to the intentional Fourth Doctor scope before this can be approved.

@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 c1cd0d376977e3e53090e38b6d588ce4244cb4ac:

I do not see a new implementation blocker in the current code, but the parser blast-radius proof is still not current for this head. This PR changes parser source, while the coverage-parse-diff sticky was last updated before the current head and still reports a very broad diff (300 card(s), 226 signature(s)). Please regenerate/update the parse-diff sticky for this exact head and either narrow or explicitly justify changes outside The Fourth Doctor scope.

@matthewevans matthewevans self-assigned this Jul 4, 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.

Maintainer sign-off on current head c1cd0d376977e3e53090e38b6d588ce4244cb4ac.

The prior land-play permission-consumption and CR annotation blockers were addressed before this head. CI is green and contributor-proof is satisfied. The refreshed parse-diff sticky is current for this head and is narrowed from the earlier 300-card contaminated output to 4 cards: The Fourth Doctor has the intended semantic change, while the other listed top-of-library permissions are identical add/remove signature churn in the parse-diff display rather than broadened parser behavior.

@matthewevans
matthewevans added this pull request to the merge queue Jul 4, 2026
@matthewevans matthewevans removed their assignment Jul 4, 2026
Merged via the queue into phase-rs:main with commit f582ccb Jul 4, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants