Skip to content

refactor(parser): Plan 05b Class-B bring-up, tranches T0-T3 - #6699

Merged
matthewevans merged 8 commits into
mainfrom
ship/plan05b-class-b-t0-t3
Jul 27, 2026
Merged

refactor(parser): Plan 05b Class-B bring-up, tranches T0-T3#6699
matthewevans merged 8 commits into
mainfrom
ship/plan05b-class-b-t0-t3

Conversation

@matthewevans

@matthewevans matthewevans commented Jul 27, 2026

Copy link
Copy Markdown
Member

Plan 05b Class-B bring-up, tranches T0–T3 (.planning/architecture-remediation/05b-class-b-bringup.md,
row table in 05b-U0-recensus-2026-07-26.md). Six commits, four tranches, seven converted producer sites.

commit tranche what
2b230ba0fa T0 retire the stale dead_code allow on OracleNodeIr::Static
8c224078ed land the PreLowered ratchet early (§6.1), seeded as a ceiling
2e1973d48a T1 baseline the Class level-section two-layer corpus
22a58386c7 T1 4 class-section-prelowered rows → StaticIr / ReplacementIr
163a47bee5 T2 leveler + Spacecraft preprocessors own their source text
ddf26b32a9 T3 dual as-enters (CR 614.1c + CR 708.11) + Saga ETB (CR 714.3a) → IR

Gate half one — empirical

Byte-identity: PASS, by attribution rather than by cmp.

The pinned baseline was voided mid-tranche by exactly the hazard the charter names
("MTGJSON refresh rolls Monday — a mid-tranche refresh voids the baseline"): 3cf2930a39 bumped
crates/engine/data/mtgjson-vintage. A raw cmp therefore shows red for reasons that are not this PR.

Full-pool regeneration (card-data build 2427) diffed per card key against the pinned baseline:

  • 35,516 entries both sides; 0 added, 0 removed.

  • 13 entries changed — all 13 are Licids, attributable to 6b29e0d720
    "fix(engine): Licids attach themselves, not their enchant target (fix(engine): Licids attach themselves, not their enchant target (#605) #6668)", which landed between the
    baseline pin and the regeneration.

  • 0 changed entries in any class these conversions touch, and the negative is non-vacuous:

    class in pool changed
    leveler (LEVEL block) 27 0
    Spacecraft (N+ | body) 68 0
    Saga (chapters) 243 0
    as-enters / turn-face-up counters 35 0

Snapshots. student_of_warfare_ir is the only genuinely pre-conversion baseline among the churned
fixtures; its churn was verified to be pure re-nesting by normalizing Static{definition,..} back to
PreLoweredStatic{..} and comparing == — not by reading the re-indented diff. active_zones stayed
[] and no CountersOn.scope rebound, which is the charter's STOP condition for the double-lowering.
student_of_warfare_lowered.snap did not change at all.

Two fixtures were added because the corpus could not witness T2's rows at all
(lumen_class_frigate reaches both Spacecraft static arms; kabira_vindicator reaches the leveler
body arm twice). Six Static nodes, zero PreLoweredStatic, every source_text the real recognizer
input rather than lines[line].

test-engine green at build 2420 — observed red first at 2413 on exactly these two fixtures, then
green after accepting, per the seen-red rule. cargo fmt --all clean. Ratchet gate PASS.

Gate half two — CR-faithfulness for the class

  • T1/T2 statics. lower_static_ir applies two transforms and both are idempotent and blind to the
    interposed condition: collect_source_in_zones reads only SourceInZone and
    populate_active_zones_from_condition self-guards on active_zones.is_empty();
    bind_anaphoric_counters rewrites only ObjectScope::Anaphoric and derives its bound value from
    def.affected, which neither the ClassLevelGE wrap nor CR 711.2a / CR 702.184a HasCounters
    touches. So the double-lowering is a no-op for the class, not merely for today's corpus.
  • T1/T3 replacements. lower_replacement_ir is ir.definition.clone() — identity by construction.
  • T3 provenance convention. A synthesized rule item's source text is its own description. The Saga
    ETB replacement (CR 714.3a) prints no line — the rule applies from the subtype alone — and the anchor
    tempts a lie, since etb_line points at chapter I's line, which the chapter-I trigger owns.

