fix(engine): read entered-this-turn quantities from the entry ledger (BB-FU10) - #6323
Conversation
…(BB-FU10)
`parse_entered_this_turn_ref` emitted the live-board `QuantityRef::EnteredThisTurn`
for Oracle text whose "entered the battlefield under your control this turn" clause
is an entry event, so a permanent that entered this turn and has since left stopped
counting — contradicting CR 608.2i (look-back).
Migrate only the entry-event surface to the ledger-backed
`QuantityRef::BattlefieldEntriesThisTurn { player, filter }`. The subject-noun
surface (Tromell) stays on the live read per CR 608.2h; the discriminator is the
controller attachment site, not the substring "the battlefield".
The migration exposed three defects in the entry-record matcher, all repaired here:
- `TargetFilter::Or`/`And` fell to a fail-closed wildcard, so composite filters
counted 0. Making them monotone unlocks three shipped cards, two of which gain a
previously-impossible activation (driven, not asserted).
- `TypeFilter::Subtype` compared strings directly, bypassing
`subtype_matches_with_changeling` — the declared single authority — so a
changeling entrant stopped counting for exactly the cards this change migrates
(CR 702.73a). Now routed through it, mirroring
`zone_change_record_matches_type_filter`.
- Filters carrying a `FilterProp` the entry snapshot cannot answer were emitted
anyway and resolved to a silent constant 0. They now refuse the parse and lower
to an honest `Effect::Unimplemented`.
Reclassify `BattlefieldEntriesThisTurn` as `sibling: true` for the CR 732.2a loop
firewall. `analysis/resource.rs` already clears the entry journal as a loop-pumped
append-only event, so the previous `sibling: false` let the engine hand out a false
infinite-loop certificate. This suppresses a shortcut offer for three shipping
permanents; restoring them under a narrower predicate is tracked as BB-FU10-N and
pinned by a driven regression test.
Assisted-by: ClaudeCode:claude-opus-4-8
|
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 (15)
📝 WalkthroughWalkthroughThe change migrates qualifying entered-this-turn quantities to battlefield-entry ledger reads, adds runtime-aware subtype matching and fail-closed coverage checks, updates loop analysis, and adds parser, engine, and integration regressions for departed entries and object-growth suppression. ChangesBattlefield Entry Ledger Semantics
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
matthewevans
left a comment
There was a problem hiding this comment.
Reviewed the current head.
I found no additional concrete correctness blocker in the migrated entry-event path. The ledger migration, the preserved live subject-noun path, and the entry-record filter guard follow the existing condition-side model.
This remains unapproved pending the in-progress CI and current-head card-data/coverage evidence. No local build was run.
Parse changes introduced by this PR · 4 card(s), 4 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Approved: the entry-event grammar now selects the ledger only when control modifies the entry event, retains the live subject-noun path, and exercises current-head parser, quantity, activation, and layer-invalidation paths. The parse-diff’s four affected cards match the migrated semantic shape; no quality label is requested because the claimed and measured card sets differ.
…(BB-FU10) (phase-rs#6323) `parse_entered_this_turn_ref` emitted the live-board `QuantityRef::EnteredThisTurn` for Oracle text whose "entered the battlefield under your control this turn" clause is an entry event, so a permanent that entered this turn and has since left stopped counting — contradicting CR 608.2i (look-back). Migrate only the entry-event surface to the ledger-backed `QuantityRef::BattlefieldEntriesThisTurn { player, filter }`. The subject-noun surface (Tromell) stays on the live read per CR 608.2h; the discriminator is the controller attachment site, not the substring "the battlefield". The migration exposed three defects in the entry-record matcher, all repaired here: - `TargetFilter::Or`/`And` fell to a fail-closed wildcard, so composite filters counted 0. Making them monotone unlocks three shipped cards, two of which gain a previously-impossible activation (driven, not asserted). - `TypeFilter::Subtype` compared strings directly, bypassing `subtype_matches_with_changeling` — the declared single authority — so a changeling entrant stopped counting for exactly the cards this change migrates (CR 702.73a). Now routed through it, mirroring `zone_change_record_matches_type_filter`. - Filters carrying a `FilterProp` the entry snapshot cannot answer were emitted anyway and resolved to a silent constant 0. They now refuse the parse and lower to an honest `Effect::Unimplemented`. Reclassify `BattlefieldEntriesThisTurn` as `sibling: true` for the CR 732.2a loop firewall. `analysis/resource.rs` already clears the entry journal as a loop-pumped append-only event, so the previous `sibling: false` let the engine hand out a false infinite-loop certificate. This suppresses a shortcut offer for three shipping permanents; restoring them under a narrower predicate is tracked as BB-FU10-N and pinned by a driven regression test. Assisted-by: ClaudeCode:claude-opus-4-8
🤖 AI text below 🤖
Summary
parse_entered_this_turn_refemitted the live-boardQuantityRef::EnteredThisTurnfor Oracle text whose "entered the battlefield under your control this turn" clause is an entry event, so a permanent that entered this turn and has since left stopped counting — contradicting CR 608.2i (look-back). This migrates only the entry-event surface to the ledger-backedQuantityRef::BattlefieldEntriesThisTurn { player, filter }, leaves the subject-noun surface (Tromell, CR 608.2h live set) alone, and repairs three defects the migration exposed in the entry-record matcher.Implementation method (required)
Method: /engine-implementer
CR references
CR 608.2i(look-back — the migration's basis),CR 608.2h(once-determined live set — why Tromell must NOT migrate),CR 702.73a(Changeling CDA),CR 205.3m(creature-type namespace),CR 403.3(permanents exist only on the battlefield),CR 109.1,CR 109.2,CR 109.5,CR 111,CR 400,CR 102.2,CR 102.3,CR 205.4b,CR 308.1,CR 603.3b,CR 603.6a,CR 611.3a,CR 701.21,CR 732.2a.Every number was grep-verified against
docs/MagicCompRules.txtbefore being written; the whole-diff gate reports zero unverified. The pre-existing// CR 608.2b: Disjunctionmiscitation atfilter.rs:2833was deliberately not propagated (neither 608.2b nor 608.2i is a disjunction rule).Verification
Required checks ran clean, or the exact CI-owned alternative is stated below.
Gate A output below is for the current committed head.
Final review-impl below is clean for the current committed head.
Both anchors cite existing analogous code at the same seam.
cargo fmt --all --check— exit 0cargo clippy --all-targets -- -D warnings— exit 0, zero diagnosticscargo nextest run -p engine— 21258 passed / 0 failed / 8 skippedcargo nextest run -p engine -E 'test(combo_infinite_pile) | test(kilo_live_offer_from_real_dump) | test(sprout_inalla_realistic_offer) | test(loop_shortcut)'— 96/96 passed./scripts/check-parser-combinators.sh— exit 0 (Gate G + Gate A PASS)cargo combo-verify— 13 confirmed / 4 gated / 37 deferred / 0 failed (of 54); all 54 rows row-for-row identical to baseBASE-vs-POST
data/card-data.json— 35477 cards both sides, 0 added, 0 removed, 5 changed (see Claimed parse impact)BASE-vs-POST
data/coverage-data.json—supported_cards31504 both sides, supported-flip multiset diff 0 / 0FORGE_TEST_FULL_DB=1 … ordering_parity_sweep—unexplained=18on both base and head, all metrics and all 18 rows byte-identical (delta zero). Pre-existing Urza-block failure, opt-in gate, not default CI.Base and head card-data were generated from md5-identical MTGJSON inputs (
AtomicCards.json0dd2d256…, verified after both runs), so every delta above is code-caused, not input drift.Gate A
Gate A PASS head=96d5a3b0b434052651e4a95a77941839bcd7fb18 base=836ff312ae2073c99af28d286b0c4915faa8a458
Anchored on
crates/engine/src/game/filter.rs:2840—zone_change_record_matches_type_filter: the existing snapshot-record type-filter matcher that already threadsall_creature_typeslive against a snapshotted record. Step 8 mirrors it exactly.crates/engine/src/parser/oracle_nom/condition.rs:7725—parse_or_more_entered_count: BB-FU1's already-shipped condition-side emitter of the same ledger variant, i.e. the sibling surface this PR's quantity-side migration matches.Final review-impl
Final review-impl PASS head=96d5a3b0b434052651e4a95a77941839bcd7fb18
Claimed parse impact
Three further cards change behavior with no AST change, so they cannot appear in a card-data diff — measured and confirmed absent from it:
They benefit from the
TargetFilter::Orrepair in the entry-record matcher; two of them gain a previously-impossible activation. The unlock is driven, not asserted:bbfu10_lilypad_village_activation_unlocked_by_composite_fixdrivescheck_activation_restrictionsand measures ILLEGAL → LEGAL.Disclosures
1. A CR 732.2a firewall reclassification suppresses three shipping combo offers.
ability_scan.rsclassifiedBattlefieldEntriesThisTurnassibling: false, whileanalysis/resource.rsalready clearsbattlefield_entries_this_turnas "append-only event journals a loop pumps". Those two statements contradict each other, and the combination lets the engine hand out a false ∞ certificate today. This PR flips the axis tosibling: true, which is what the module's own ⛔ INVARIANT requires.Measured cost: three battlefield permanents — Park Heights Pegasus, Smuggler's Share, and The Prydwen, Steel Flagship (one card; the comma is part of its name) — stop receiving a CR 732.2a loop-shortcut offer they receive today. This is a deliberate, disclosed trade, not an oversight; restoring them under a narrower predicate is tracked as follow-up BB-FU10-N.
loop_shortcut.rscarries a driven regression test that asserts the suppression and names BB-FU10-N as the item that will flip it back.DB-wide census (measured at the pre-rebase base
e48c7d6e1; the suppression itself is pinned at the current head by a committed driven test): 35454 faces,sib_true16939 → 16945; the six added faces are exactly the predicted set and the removed set is empty. Step 1 in isolation measures 16937 — it relaxes Geralf and Hobgoblin Bandit Lord, which is precisely the false-certificate relaxation Step 0c closes.2. Not fixed, and deliberately so. The entry-record snapshot is taken pre-layer, so only Changeling is repairable from it (the keyword travels; granted subtypes do not). 192 MTGJSON cards grant types via continuous effects and remain invisible to any snapshot-only matcher.
ParsedCondition::YouHadAngelOrBerserkerEnterThisTurnstill compares subtypes with a bareeq_ignore_ascii_case, so Cleaving Reaper's Changeling axis stays wrong — that arm never routes through the matcher, so this PR's chokepoint cannot reach it. Our Cleaving Reaper claim above is about the ledger-count axis only.3. Coverage classification is filter-aware now, but only where the classifier looks.
extract_card_featureswalks static-ability conditions and effect positions (21 corpus cards reach the ledger arm, allHandled) but treats a trigger intervening-if as opaque, so it cannot see 20 measured trigger-side cards. Tunnel Tipster —face-down creature entered … this turn— is therefore still silentlysupported=truewith a constant-false condition. That is a pre-existing BB-FU1-era defect, disclosed here, not introduced and not fixed.4. Instrument blindness, stated plainly. The combo corpus contains none of the three suppressed cards, so
combo-verify's row-for-row identity is expected and is not evidence that no offer was suppressed. The suppression was measured directly by DB census and by a driven fixture instead.Re-verified after rebase
Rebased onto
upstream/main=8fe91f145(29 commits). Every gate above was re-measured at the rebased head against a freshly generated base, and every result is identical to the pre-rebase measurement — including the blast radius holding at exactly the same 5 cards, which was the drift most worth checking given the upstream tip (fix(parser): stamp Permanent on duration-less additive type grants) touches this PR's seam. A dedicated review pass confirmed no semantic interaction: this PR'scondition.rsdiff is comment-only, andentry_type_filter_matchesreads the pre-layer entry snapshot, which never observed the grant under either duration.Summary by CodeRabbit
New Features
Bug Fixes
Documentation