fix(engine): commit W30 MTGJSON token catalog and make coverage snapshot vintage-proof - #6274
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the token coverage test in crates/engine/src/game/coverage.rs to assert invariants and non-vacuity floors instead of absolute token counts, preventing test failures caused by weekly MTGJSON data updates. The feedback suggests extracting the magic numbers used for the minimum token floors into named constants to improve readability and maintainability.
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.
| assert!(summary.total_tokens >= 2845); | ||
| assert!(summary.rules_text_tokens >= 1480); |
There was a problem hiding this comment.
To improve readability and maintainability, consider extracting these magic numbers into named constants (e.g., MIN_TOTAL_TOKENS, MIN_RULES_TEXT_TOKENS). This makes their purpose as non-vacuity floors clearer and simplifies future updates.
You could define them at the top of the tests module:
const MIN_TOTAL_TOKENS: usize = 2845;
const MIN_RULES_TEXT_TOKENS: usize = 1480;And then use them in the assertions:
assert!(summary.total_tokens >= MIN_TOTAL_TOKENS);
assert!(summary.rules_text_tokens >= MIN_RULES_TEXT_TOKENS);
Parse changes introduced by this PR✓ No card-parse changes detected. |
matthewevans
left a comment
There was a problem hiding this comment.
Request changes — the token-catalog refresh needs a guard that can detect losing the refresh.
🔴 Blocker
[MED] The new non-vacuity guard accepts loss of this entire catalog update. Evidence: crates/engine/src/coverage.rs:11314-11317 accepts the prior 2845 token-total / 1480 rules-text floors even though this head's catalog is 2858 / 1490; .github/workflows/ci.yml:251-281 regenerates the card export and checks only oracle-subtypes, never regenerating or comparing known-tokens.toml.
Why it matters: deleting all 13 newly cataloged presets, including the 10 with rules text, would still report full token coverage and pass CI, so the generated catalog has no provenance gate.
Suggested fix: add a deterministic tokens-gen-to-temp-and-compare gate for the catalog input vintage, or retain a discriminating assertion that fails when this refresh's catalog entries are lost.
🟡 Non-blocking
The PR body’s source-ref arithmetic appears off by one (8638 → 9821 is +1183); correct it when updating the guard.
✅ Clean
The changed coverage thresholds are meaningful semantic floors; no named-constant refactor is needed for those two single-test values.
Recommendation: add the generated-catalog provenance guard, then rerun current-head CI.
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
|
🤖 AI text below 🤖 Thanks @matthewevans — the blocker is real and is fixed in 🔴 Blocker — fixed in
|
| date | commit | tokens | rules_text | source_card_refs |
|---|---|---|---|---|
| 2026-05-22 | ed12ff1f1 |
2775 | 1436 | 9500 |
| 2026-06-01 | 3c4b34997 |
2777 | 1437 | 9530 |
| 2026-06-19 | a347f9023 |
2844 | 1479 | 9721 |
| 2026-06-29 | 11d0c9480 |
2844 | 1479 | 9810 |
| 2026-07-19 | 5931f7aa1 |
2845 | 1480 | 8637 ⬅ −1173 |
| 2026-07-21 | f0ac54354 |
2858 | 1490 | 9821 |
tokens and rules_text are monotone across all ten revisions. source_card_refs shrank exactly once: #6199 — "fix(parser): preserve TrackedSet on plural 'those tokens' delayed exile" — a parser PR that silently dropped 1173 token↔card links while the token count simultaneously grew (2844 → 2845). Nothing caught it; this PR's regen is what repaired it.
Two consequences worth flagging: a token-count floor would not have caught #6199 (it grew straight past one), so the ref floor is the only one of the three that detects a partial regen — it is the most load-bearing assert here, not the most speculative. And the sole historical ref shrink was an accidental partial regen rather than benign upstream churn, so the tight floor has no measured false-red precedent.
Non-vacuity evidence
The three floors are sequential assert!s, so a later one is a vacuous discriminator if an earlier conjunct dominates it. Each was tripped in isolation, catalog restored byte-identically after every probe:
| axis | mutation (only this axis moved) | result |
|---|---|---|
total_tokens >= 2858 |
catalog reverted to f0ac54354^ — your exact scenario |
FAIL token catalog shrank: 2845 presets < 2858 |
rules_text_tokens >= 1490 |
1 rules_text line removed |
FAIL rules_text_tokens >= 1490 |
source_card_refs >= 9821 |
5 [[token.source_card_refs]] blocks removed |
FAIL source_card_refs >= 9821 |
Reaching the axis-2/3 assert lines also proves both upstream assert_eq! invariants passed under those mutations, i.e. neither floor is shadowed. Gate on the new head: cargo nextest run -p engine -E 'test(analyze_token_coverage) + test(token_coverage) + test(source_defined_pt) + test(token_rules_text)' → 5 passed, 0 failed; cargo fmt --all clean.
Known limit, stated rather than papered over: a count ratchet is fungible — deleting these 13 presets while upstream adds 13 unrelated ones would pass. Every accidental loss mode (stale or partial sets/ fetch, truncated file, dropped merge hunk, lost regen) is a shrink in at least one axis, which is what this catches. Closing the fungibility gap needs the generation-side compare, below.
🔵 Alternative #1 (tokens-gen-to-temp-and-compare) — not taken here, and the input is subtler than it looks
tokens_gen.rs:38 reads data/mtgjson/sets. That directory is populated in CI, but only by the draft-pools job — which is if: github.event_name == 'push' && github.ref == 'refs/heads/main' (ci.yml:513), so it never runs on a PR, and it populates via scripts/fetch-draft-sets.sh (draftable sets) while tokens-gen needs scripts/fetch-token-sets.sh (token-bearing sets, gen-card-data.sh:90). A regen-compare against that directory would build a different, smaller catalog and fail spuriously, so this needs its own fetch wired into a PR-scoped job — a .github/workflows/** edit, which I am hard-stopped from making. Happy to do it as a follow-up on your say-so.
Worth noting the gate is not entirely absent, just not in CI: gen-card-data.sh:119-136 already refuses to promote the tracked catalogs when MTGJSON's .meta.date predates crates/engine/data/mtgjson-vintage (2026-07-20, matching this regen). Your "no provenance gate" holds for CI; the generation path has a monotonic one.
🟡 Non-blocking — the arithmetic is correct as written; the 8638 premise is off by one
Respectfully disagreeing here, measured both ways:
$ git show f0ac54354^:crates/engine/data/known-tokens.toml | grep -c '^\[\[token.source_card_refs\]\]'
8637
$ grep -c '^\[\[token.source_card_refs\]\]' crates/engine/data/known-tokens.toml
98219821 − 8637 = 1184. The base is 8637, not 8638, and the commit message already states 1184 [[token.source_card_refs]] (8637 -> 9821). No change made — let me know if you measure differently.
✅ Named constants (@gemini-code-assist)
Agreeing with @matthewevans's ✅ — no refactor for two single-test values, and constants would not have addressed the blocker anyway, since the defect was the floors' value, not their spelling. The three floors now carry self-describing panic messages instead.
Current head fac5d5f0e; the only change on top of f0ac54354 is the one test in crates/engine/src/game/coverage.rs. Leaving the threads open for you to close.
|
Your analysis of the regression surface is spot on. Moving from strict Regarding the constants: I agree with your assessment. Given that these values are pinned to specific vintage commits and the panic messages are already self-describing, introducing constants would likely add indirection without improving clarity or maintainability. The current implementation is clear and appropriately scoped to the test logic. Your breakdown of the |
…hot 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
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
fac5d5f to
0ebe9f6
Compare
matthewevans
left a comment
There was a problem hiding this comment.
Request changes — current head 0ebe9f6823350195148bc27fe43b8a51a482124d still lacks a deterministic catalog-provenance gate.
🔴 Blocker
[MED] coverage.rs:11314-11335 now ratchets aggregate floors, but it still only observes three totals. The PR's own follow-up confirms that CI downloads AtomicCards.json and runs oracle-gen (.github/workflows/ci.yml:251-281), while tokens-gen consumes data/mtgjson/sets and no PR CI step populates that input or compares a generated known-tokens.toml with the tracked catalog.
Why it matters: a catalog that loses or swaps entries while preserving the three aggregate totals can still satisfy the invariants and floors. The tightened count floors catch truncation, but they do not establish that the committed catalog is the deterministic output of its declared MTGJSON vintage.
Suggested fix: add a PR-scoped, deterministic token-input fetch followed by tokens-gen to a temporary output and cmp against crates/engine/data/known-tokens.toml (or an equivalently discriminating provenance assertion). The author correctly identifies this as a workflow-level change, so it cannot remain an acknowledged limitation on a PR whose purpose is making weekly generated-catalog refreshes safe.
🟡 Source-reference arithmetic
Against the current base, the catalog changes from 8,638 to 9,821 source-card refs, a +1,183 delta. Please correct the PR summary/body's +1184 / 8637 wording when addressing the blocker; that older parent-count measurement does not describe this current PR diff.
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
|
🤖 AI text below 🤖 Round-2 responses (review 4746023659): N1 (provenance) — the gate you asked for was run, and it passes at this head. Your exact check ( Why it cannot be an in-test assertion (structural, not preference): the generator input is gitignored and absent at test time, and the test sees the catalog only through One factual correction to "still only observes three totals": N2 (+1,183 vs +1,184) — the premise has no source commit. Against the PR's actual |
matthewevans
left a comment
There was a problem hiding this comment.
Approving — resolves my prior CHANGES_REQUESTED.
Reconsidered the provenance blocker against the current head (2f30f85) and I'm satisfied this is the proportionate resolution:
- The three ratchet floors — including
source_card_refs >= 9821— now catch the realistic failure mode (partial-regen / truncation, the actual #6199 class where token count grew while refs silently dropped). The purely adversarial 'swap entries keeping all three aggregate totals byte-identical' is not a realistic outcome for a deterministic generator reading upstream data. - Your point that no unit test can establish provenance is correct — the
tokens-geninput (data/mtgjson/sets) is gitignored and absent from the test binary. A true provenance gate belongs in CI, and the documentedfetch-token-sets → tokens-gen → cmpseam is the right recipe for it. - A per-PR CI
cmpgate is worthwhile defense-in-depth, but it must be vintage-aware (only hard-fail the cmp when the fetched MTGJSON.meta.dateequals the committedmtgjson-vintagestamp; when newer, that's expected staleness, not tampering) or it re-introduces the exact weekly red this PR removes. That's follow-up-sized, not a blocker on a data+test PR.
Tracking the CI provenance gate separately; #6278 (weekly auto-refresh) is the natural home for it and also makes the automated regen the catalog's sole update path, giving provenance-by-construction going forward.
Non-blocking nit for a future edit: the body's source-ref arithmetic reads +1184/8637; the actual delta is 8638 → 9821 (+1183). Cosmetic, not gating.
|
🤖 AI text below 🤖 Correcting my own previous comment before you spend review time on it — an independent check of that reply found one claim overstated. Three additions, all measured. 1. Correction: the generation seam does not verify provenance. I wrote that your check "is what # tokens-gen output is deterministic, so only overwrite when content actually
# changed — an unconditional copy bumps the file's mtime and forces a full
# (40-65s) engine recompile via build.rs's rerun-if-changed for nothing.
if cmp -s "$TOKENS_TMP" "$TOKENS_FILE"; thenThat 2. The canonical command (mine carried a needless flag). $ cargo run --bin tokens-gen -- --input data/mtgjson/sets --output /tmp/kt-nofeat.toml
Generated 2858 token presets at /tmp/kt-nofeat.toml
$ sha256sum /tmp/kt-nofeat.toml
9dc2222646ea230f46a09da0359f5aadf05c9e5349e1022fa54ea3148fe3c9b3Determinism now has three independent confirmations, all 3. Conceding the limit of my "∀-quantified" point. You're right that it doesn't rescue the guard. Both What I considered and did not add, so you can see it was priced rather than skipped:
Say the word on any of these and I'll add it. Otherwise my position is unchanged: the discriminating gate is the CI one you described, it needs a One caveat for whoever does wire it: output order is pinned by |
…#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>
…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
…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>
🤖 AI text below 🤖
Summary
Commits the 2026-W30 weekly MTGJSON token catalog refresh (deterministic regen: +13 tokens, +1184
[[token.source_card_refs]], "Undercity // The Initiative" → "Undercity" face-name change, 0 removed) and converts the token coverage snapshot test from vintage-pinned absolute counts to invariants (supported == total,parsed_rules_text == rules_text) with non-vacuity floors, so the weekly refresh introduced by #6237 no longer turnstest-enginered every time upstream adds tokens at 100% coverage.Implementation method (required)
Method: not-applicable — deterministic data-catalog regeneration plus a test-assertion change in an existing coverage snapshot test; no parser, effect, resolver, targeting, or rules behavior touched.
CR references
None — coverage accounting and generated card data only; no game-rule logic changed.
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 build --profile tool --features cli --bin tokens-gen && target/tool/tokens-gen --input data/mtgjson/sets --output crates/engine/data/known-tokens.toml—Generated 2858 token presets;cmpagainst the main checkout's independently promoted catalog: byte-identical (determinism re-proven across two checkouts).cargo nextest run -p engine -E 'test(analyze_token_coverage) + test(token_coverage) + test(source_defined_pt) + test(token_rules_text)'— 5 passed, 0 failed (final run on the committed tree).Measured summary on this head:
total=2858 supported=2858 rules_text=1490 parsed_rules_text=1490(temporary eprintln, removed before commit).Discriminating-test probe: temporarily forced
token_rules_text_unparsed_gapto report a gap for everySourceDefinedOrDynamicpreset → test FAILED (assertion left == right failed — left: 2743, right: 2858, parsed_rules_text 1490→1443); probe fully reverted → test PASSED;git diffclean of probe residue. (First probe attempt attoken_pt_provenance_represents_linewas measured vacuous — no preset currently hasunparsed_rules_text_lines— hence the probe point one level up.)Non-vacuity floors: an empty/truncated catalog passes both equality invariants trivially, but fails
total_tokens >= 2845/rules_text_tokens >= 1480.grep -rn --include="*.rs" --include="*.ts" --include="*.tsx" -E "\b(2845|1480|2858)\b" crates/ client/src— coverage.rs is the only pin site (remaining hits are unrelated issue Archangel Elspeth: -6 returns only one permanent (not all qualifying) and removes 2 loyalty instead of 6 #2858 references).grep -rn --include="*.rs" --include="*.ts" --include="*.tsx" "Undercity // The Initiative" crates/ client/src— no source references the old face name.bash scripts/check-parser-combinators.sh— Gate G PASS, Gate A PASS on committed head.cargo fmt --all— clean.Gate A
Gate A PASS head=f0ac54354e82e0bd97a5da2a3e41bc6a6655974a base=836ff312ae2073c99af28d286b0c4915faa8a458
Anchored on
analyze_token_coverage()overknown_token_presets(), the summary authority the snapshot test measurestoken_pt_provenance_has_no_materialization_gap, the source-defined-PT representation path the kepttop_gapsassert and the probe exerciseFinal review-impl
Final review-impl PASS head=f0ac54354e82e0bd97a5da2a3e41bc6a6655974a
Claimed parse impact
🤖 Generated with Claude Code
https://claude.ai/code/session_01KY26RoWwb6nYW2up9eFgC9