Standing obligation created: lower_static_ir's doc comment now records that any future transform
added there must be idempotent, or all 29 sites silently double-apply it.

Harvest (§5.4)

  1. Dead branch deleted. Priority 13e ("X can't be 0.") was structurally unreachable:
    strip_x_cant_be_zero_suffix returns "" for exactly that input and lower is bound once from the
    post-strip line, so the empty-line guard always claims it first. Its own comment called it a
    "defensive fallback". Removing it also drops mutate_last_spell from two callers to one, halving the
    surface of item 2.
  2. Verified detonation path for T8. doc.rs:804 pushes both Spell(_) and PreLoweredSpell(_)
    onto spells_emitted, and take_last_spell pops that stack with no variant filter — while
    oracle.rs:3683 and :5827 destructure with unreachable!("… returns only PreLoweredSpell items").
    The first native Spell producer makes both reachable: a panic during card parsing, not a silent
    skip. This is a hard prerequisite for T8, confirming §7.4.
  3. Description is not a provenance carrier. Lumen-Class Frigate's keyword-only Spacecraft static has
    description: "12+ | Flying, lifelink" but source_text: "Flying, lifelink". Left as-is (the
    description feeds card data), and it is incidental evidence for why the preprocessor had to own
    source_text explicitly.
  4. Two rows have no pool witness, recorded rather than papered over: the leveler multi arm
    (oracle_level.rs:146, parse_static_line_multi — no printed LEVEL body lowers to more than one
    static) and T1's row U0-60 (ability-word static — all three ability-word-prefixed Class level bodies
    are Landfall — Whenever … and take the trigger arm). Their conversions rest on the class argument.
  5. Ratchet is a ceiling, not a progress meter. T2 converted two producers but the ledger moves only
    21 → 20, because the Spacecraft site emitted through the static_at helper and never had a textual
    PreLowered hit of its own. Helper-mediated producers stay invisible until the helper dies.
  6. Process. insta writes .snap.new into the Tilt-watched source tree, so a failing snapshot test
    self-perpetuates a rebuild loop that starves every other resource on the cargo lock — card-data sat
    pending for ~7h behind it. Same failure class gen-card-data.sh's .tmp. infix already guards
    against for known-tokens.toml.

Scoped out, deliberately

U0-05 / U0-06 (the drain_result_vectors static and replacement arms). Converting them needs
ParsedAbilities.statics / .replacements to carry IR — 93 call sites on a pub type — and the
modal-block feeder passes only the block HEADER line, so every drained per-mode static would have to
source its own text from inside lower_oracle_block. Until that lands, static_at and replacement_at
each retain exactly one caller, so T3's headline payoff of deleting both emitter methods is still pending.

Summary by CodeRabbit

  • Improvements
    • Improved parsing and lowering of Saga chapters, LEVEL blocks, Spacecraft threshold lines, and Class static/replacement text.
    • Refined counter/ETB-related replacement handling for more consistent IR generation.
  • Quality
    • Expanded snapshot coverage for Level sections, Class cards, and threshold-based behavior to improve regression detection.
  • Developer Tools
    • Added an extra CI lint gate and a pre-commit validation to enforce “pre-lowered” migration progress using a burn-down ledger.

The PLAN-05 DEBT note said this variant was constructed only by the
Class-B bring-up and that the allow should be retired there. That
landed in #6325: static_ir_at (oracle.rs) constructs
OracleNodeIr::Static from 26 call sites, so the allow is stale and
was masking nothing.

OracleNodeIr::Trigger keeps its allow deliberately -- it still has no
live producer until the trigger seam converts (Plan 05b T4/T5).

Plan 05b T0.
"Is Plan 05b done?" was answerable only by archaeology. That is how the
#[allow(dead_code)] on OracleNodeIr::Static survived nine days past the
commit that discharged its debt.

Adds a per-file ceiling ledger over textual PreLowered occurrences in
crates/engine/src/parser (generated snapshots excluded). Reducing a count
passes; raising one fails; a parser file carrying PreLowered with no
ledger entry fails, so a new producer cannot be added in a third file and
escape the burn-down. Slack prints the tightening edit rather than failing,
per plan 6.1, so each tranche lowers its own number and the burn-down shows
up in git log on one file.

