Fix Trade Caravan#5
Open
keloide wants to merge 26 commits into
Open
Conversation
…copies (phase-rs#6276) * feat(engine,client): badge permanents that a copy effect turned into copies A Phantasmal Image that entered as a copy of Reveillark rendered identically to the real Reveillark — same art, same name, same P/T — so the board gave no way to tell them apart (phase-rs#5932). The board already had a "Copy" badge, but it was gated on a TOKEN-copy heuristic: obj.is_token === true && obj.display_source !== "Token" && !obj.face_down A real card under a copy effect is not a token, so it never qualified. No serialized signal existed for the frontend to use instead. `is_copy` means "not represented by a card" (CR 707.10) and is deliberately cleared when a copy resolves onto the battlefield (`game/stack.rs`, `game/sba.rs`), so it is correctly false here. The copy itself lives in a Layer 1a `CopyValues` continuous effect (CR 613.2a + CR 707.2), not on the object, and the copy overrides `printed_ref` so even image lookup follows the copied card. So the engine now classifies it: `DerivedViews::copied_permanents` lists battlefield permanents whose copiable values a live copy effect supplies, matched through the same `matches_target_filter` the layer engine uses to pick an effect's recipients — the projection and the effect that actually rewrote the object can never disagree. The client unions it with the existing token-copy case, so the badge now covers the whole class: Clone, Vesuvan Doppelganger, and every "enters as a copy" permanent, not just token copies. CR 708.2: face-down permanents are excluded on BOTH sides, since their characteristics are only those the face-down rules grant and surfacing "Copy" would leak what one really is. Neither side alone can leak it. Tests: engine coverage for the projection (flagged under a live effect, empty on an ordinary board, never for a face-down permanent) and client coverage for the badge, including the face-down guard. The positive client test is discriminating — restoring the old token-only condition fails it while the two guards stay green. Closes phase-rs#5932 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(engine): gate the copy badge on the layer engine's liveness predicate Review blocker: the projection treated a STORED copy effect as a live one. A lapsed effect stays in `transient_continuous_effects` until it is swept, so Zygon Infiltrator's "for as long as that creature remains tapped" copy ended the moment its target untapped while the permanent kept displaying the badge. `gather_transient_continuous_effects` already knew the answer — it rejects a departed `UntilHostLeavesPlay` host, a `ForAsLongAs` duration that no longer holds, and a false source condition. Those three gates are now extracted into `transient_effect_is_live`, which both the layer engine and `derive_views` call, so a display projection cannot claim an effect is live after the layer engine has stopped applying it. Extracted rather than duplicated: a second copy of the predicate would be free to drift, which is exactly the class of bug this review caught. `gather_transient_continuous_effects` keeps its existing behaviour — the condition gate now runs inside the shared predicate, and the retained recipient-context condition is unchanged. Also from review: * `copied_permanents` is sorted, matching what its doc comment already claimed. * The client's duplicated `!face_down` check is factored out so the CR 708.2 guard leads and covers both copy sources. Tests: a derived-view regression builds Zygon's target-relative `ForAsLongAs` copy, asserts the badge shows while the target is tapped, then untaps it and asserts the badge is gone while the effect is still stored — so it exercises liveness rather than removal. It fails without the gate. Verified: 17442 lib tests, 3676 integration tests, clippy -D warnings, all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(PR-6276): exclude merge layer effects from copy badges A merge representation uses CopyValues for its top component but is not a copy effect. Exclude only the tracked merge layer effect and cover the derived view. Co-authored-by: minion1227 <romantymkiv1999@gmail.com> * test(PR-6276): preserve copy badges after merge representation Prove the merge-layer exclusion is scoped to its tracked effect ID, so an independent copy effect on the merged host remains visible. Co-authored-by: minion1227 <romantymkiv1999@gmail.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…hot vintage-proof (phase-rs#6274) * fix(engine): commit W30 MTGJSON token catalog and make coverage snapshot vintage-proof The weekly MTGJSON refresh introduced by phase-rs#6237 (vintage gate 2026-07-20) unfroze a stale token catalog cache. Deterministic regen (byte-identical across two checkouts) against the 2026-07-20 MTGJSON data adds 13 tokens (12 Secret Lair Drop printings: Food x8, Treasure, Ooze, Elf Warrior x2; 1 HOB Goblin Army) and 1184 [[token.source_card_refs]] (8637 -> 9821), and picks up MTGJSON's face-name change 'Undercity // The Initiative' -> 'Undercity'. No tokens removed; coverage stays complete (2858/2858 supported, 1490/1490 rules-text tokens parsed). analyze_token_coverage_treats_source_defined_pt_as_represented pinned vintage-dependent absolutes (2845/1480), which go red every week upstream adds tokens even at 100% coverage. Replace them with invariants (supported == total, parsed == rules_text) plus non-vacuity floors at the last-known-good baseline so an empty or truncated catalog cannot pass vacuously. Assisted-by: ClaudeCode:claude-fable-5 * test(engine): ratchet token-catalog floors to the committed vintage Review feedback (phase-rs#6274): the non-vacuity floors shipped at 2845/1480 sat exactly at the PRE-refresh catalog, so reverting this PR's regen still passed every assertion — the guard could not detect losing the refresh it was added alongside. The pre-PR test asserted the parent counts as equalities (assert_eq!(total_tokens, 2845)) and was green on main, so the parent catalog measures exactly those floors. Ratchet all floors to the committed vintage (2858/1490) and add the third axis already carried on the summary, source_card_refs >= 9821. `>=` still keeps weekly upstream ADDITIONS green — the false-red phase-rs#6237 introduced — while any shrink now fails. The ref floor is the load-bearing one and is deliberately tight: across the ten recorded revisions of known-tokens.toml, tokens and rules_text are monotone, and source_card_refs shrank exactly once — phase-rs#6199, a parser PR that silently dropped 1173 token<->card links (9810 -> 8637) while the token count GREW past a count-based floor. This regen is what repaired it. Each floor is probed in isolation, since sequential assert!s let an earlier conjunct dominate a later one and render it vacuous: - catalog reverted to f0ac543^ -> FAIL "token catalog shrank: 2845 presets < 2858" - 1 rules_text line removed -> FAIL rules_text_tokens >= 1490 - 5 source_card_refs blocks removed -> FAIL source_card_refs >= 9821 Catalog restored byte-identically after each probe. Floors carry the measured count in the panic so a CI red names the axis and the delta instead of requiring a local repro. Assisted-by: ClaudeCode:claude-opus-4.8 * docs(engine): document the provenance seam for the token-catalog floors The floors observe totals; provenance (committed catalog == tokens-gen output for the declared MTGJSON vintage) is structurally untestable at test time (the ~560 MB gitignored generator input is absent; the test sees build.rs's embed of the tracked file) and is verified at the generation seam instead (gen-card-data.sh temp+cmp+vintage gate). Comment documents the seam and the by-hand reproduction recipe, per review 4746023659. Assisted-by: ClaudeCode:claude-opus-4.8
…ring mana payment (phase-rs#5963) (phase-rs#6272) * fix(engine): fire sacrifice/death triggers for mana abilities paid during mana payment (phase-rs#5963) A mana ability with a non-tap cost (e.g. Gilded Goose's "{T}, Sacrifice a Food: Add {C}") activated during mana payment for a spell silently dropped every observer of its cost. Sacrificing a creature-that-is-a-Food never fired Scavenger's Talent's "creatures you control die" (L1) or "you sacrifice a permanent" (L2) triggers. The PayCost -> CostResume::ManaAbility arm never scanned its cost events for triggers. The post-action trigger pipeline only runs for Priority resumes (guarded by waiting_for == Priority), so a ManaPayment/UnlessPayment resume lost the events. This mirrors the inline scans already done by the ChooseManaColor and mid-payment ActivateAbility arms: snapshot events before the cost handler, process_triggers on the new events, surface any OrderTriggers prompt, and claim the scan for Priority resumes so the pipeline does not double-fire. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(engine): move mana-ability cost trigger scan to the settlement authority (phase-rs#5963) The initial fix scanned mana-ability cost events in engine.rs's PayCost -> CostResume::ManaAbility arm. That was the wrong layer and double-fired: finish_mana_ability_cost_payment is the single settlement authority for mana-ability cost events and already scans them for the ChooseManaColor path, so an extra scan in the reducer double-counted Kilo's becomes-tapped proliferate trigger under a standalone Relic activation (Pentad grew 3->5 instead of 3->4, breaking kilo_accept_marks_pentad_charge_as_unbounded_display_target). Revert the engine.rs scan and fix the real gap inside finish_mana_ability_cost_payment: a direct (non-paused) mana ability whose root resume is not Priority (ManaPayment/UnlessPayment, i.e. activated during mana payment) never reaches the post-action trigger pipeline, so its already-paid sacrifice/discard/exile cost events were dropped. Scan them at the settlement authority for non-Priority resumes only; Priority resumes stay with the pipeline so nothing double-fires. Co-authored-by: Cursor <cursoragent@cursor.com> * fix * style(PR-6272): hoist mana payment test imports Co-authored-by: Andriy Polanski <andriy.polanski@gmail.com> --------- Co-authored-by: Andriy Polanski <andriy.polanski@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…#6278) The engine embeds three git-tracked, generated data files at compile time (known-tokens.toml, oracle-subtypes.json, mtgjson-vintage) that were regenerated and committed by hand. When MTGJSON publishes new tokens/subtypes weekly they drift, which periodically turned CI red and required a manual catalog PR (e.g. phase-rs#6274). Add a scheduled workflow that force-refreshes MTGJSON, runs the single source-of-truth generator (scripts/gen-card-data.sh), and opens an auto-merge PR only when the tracked catalogs change. Mirrors refresh-feeds.yml. The vintage write-gate makes it self-correcting (no newer input -> empty diff -> no PR), so it is safe on all three triggers: weekly schedule, manual dispatch, and after the Clear Caches workflow (which only deletes caches; this re-fetches, re-processes, and commits the result). Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
… Caravan)
Trade Caravan's activated ability "Remove two currency counters from ~:
Untap target basic land. Activate only during an opponent's upkeep." dropped
its timing restriction to `Effect::Unimplemented`, leaving the ability
activatable at any time.
`parse_activation_during_role_gate` handled "during an opponent's turn" and
"during your upkeep" but not "during an opponent's upkeep". Rather than add a
`DuringOpponents*` sibling to `ActivationRestriction` (which expresses
opponent-turn scope via `RequiresCondition { Not(IsYourTurn) }`, not a
dedicated variant), compose from building blocks:
- Add `ParsedCondition::IsDuringUpkeep` (CR 503.1) — a scope-free upkeep-step
predicate, orthogonal to every existing `ParsedCondition` variant.
- Map "during an opponent's upkeep" to
`RequiresCondition { And([Not(IsYourTurn), IsDuringUpkeep]) }` (CR 602.5b +
CR 102.1 + CR 503.1), reusing the existing opponent-turn composition idiom.
- Evaluate `IsDuringUpkeep` against `state.phase == Phase::Upkeep`; classify it
memo-safe in the AI condition filter (reads only apply()-constant state).
Removes Trade Caravan from the parser-misparse backlog (root cause 28).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HwvJgpQpXqutChWLLLGB8E
Parse changes introduced by this PR · 3 card(s), 4 signature(s) (baseline: main
|
…dexing (Plan 04) (phase-rs#6269) * feat(engine): migrate ability continuation to resolution stack * feat(engine): migrate repeat-for to resolution stack * feat(engine): migrate repeat-until to resolution stack * feat(engine): migrate choose-one-of to resolution stack * feat(engine): migrate vote-ballot to resolution stack * feat(engine): migrate per-player zone choice to resolution stack * feat(engine): migrate per-category zone choice to resolution stack * fix(engine): retain per-player continuation below prompt * fix(engine): preserve nested resolution frame order * fix(engine): migrate resolution stack consumers * fix(engine): keep resolution frame authority documented * refactor(engine): migrate ChangeZone resolution frame * refactor(engine): migrate BatchDelivery resolution frame * refactor(engine): migrate CounterMoves resolution frame * refactor(engine): migrate CounterRemovals resolution frame * refactor(engine): migrate CounterAdditions resolution frame * refactor(engine): migrate CopyToken resolution frame * refactor(engine): migrate EachPlayerCopyChosen resolution frame * fix(engine): parent continuations beneath migrated frames * fix(engine): preserve nested tranche two frame order * fix(engine): retain devour owner at nested boundary * fix(engine): retain nested change zone owners * refactor(engine): migrate optional effect resolution frame * refactor(engine): migrate repeated optional payment frame * refactor(engine): migrate coin flip frame * refactor(engine): migrate proliferate frame * refactor(engine): migrate mutate merge frame * fix(phase-ai): read optional effect resolution frame * fix(engine): retain direct choice frame ownership * fix(engine): reject buried direct choice wire owners * fix(engine): preserve legacy parent wire ordering * refactor(engine): move draw replacement tails into frames * refactor(engine): move connive reentry into frames * refactor(engine): move life assignment into frames * refactor(engine): move spell resolution into frames * fix(engine): preserve optional frame takes * fix(engine): preserve replacement frame nesting * fix(engine): preserve post-replacement frame parents * fix(engine): resume paired draw continuations * fix(engine): retire paused replacement prompt frames * fix(engine): retain replacement context through promoted tails * test(engine): cover paired replacement elimination * refactor(engine): centralize resolution settlement checks * fix(server): hide resolution frames from player views * fix(engine): clear token seed after frame drain * docs(engine): clarify token seed drain authority * test(engine): cover resolution wire allocator migration * test(engine): pin resolution wire compatibility contract * ci: guard resolution-frame boundaries * fix(engine): bind inline mana overrides to trigger identity * perf(engine): index inline mana triggers * fix(ship): restore main's protocol 20 dropped by stale freeze rule The merge resolution pinned the application protocol at 19 per the Plan-04 charter's frozen-constants rule, but that rule predates main's shipped bump to 20 (actor-scoped priority passing, phase-rs#6213). Restore main's constants and version tests; keep the Phase-4 changelog note that the resolution-wire pinning added no protocol change of its own, and retarget the protocol ratchet script to 20. * fix(ci): drop ripgrep dependency from resolution-frame boundary guard The guard's embedded Python shelled out to 'rg -l' to list engine files containing legacy wire keys; ripgrep is not installed on CI runners, so the lint job died with FileNotFoundError after every lint step had passed. Scan with a pure-Python rglob over the same path and pattern instead. Red/green re-verified: clean tree passes, an injected legacy key in production code fails with exact path:line. --------- Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…tency (phase-rs#6286) The coverage snapshot's ratchet floors (phase-rs#6274) observe three totals, so they catch a catalog that shrank but not one that was hand-edited, spliced, or partially rewritten while keeping its counts. Add the two structural properties of tokens-gen output that ARE checkable at test time: - ids strictly ascending, matching tokens_gen.rs:106's `presets.sort_by(|a, b| a.id.cmp(&b.id))` - `token_image_ref.preset_id == id` wherever a ref exists, since the generator builds both from the same MTGJSON uuid Both are per-entry properties with no pinned totals, so a weekly refresh that only adds tokens stays green — the phase-rs#6237 false-red phase-rs#6274 removed does not come back. Neither establishes provenance (that needs the ~560 MB gitignored generator input, absent at test time); that limit is stated in-code. Consistency is asserted only where an image ref exists: a token with no Scryfall image is legal upstream data, and requiring one would re-introduce a weekly false-red. Scope is stated precisely in-code because it is narrow: neither assertion catches a body-only rewrite (nothing ties body/fidelity/source_card_refs to an identity) nor a deleted entry (removing from the middle keeps the sequence ascending) — deletion is the count floors' job. Also corrects known_token_presets()'s doc comment, which claimed presets are "sorted by category then id for stable display order". Both halves were wrong: tokens_gen.rs:106 sorts by id alone, and nothing displays in engine order — DebugCreateActions.tsx regroups by category and re-sorts by power/toughness/ name. The real reason for id order is a minimal diff on regen. Assisted-by: ClaudeCode:claude-opus-4.8
* feat(server): channel-aware startup data bootstrap and shell lifecycle flags Add configurable bind addressing for loopback shell launches. Add stdin-close lifecycle shutdown for orphan prevention. Add optional exact-Origin WebSocket handshake filtering. Add signed release/preview data bootstrap with verified downloads and dev-only fixture gating. * fix(server): best-effort draft-pools bootstrap, HTTP timeouts, https-only manifests * ci: slim signed server artifacts, content-addressed data, and continuous preview server pipeline Publish content-addressed draft pools in deploy and release data pipelines. Compute and export the deploy engine fingerprint, stamp the preview frontend environment, and dispatch the continuous preview server artifact workflow. Add three-platform preview server builds with Ubuntu minisign signing, signed manifest-last publication, and current-plus-previous R2 retention. Publish signed release data manifests and slim binary-only server assets while retiring bundled data archives. Remove server-data artifact plumbing and Docker data baking so server data bootstraps at runtime. Sequencing: merge together with, or after, the server bootstrap commit; slim artifacts without bootstrap support would strand fresh self-hosters. * fix(ci): globally serialize preview manifest publication * fix(ci): dispatch preview-server.yml from main, not a bare SHA workflow_dispatch refs must be a branch or tag — a 40-hex SHA fails with 422, so every deploy's preview-server dispatch would have silently missed (the failure is deliberately non-fatal). The exact source tree is already pinned by the 'commit' input the workflow checks out. Same pattern as nightly-release.yml. Also documents the preview image's PHASE_CHANNEL identity gap (GH phase-rs#6287) where the old comment claimed runtime bootstrap. --------- Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
* feat(desktop): native engine lifecycle — download, verify, spawn, adopt, GC (Workstream E) - Add typed remote ensure/stop commands and progress events.\n- Resolve release and preview artifacts with downgrade ratchets.\n- Verify signed manifests and server binaries with the pinned key.\n- Provision writable per-key data from a SHA-256 shared cache.\n- Manage adoption, stdin watchdog shutdown, single-instance, and manifest-diff GC.\n- Scope the commands to remote capabilities and refresh generated schemas. * fix(desktop): non-fatal native-engine GC, single-instance first, fast-fail health poll * test(desktop): de-flake migration temp dirs with a per-process sequence SystemTime nanos alone can collide across parallel test threads on coarse clocks, making concurrent tests share a temp directory and race the channel preference file. * fix(desktop): re-verify PID before SIGKILL, basename process match on BSD, drop redundant Windows pre-delete Addresses the three Gemini review findings on phase-rs#6283: PID-reuse race between TERM and KILL, ps -o comm= reporting bare executable names on BSD-derived systems, and delete-before-rename defeating crash atomicity on Windows (std::fs::rename already replaces the destination there). * fix(desktop): never kill an unowned spawn record on app exit stop_native_engine's exit path fell back to killing whatever PID the shared on-disk spawn record named, even when this process spawned no engine — a record another live instance owns. Today the single-instance secondary hard-exits inside plugin init (verified in tauri-plugin-single-instance 2.4.3 on all three platforms) so the path is dormant, but killing a process this instance did not spawn must never be exit-path behavior. Orphans already self-terminate via --exit-on-stdin-close; stale records resolve at the next ensure_native_engine adopt-or-kill. --------- Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…lity gate (phase-rs#6288) The Full-mode server's action-legality gate does an exact containment check against the enumerated candidate set, but candidate enumeration emits every cast-family action with CastPaymentMode::Auto — so any cast submitted with the Manual payment preference could never match, and manual-mana players could tap lands but never cast anything. CR 601.2g: the payment mode selects whether the engine auto-pays or pauses for manual mana activation — a payment preference, not part of the cast's legality. GameAction::with_canonical_payment_mode() (engine- owned, Cow-borrowing for the common case) erases Manual before the membership test; the applied action keeps the submitted mode. Fixes phase-rs#6275 Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…oors (phase-rs#6290) The refresh-card-data.yml test run regenerated known-tokens.toml from a clean, complete MTGJSON fetch (333/333 token sets, 0 failed) for vintage 2026-07-21, producing 8644 source_card_refs — vs 9821 in the hand-committed catalog (phase-rs#6274), which came from a developer's local data/mtgjson/sets/ dir carrying extra reprint set files beyond the reproducible fetch scope. Every token keeps its source-card names (103078 -> 103033 name-list lines, 0.04%) and >=1 printing; only redundant reprint scryfall_ids (e.g. akr/usg/j25 reprints) drop, which no consumer depends on. - Land the canonical 8644-ref catalog + vintage 2026-07-21. - Recalibrate the token_coverage floors from exact-pins (which rejected the correct clean regen) to catastrophic-loss backstops with headroom (total >= 2700, rules_text >= 1400, source_card_refs >= 8000). - Add a fetch-completeness gate to refresh-card-data.yml: refuse to open a catalog PR unless fetch-token-sets.sh reports 0 failed / 0 skipped, making provenance a build-time guarantee instead of relying on the engine floor to catch a partial regen after the fact. Supersedes the bot-opened phase-rs#6285 (stale base). Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…phase-rs#5955) (phase-rs#6291) Co-authored-by: Andriy Polanski <andriy.polanski@gmail.com>
…aphics bug at no performance cost (phase-rs#6293)
…to phase-server (phase-rs#6292) Two distinct causes broke the first executed deploy since phase-rs#6238/phase-rs#6282: 1. setup-rust-toolchain exports RUSTFLAGS='-D warnings' by default, and an env RUSTFLAGS overrides ALL .cargo/config.toml rustflags — silently dropping the [target.wasm32-unknown-unknown] 16 MiB shadow-stack link-arg phase-rs#6238 added, tripping build-wasm.sh's assert_wasm_stack guard. Pass rustflags: '' on every wasm-building job (deploy build-wasm, release wasm + broker-wasm) so config.toml stays authoritative. 2. phase-rs#6282 gave phase-server a rustls-only reqwest, but the CI builds run 'cargo build --bin phase-server' unscoped from the workspace root, so feature unification folds feed-scraper's native-tls reqwest features in, dragging openssl-sys into the musl cross-compile (no OpenSSL → build failure) and dynamic OpenSSL into the Docker image (runtime has no libssl). Scope every server build with -p phase-server: deploy, release (linux + matrix legs), Dockerfile compile stage, Tiltfile. Verified: cargo tree -p phase-server -i openssl-sys --target x86_64-unknown-linux-musl finds no path post-fix (present unscoped); rustflags input semantics confirmed against the action's action.yml. Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
) The tauri resource still built phase-server and copied it into client/src-tauri/binaries/ as a sidecar — removed with the thin shell (no externalBin remains in tauri.conf.json), so the cmd compiled a full engine tree to feed a path nothing reads. It also redeclared engine/AI/ wasm/server sources as deps, restarting the whole dev loop on changes tauri dev's own Rust watcher already handles. Local shell testing is the devUrl flow: tauri dev starts vite and hosts the LOCAL frontend in the shell window (frontend's auto_init already yields :5173 when tauri is enabled). Tilt now only restarts the loop on Tauri config/manifest changes. Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
) The Dispatch Existing Tag Recovery job runs gh workflow run without a checkout, so gh tried to infer the repository from a local git directory and failed with 'not a git repository' — the mode-D residual: every recovery dispatch for a stranded tag silently died (first hit: v0.32.0 after today's wasm-guard release failure). Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…he free review pass (phase-rs#6298) Gemini Code Assist has been sunset, so the review tooling was leaning on a bot that no longer answers. Replace that dead dependency without adding per-PR token cost by promoting the free external reviewer for this public repo. - pr-contribution-handler: rewrite "Gemini review handling" -> provider-neutral "Independent review pass". Drop the @gemini-code-assist trigger and quota logic (summoning a dead bot burns a round-trip and posts noise). CodeRabbit is the free external pass; local /code-review is a risk-scaled fallback only. - review-impl: step 4 now assumes NO external bot pre-screened the PR by default, so its own lenses are the complete review, not a supplement to a backstop. Generalize the remaining Gemini-specific mentions (COMMENTED/reviewDecision behavior, severity-reconcile checklist) to any bot. - Add .coderabbit.yaml: chill profile, generated-artifact path_filters ported from .gemini/config.yaml, and 10 layered path_instructions distilled from CLAUDE.md, the .gemini styleguide, and the review-impl lenses (Rust idioms, CR annotations, engine-owns-logic, parser nom mandate, enum parameterization, frontend display-purity + i18n, AI classifiers, transport hidden-info, feed safety, test adequacy). - Remove now-orphaned .gemini/config.yaml and .gemini/styleguide.md; their review substance is preserved in .coderabbit.yaml. The pr_review.py gemini-case-finding-refuted signal alias is intentionally kept: it normalizes historical events already in the append-only log. Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…sys feature unification (phase-rs#6299) Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…-rs#5952) (phase-rs#6296) * fix(engine): don't force a MustAttack carrier to attack itself (phase-rs#5952) * fix --------- Co-authored-by: Andriy Polanski <andriy.polanski@gmail.com> Co-authored-by: matthewevans <matthewevans@users.noreply.github.com> Co-authored-by: andriypolanski <andriypolanski@users.noreply.github.com>
…se-rs#6300) * fix(parser): gate DamageDone triggers on combat-status recipient (Kusari-Gama) * fix(PR-6300): harden blocking-recipient review Correct the blocking-creature CR citations and add a parsed-trigger-to-runtime-matcher regression that rejects player damage while accepting damage to a blocking creature. Co-authored-by: andriy-polanski <andriy-polanski@users.noreply.github.com> * test(PR-6300): cover nonblocking damage recipient Co-authored-by: andriy-polanski <andriy-polanski@users.noreply.github.com> --------- Co-authored-by: Andriy Polanski <andriy.polanski@gmail.com> Co-authored-by: matthewevans <matthewevans@users.noreply.github.com> Co-authored-by: andriy-polanski <andriy-polanski@users.noreply.github.com>
…lass) (phase-rs#6302) Self-library peek-and-cast cards (Kiora Sovereign of the Deep, Aetherworks Marvel, Construct a Cosmic Cube, Perception Bobblehead, Svella Ice Shaper, Velomachus Lorehold) previously routed "cast ... from among them" through the LingeringPermission driver: the looked-at cards were exiled, the free cast was never offered, and the rest were never bottomed. - Parser: seed chain_prior_self_library_peek from the raw bare-private-peek Dig shape via a single shared effect_is_bare_private_peek predicate (also used by the assembly pure-peek lowering, so the two sites cannot drift); flip the "from among them" fall-through to CastFromZoneDriver:: DuringResolution when the chain has a self-library peek and no exile producer (CR 608.2g). - Parser: parse the "with mana value N or less/greater" dig-peek suffix into a typed cast-permission constraint (fixes Perception Bobblehead's dropped MV<=3 cap; Founding the Third Path stays constraint-free). - Resolver: park an EffectZoneChoice on the Library for eligible candidates, freeze dynamic constraint refs (X, power) to Fixed at binding time (CR 608.2h), cast the chosen spell during resolution, and bottom the rest in a random order on accept, decline, and zero-eligible paths (CR 401.4). - Resolver: gate the pre-injected-target library route on references_exiled_by_source() so Planetarium of Wan Shi Tong's ParentTarget flow keeps its direct free cast. - Visibility: the prompt player sees the looked-at Library cards; opponents get redacted placeholders across serde round-trips (CR 701.20e, CR 400.2). Fixes the Discord bug report (thread 1529132485668245575). Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
… paths-filtered tips (phase-rs#6303) Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
…ient routing (phase-rs#6305) * feat(desktop): pinned native-engine WebSocket bridge commands and JS shim * test(desktop): cover bridge shim ordering contract and bridge failure paths * feat(client): route native-AI games through the shell bridge with WASM fallback * fix(client): use camelCase AI seat wire fields and surface native engine death * fix(client): surface the connection-lost banner when the native engine dies mid-game * test(client): index mock calls without Array.at to satisfy type-check --------- Co-authored-by: matthewevans <matthewevans@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Trade Caravan's activated ability
Remove two currency counters from ~: Untap target basic land. Activate only during an opponent's upkeep.dropped its timing restriction toEffect::Unimplemented, leaving the ability activatable at any time.parse_activation_during_role_gatehandledduring an opponent's turnandduring your upkeepbut had no arm forduring an opponent's upkeep. This adds that arm by composing existing building blocks rather than proliferating a newDuringOpponents*restriction sibling.Implementation method (required)
Method: /engine-implementer (gates applied inline:
add-engine-variantexistence/parameterization/boundary gate + mandatory finalreview-impl)CR references
CR 503.1— the upkeep step (newParsedCondition::IsDuringUpkeeppredicate + its evaluator arm).CR 102.1— the active player is the player whose turn it is (opponent scope viaNot(IsYourTurn)).CR 602.5b— an activated ability's use restriction persists across control changes.All three grep-verified against
docs/MagicCompRules.txt.Design
Rather than add a
DuringOpponentsUpkeepsibling toActivationRestriction(which already expresses opponent-turn scope viaRequiresCondition { Not(IsYourTurn) }, not a dedicatedDuringOpponentsTurnvariant), the fix composes:ParsedCondition::IsDuringUpkeep— a scope-free upkeep-step predicate, orthogonal to every existingParsedConditionvariant (there was no prior phase predicate).during an opponent's upkeep→RequiresCondition { And([Not(IsYourTurn), IsDuringUpkeep]) }, reusing the same opponent-turn composition idiom as the existing bare opponent-turn gate.add-engine-variantgate result forParsedCondition::IsDuringUpkeep: Stage 1 DOES_NOT_EXIST (no phase predicate inParsedCondition), Stage 2 EXTEND_OK (orthogonal — not a sibling cluster), Stage 3 WITHIN_SECTION (CR 503, upkeep step).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— clean.cargo clippy -p engine --all-targets -- -D warnings— clean, 0 warnings (full compile; confirms every exhaustiveParsedConditionmatch is satisfied).cargo test -p engine --lib—ok. 17440 passed; 0 failed; 6 ignored.cargo test -p engine --test integration—ok. 3676 passed; 0 failed; 2 ignored.New tests:
activated_ability_opponents_upkeep_restriction_composes_scope_and_step(parser building block, drives productionparse(), asserts composed restriction + zeroEffect::Unimplemented) andopponents_upkeep_activation_condition_gates_on_step_and_scope(runtime, drives realevaluate_conditionacross the full turn-scope × step matrix) — both pass.cargo coverage/cargo semantic-audit— deferred to CI: both require a full MTGJSONcard-data.jsonexport (156 MB+ multi-request per-set fetch) that isn't feasible in this environment (only a test fixture is present locally). Direct evidence instead: the parsed AST for Trade Caravan now contains zeroEffect::Unimplementedand the correctRequiresCondition { And([Not(IsYourTurn), IsDuringUpkeep]) }restriction. Sincecard_face_has_unimplemented_parts(game/coverage.rs) derivessupportedprecisely from the absence ofUnimplementedparts, Trade Caravan is nowsupported: true, gap_count: 0.Gate A
Gate A PASS head=c5024c97678be3ffdcbac252e235cd8ed145fdab base=ee061e5c7485619644dc6089976b94a1d29a0202
Anchored on
crates/engine/src/parser/oracle.rs:6792—value(opponents_turn_activation_restriction(), alt((tag("during an opponent's turn"), tag("during an opponents turn")))): the samevalue(computed_restriction, alt((tag, tag)))during-role arm the new opponent's-upkeep arm mirrors (combinator family, naming, placement).crates/engine/src/parser/oracle.rs:6803—value(ActivationRestriction::DuringYourUpkeep, alt((tag("during your upkeep"), tag("during their upkeep")))): the sibling upkeep-step during-role arm, immediately adjacent, establishing the apostrophe/possessive variant pattern reused verbatim.crates/engine/src/game/restrictions.rs:1520ParsedCondition::IsYourTurn => state.active_player == playerandrestrictions.rs:1066CastingRestriction::DuringOpponentsUpkeep => state.active_player != player && state.phase == Phase::Upkeep— the evaluator arms the newIsDuringUpkeep => state.phase == Phase::Upkeeparm mirrors.Final review-impl
Final review-impl PASS head=c5024c97678be3ffdcbac252e235cd8ed145fdab
Claimed parse impact
docs/parser-misparse-backlog.mdroot cause 28)🤖 Generated with Claude Code
Generated by Claude Code