The metric is deliberately a grep, not a type-level check: the point is to
catch a producer being written, not a variant being reachable. A type check
would go green only when the last variant is deleted -- the end state, not
the tracking mechanism.

Both failure branches were verified red before wiring: over-ceiling, and
present-but-unlisted.

Plan 05b 6.1.
No Class card was in the two-layer IR corpus, which made Plan 05b T1's
byte-identity gate vacuous: oracle_class.rs's level-section arms could be
converted from PreLowered* to IR nodes with zero snapshot churn and zero
proof the converted path was reached.

Five pool-verified cards, chosen so each arm T1 converts has a witness and
so both the wrapped and unwrapped halves of the ClassLevelGE gate are
covered:

  granted quoted static (193)  wrapped: Sorcerer Class L2
  plain static (204)           unwrapped: Wizard Class L1
                               wrapped: Barbarian L3, Innkeeper L2, Bard L2
  replacement (221)            unwrapped: Bard L1
                               wrapped: Innkeeper's Talent L3

Arm attribution was read off the generated baseline (node variant + source
fragment per item), not predicted -- predicting it got three of the seven
wrong. is_granted_static_line needs a prefix AND a quoted verb (has " /
have " / gains " / gain "), so an unquoted grant like "Creatures you
control have haste" falls through to is_static_pattern.

Two gaps recorded in the test file rather than papered over:

  * Row 260 (ability-word-prefixed static) has NO witness in the pool --
    all three such Class level bodies are "Landfall -- Whenever ..." and
    take the trigger arm. Its conversion rests on the class argument alone.
  * Barbarian Class L1 does not reach the replacement arm; it lands as
    PreLoweredSpell/Unimplemented. Pre-existing parser gap, baselined so
    T1 churn against it would be visible.

This commit must precede the conversion or it proves nothing.

Plan 05b T1 (baseline).
Convert the four `class-section-prelowered` producers in
`parse_class_oracle_text` from `OracleNodeIr::PreLowered{Static,Replacement}`
to the real IR nodes `Static(StaticIr)` / `Replacement(ReplacementIr)`, so the
Class level sections lower through the same seam as every other static.

Each site passes the text the recognizer above it actually consumed as
`source_text` — `&static_line` at the two static arms, `line` at the
replacement arm, `&effect_static` at the ability-word arm — rather than
`lines[line]`, which would fabricate provenance for the post-strip body.

Because `parse_static_line` already lowers internally, routing its result
through `StaticIr` makes document lowering run `lower_static_ir` a second
time, with `wrap_static_with_class_level` interposed between the passes. That
is a no-op for the whole class, not just today's faces:
`populate_active_zones_from_condition` self-guards on `active_zones.is_empty()`
and its collector reads only `SourceInZone` (so `ClassLevelGE` contributes
nothing), and `bind_counter_anaphor_to_recipient` rewrites only
`ObjectScope::Anaphoric`, of which none survive the first pass. Record that
invariant as a standing obligation on `lower_static_ir`'s doc comment.
`lower_replacement_ir` is pure identity, so the replacement arm is safe by
construction. CR 607.2d relation discovery and CR 707.9a slot indexing are
untouched: `item_static` and `finish()` already group the converted variants
with their `PreLowered` counterparts in the same match arms.

Snapshot churn is confined to the five `_ir` fixtures and is a pure
re-nesting — verified structurally, not by eye: unwrapping `Static.definition`
reproduces the previous `PreLoweredStatic` payload byte-for-byte across all
seven converted sites, with `active_zones` still empty and no rebound counter
scope. The `_lowered` fixtures are unchanged, which is the byte-identity claim.

Tighten the burn-down ledger 10 -> 6 to match.
… own provenance

Convert the two `preprocessor` static producers — `parse_level_blocks`
(CR 711 LEVEL blocks) and `parse_spacecraft_threshold_lines`
(CR 702.184a / CR 721.2 `N+ | body` lines) — to yield `StaticIr` rather than
bare `StaticDefinition`, so each preprocessor is the single authority for the
source text of what it produced.

That authority is the point of the change. The real recognizer input is the
post-`|` threshold body or the self-ref-normalized LEVEL sub-line, never the
printed line the span anchors to; only the preprocessor knows which. Passing
`lines[line]` at the call site would compile and would fabricate provenance.
The leveler's block summary is synthesized from the block's modification
lines with no printed line behind it at all, so it carries those joined lines
— the same string as its description — rather than claiming the `LEVEL N-M`
header its span happens to anchor to.

The leveler keeps `emit_span` instead of routing through `static_ir_at`: its
`first..=last` range is load-bearing, and `static_ir_at` also writes the
`last_static` peek mirror, which the leveler deliberately does not update.
Spacecraft moves `static_at` -> `static_ir_at`, which does update that mirror
— as `static_at` already did.

Byte-identity rests on T1's argument: both definitions arrive already lowered
from `parse_static_line`, so document lowering runs `lower_static_ir` a second
time, and both of its transforms are idempotent. CR 711.2a level ranges and
CR 702.184a station thresholds are encoded as `StaticCondition::HasCounters`,
which `collect_source_in_zones` does not read, so the second pass cannot
rewrite `active_zones`.

Two fixtures, because the existing corpus could not witness either row:

  * `lumen_class_frigate` — the only two-layer card reaching a Spacecraft
    threshold line at all, and it reaches both static arms (`parse_static_line`
    at :256 and keyword-only at :178). Chalice of the Void matched a grep for
    `charge` but prints no threshold line, so without this fixture the
    Spacecraft conversion would have been snapshot-invisible.
  * `kabira_vindicator` — `student_of_warfare` reaches only the block-SUMMARY
    arm (:194); Kabira prints a sentence in each LEVEL block, witnessing the
    body arm (:154) twice while still carrying two summaries.

The Spacecraft fixture also pins a pre-existing wart rather than fixing it: the
keyword-only arm's description keeps the `12+ | ` prefix while its source text
does not. Left alone — the description feeds card data, and this is incidental
evidence for the change's own premise, that the description was never a
trustworthy provenance carrier.

Recorded gap: the multi arm (`oracle_level.rs:146`, `parse_static_line_multi`)
has no pool witness — no printed LEVEL body line lowers to more than one
static. Its conversion rests on the class argument alone.

Note the ledger moves only 21 -> 20: the Spacecraft site emitted through the
`static_at` helper, so it never had a textual `PreLowered` hit of its own. The
ratchet is a ceiling, not a progress meter — helper-mediated producers stay
invisible until the helper itself dies.
…s IR

Convert the two replacement producers that Plan 05b's T3 can reach today:

  * `lower_as_enters_or_face_up_counters` (CR 614.1c + CR 708.11, the dual
    "As ~ enters or is turned face up, put X counters on it" sentence) now
    returns `Option<Vec<ReplacementIr>>` instead of pushing into a shared
    `ParsedAbilities` scratch and returning `bool`. Both arms derive from the
    same printed sentence — the sentence never splits, which is the whole
    reason this recognizer exists — so both carry it as their source text.
  * `parse_saga_chapters`' ETB lore-counter replacement (CR 714.3a) now yields
    a `ReplacementIr`.

Dropping the mutator's `drain_result_vectors` call is safe, and provably so
rather than by inspection: the loop hands `&mut result` to exactly three
mutators, each immediately drain-followed, and the only other `&mut result`
uses in the loop touch the `additional_cost` singleton, which the drain does
not clear. The scratch's vectors are therefore empty on entry, and replacements
were the only vector this recognizer ever wrote.

CR 714.3a forces a provenance decision, so state it as a convention: a
SYNTHESIZED rule item's source text is its own description. The Saga ETB
replacement prints no line — the rule applies from the subtype alone. The
anchor tempts a lie here, since `etb_line` points at the first chapter's line
for ordering, but the chapter-I trigger owns that text. A synthetic label
cannot be located in the Oracle text, which is the honest answer for an item
that was never printed.

Both conversions are byte-identical by construction: `lower_replacement_ir` is
`ir.definition.clone()`, and `emit_at` derives the span and fragment from
`self.lines[line]`, not from `ir.source_text`. Emission line and ordering are
unchanged.

Scoped out of this commit, and out of T3 as chartered: U0-05 / U0-06, the
`drain_result_vectors` static and replacement loops. Converting them requires
`ParsedAbilities.statics` / `.replacements` to carry IR, which is 93 call sites
across the parser on a `pub` type, and — the reason it is design work rather
than mechanical — the modal-block feeder passes only the block HEADER line, so
every drained per-mode static would have to source its own text from inside
`lower_oracle_block`. Until that lands, `static_at` and `replacement_at` each
retain exactly one caller (those two loops), so T3's headline payoff of
deleting both emitter methods is still pending.
@matthewevans
matthewevans enabled auto-merge July 27, 2026 18:15
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6815bd5a-47ab-4866-a3b6-3f15801cacde

📥 Commits

Reviewing files that changed from the base of the PR and between 6f7379f and 010ab75.

📒 Files selected for processing (3)
  • .claude/skills/oracle-parser/SKILL.md
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_replacement.rs
💤 Files with no reviewable changes (1)
  • .claude/skills/oracle-parser/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/engine/src/parser/oracle_replacement.rs
  • crates/engine/src/parser/oracle.rs

📝 Walkthrough

Walkthrough

Oracle parser paths now construct StaticIr and ReplacementIr values, snapshot coverage is added for affected card patterns, and a PreLowered producer ratchet runs in both pre-commit and CI.

Changes

Oracle IR migration and enforcement

Layer / File(s) Summary
PreLowered ratchet gates
.githooks/pre-commit, .github/workflows/ci.yml, scripts/check-prelowered-ratchet.sh, scripts/prelowered-ratchet.txt
A ledger-backed check counts PreLowered producers, enforces per-path ceilings, and runs from local and CI lint workflows.
Static IR parsing and emission
crates/engine/src/parser/oracle_level.rs, crates/engine/src/parser/oracle_spacecraft.rs, crates/engine/src/parser/oracle_class.rs, crates/engine/src/parser/oracle.rs, crates/engine/src/parser/oracle_static/mod.rs, crates/engine/src/parser/oracle_ir/doc.rs
LEVEL, Spacecraft, Class, and related parser paths now construct or emit StaticIr; lowering stability documentation and enum attribute formatting are updated.
Replacement IR parsing and emission
crates/engine/src/parser/oracle_replacement.rs, crates/engine/src/parser/oracle_saga.rs, crates/engine/src/parser/oracle_class.rs, crates/engine/src/parser/oracle.rs, .claude/skills/oracle-parser/SKILL.md
Saga, Class, and counter replacement paths now construct, return, and emit ReplacementIr, including synthesized ETB provenance. The unreachable priority 13e guard and its documentation are removed.
Two-layer IR snapshot coverage
crates/engine/src/parser/oracle_ir/snapshot_tests.rs
Snapshots are added for Leveler, Spacecraft threshold, and Class level-section parsing at both IR and lowered layers.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Suggested labels: quality

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the parser refactor and Plan 05b Class-B tranche work.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ship/plan05b-class-b-t0-t3

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
crates/engine/src/parser/oracle_static/mod.rs (1)

221-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Idempotency-requirement doc understates which producers actually double-lower.

The new comment on lower_static_ir explains that a recognizer calling parse_static_line (which already lowers) and then wrapping the result in StaticIr::from_definition causes a second lower_static_ir pass, and names "the Class level-section arms in oracle_class.rs" as "the current example." But this same PR introduces the identical pattern in two other files that the comment doesn't mention, so a future contributor auditing "which producers double-lower" before adding a non-idempotent transform, using this comment as the checklist, would miss two real sites.

  • crates/engine/src/parser/oracle_static/mod.rs#L221-L234: broaden the comment to name oracle_level.rs (body-arm statics) and oracle_spacecraft.rs (static-body arm) as additional double-lowering sites, not just the Class arms.
  • crates/engine/src/parser/oracle_level.rs#L153-L161: body statics from parse_static_line/parse_static_line_multi (already lowered) are re-wrapped via StaticIr::from_definition and will be lowered a second time at the document level.
  • crates/engine/src/parser/oracle_spacecraft.rs#L258-L268: same pattern for the static-body arm (multi and single).

This isn't a live bug today — both existing transforms are self-guarded/idempotent per the same comment — but the whole point of this doc addition is to give future maintainers an accurate map before they add a transform that isn't.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/engine/src/parser/oracle_static/mod.rs` around lines 221 - 234,
Broaden the idempotency documentation in lower_static_ir to identify
oracle_level.rs body-arm statics and oracle_spacecraft.rs static-body arms
alongside the existing oracle_class.rs Class level-section arms as
double-lowering producers. At oracle_level.rs lines 153-161 and
oracle_spacecraft.rs lines 258-268, no direct code change is required; these
sites provide the parse_static_line/parse_static_line_multi to
StaticIr::from_definition pattern that the updated documentation must accurately
cover.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/engine/src/parser/oracle_static/mod.rs`:
- Around line 221-234: Broaden the idempotency documentation in lower_static_ir
to identify oracle_level.rs body-arm statics and oracle_spacecraft.rs
static-body arms alongside the existing oracle_class.rs Class level-section arms
as double-lowering producers. At oracle_level.rs lines 153-161 and
oracle_spacecraft.rs lines 258-268, no direct code change is required; these
sites provide the parse_static_line/parse_static_line_multi to
StaticIr::from_definition pattern that the updated documentation must accurately
cover.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bd35e039-aa63-40f7-b96f-03edd92264f3

📥 Commits

Reviewing files that changed from the base of the PR and between 3cf2930 and 6f7379f.

⛔ Files ignored due to path filters (15)
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__barbarian_class_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__barbarian_class_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__bard_class_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__bard_class_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__innkeepers_talent_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__innkeepers_talent_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__kabira_vindicator_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__kabira_vindicator_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__lumen_class_frigate_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__lumen_class_frigate_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__sorcerer_class_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__sorcerer_class_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__student_of_warfare_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__wizard_class_two_layer_ir.snap is excluded by !**/*.snap, !**/snapshots/**
  • crates/engine/src/parser/oracle_ir/snapshots/engine__parser__oracle_ir__snapshot_tests__wizard_class_two_layer_lowered.snap is excluded by !**/*.snap, !**/snapshots/**
📒 Files selected for processing (13)
  • .githooks/pre-commit
  • .github/workflows/ci.yml
  • crates/engine/src/parser/oracle.rs
  • crates/engine/src/parser/oracle_class.rs
  • crates/engine/src/parser/oracle_ir/doc.rs
  • crates/engine/src/parser/oracle_ir/snapshot_tests.rs
  • crates/engine/src/parser/oracle_level.rs
  • crates/engine/src/parser/oracle_replacement.rs
  • crates/engine/src/parser/oracle_saga.rs
  • crates/engine/src/parser/oracle_spacecraft.rs
  • crates/engine/src/parser/oracle_static/mod.rs
  • scripts/check-prelowered-ratchet.sh
  • scripts/prelowered-ratchet.txt
💤 Files with no reviewable changes (1)
  • crates/engine/src/parser/oracle_ir/doc.rs

@github-actions

Copy link
Copy Markdown

Parse changes introduced by this PR

✓ No card-parse changes detected.

`clippy::question_mark`, surfaced by the T3 signature change rather than by
new code: `lower_as_enters_or_face_up_counters` returned `bool` until this
tranche, so its `let ... else { return false }` guards were invisible to the
lint. Returning `Option` makes the `checked_sub` guard a textbook `?`.

The sibling guard above it stays a `let ... else` — it destructures a `Result`
from `tag(", ")`, where `?` would try to convert the nom error into the
function's `Option` and does not apply.
`check-skill-doc.sh` harvests §3 priority-table labels with
`grep -oE '// Priority [^:]+:'`. The comment this tranche left where slot 13e
used to live kept that exact shape, so the gate read the gravestone as a
declaration and demanded a §3 row for a slot the same commit had just deleted —
it asked the table to document something that no longer exists.

The count invariant did not catch it: deleting the slot and leaving a
colon-terminated comment held the total at 41, so only the per-label check
fired. That is the gate working correctly; two of its three invariants are
count-based and this is the one shaped to catch renames.

Reword the gravestone out of the harvested shape (41 -> 40 slots) and drop the
now-dead `13e` row (41 -> 40 rows). A retired slot is documented by its absence
from the table, not by a row saying it is gone — a row would re-assert the slot
to every reader who greps the table for live dispatch order.

The comment says so inline, because the first fix attempt reintroduced the
failure: prose citing the harvested shape verbatim is itself harvested. The gate
is textual and cannot tell a declaration from a quotation of one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant