| id | 2606171258 | |
|---|---|---|
| title | Parity Layer-0 parse-skip: migrate the AST-forcing parity rules | |
| status | ✅ | |
| summary | Move every parity-active rule that still forces the goldmark parse onto the Layer-0 / Layer-1 projections so `mdsmith check -c parity` can skip the parse on benchmark 2. The parse-skip gate is all-or-nothing, so no benchmark gain lands until the whole set migrates; this plan tracks the rule-by-rule effort, starting with heading-style (MDS002). | |
| model | opus | |
| depends-on |
|
Let mdsmith check -c parity shed the goldmark parse on benchmark 2 (the
234-file neutral corpus), where the parse is ~40% of the check. The
engine already skips the parse when every enabled rule resolves to
Layer 0; this plan migrates the parity rules that still force it.
Measured head-to-head against gomarklint 3.2.3 on the real corpus,
parity runs at ~1.8x gomarklint. The parse is the largest single
bucket. The lazy-parse infrastructure is already in place. It has the
Layer 0 block scan (layer0.go), the
rule.BlockChecker seam, the Layer 1 inline index, and the rulelayer
audit gate. MDS013 (blank-line-around-headings) and MDS044
(horizontal-rule-style) are the proven template.
The gate is all-or-nothing: the parse is skipped only when every enabled rule is Layer 0, so the benchmark does not move until the whole parity set migrates. Each rule is still a correct, independently-gated increment.
A scope sweep found 28 parity-enabled rules still forcing the parse. That is beyond MDS002 ✅, MDS013 ✅, MDS015 ✅, and MDS044 ✅, which are already migrated. The 28 are decomposed into five batch plans, grouped by the data each rule reads. Layer is the data a rule needs, not its trigger kind:
- Gate unification (2606171400): MDS001 line-length, plus the mechanism that lets a config-dependent line rule count as Layer 0.
- Layer-0 heading + front matter (2606171401): MDS003, MDS004, MDS051, MDS069.
- Layer-0 fenced code (2606171402): MDS010, MDS011, MDS031, MDS065, MDS066.
- Layer-0 list + blockquote (2606171403): MDS014, MDS016, MDS045, MDS046, MDS061, MDS059, MDS067.
- Layer-1 inline (2606171404): MDS005, MDS017, MDS041, MDS042, MDS049, MDS050, MDS052, MDS053, MDS063, MDS068, MDS034.
A heading or fence rule that reads only its own line is Layer 0. A rule that reads flattened inline text — heading text, link text, an emphasis run — is Layer 1 and drives the shared inline re-parse.
Known blocker (found while scoping the heading batch). scanLayer0
emits heading spans only for top-level headings; it does not descend into
list-item or blockquote bodies. So any nil-AST path that walks heading
spans diverges from the AST for a heading nested in a container. The
heading and single-h1 rules cannot migrate until the scanner emits nested
heading spans (or the gate excludes container-nested headings). See
2606171401.
- Per rule: add the nil-AST path (a
rule.BlockCheckerCheckBlockfor per-block rules, or a span/line walk for standaloneCheckrules), reusing the existing extraction where possible. - Regenerate the walk audit (
MDSMITH_REGEN_WALK_AUDIT=1) and sync the embedded rulelayer copy so the rule flips toA-no-skipping. - Confirm
TestLayer0Gate_CorpusDiagnosticsEquivalencestays green — it enables every Layer-0 rule and diffs parse-skip vs full-parse across the corpus, so a divergent rule fails it. Once the list/quote rules land, drop the gate's code-block guardDone early — see the measurement below.
A full corpus sweep (1046 files, 453 code-bearing) compared every
Layer-0 rule's AST output against its nil-AST output, with front matter
stripped on both sides as the engine does. Result: one divergence,
not the feared code-in-list class. scanLayer0's block spans and the
flat ClassifyLines code-line projection both already match goldmark on
code, including fences and indents inside list items. The lone
divergence was MDS002 on a ≤3-space-indented ATX heading: the AST path's
lineStartsWithHash reads column 1 without skipping indent, so it
flagged a heading scanLayer0 (correctly) treats as ATX. Fixed by keying
MDS002's CheckBlock isATX off the raw first byte too.
So the coarse SourceMayHaveCodeBlock guard bailed on any fence, tab, or
four-space run. That was far more conservative than the scanner needed.
The guard is now gone. The skip File carries the ClassifyLines
projection (via NewFileFlatPooled), so code-line rules serve the
validated classifier. The corpus equivalence gate now engages on
code-bearing files and stays green.
This does not by itself speed up benchmark 2: MDSMITH_LAYER0_SKIP is
still default-off, and the gate still requires every enabled rule to
be Layer 0 (the remaining migration). But it removes the blocker that
framed parse-skip on code as a scanner rewrite — it was a one-line rule
fix plus a guard removal.
- MDS002 heading-style: reads only the heading line (style + level),
never the inline tree.
CheckBlockserves the nil-AST path byte-identically, including the indented-ATX edge. - MDS013, MDS015, MDS044: confirmed byte-identical to the AST on all 453 code-bearing corpus files (the sweep above).
- Code guard removed; skip File carries
ClassifyLines; the corpus equivalence gate engages on code files and is green.
The five batch plans landed every rule's nil-AST path. But one rule still
forced the parse under parity: MDS066 commands-show-output. The walk audit
marks it B-prose-only, because it lints the fenced-code body (a block of
$ prompts) rather than ignoring it. The static category withholds Layer 0
from any code-content-sensitive rule. So IsLayer0(MDS066) stayed false and
the all-or-nothing gate kept parsing.
MDS066 is nonetheless parse-skip-safe: its CheckBlock reads the
BlockFencedCode span's body straight from f.Lines, which the Layer 0
scan delimits exactly as goldmark does. A corpus sweep confirmed its
nil-AST output is byte-identical to the AST output on all 1054 files (it
fires on one). MDS066 is the only B-prose-only rule parity enables —
MDS041, MDS050, MDS052 are all disabled by the convention — so it was the
lone blocker.
The fix generalizes the layer mapping rather than special-casing MDS066.
rulelayer.nilASTBackable promotes the whole nil-AST-safe B-prose-only
category to Layer 0, gated on the manifest's nil_ast_safe signal. A
B-prose-only rule already matched the AST run on its fixture; a
divergent rule lands in hybrid. The only withheld signal was
code-sensitivity. The validated ClassifyLines and Layer-1 projections
now reproduce that code content. This admits MDS066, parity's lone
blocker, plus the opt-in MDS041, MDS050, and MDS052.
astProjectionConsumers stays as the veto for a future rule that reads an
unbacked AST projection. Three guards back the promotion.
TestBProseOnlyRulesAreNilASTSafe pins that every B-prose-only entry is
nil_ast_safe. Each rule's TestCheck_NilASTMatchesAST checks its own
nil-AST path against the AST path. For MDS066 — the only one parity
enables — TestParityConvention_SkipsParse drives the nil-AST path on a
skip-eligible file and asserts it fires.
- Every AST-forcing parity rule resolves to Layer 0 / Layer 1.
-
TestLayer0Gate_CorpusDiagnosticsEquivalencegreen with the full parity set enabled. -
mdsmith check -c parityskips the parse on benchmark 2 (TestParityConvention_SkipsParseandTestParityConvention_AllEnabledRulesSkipSafe). - All tests pass:
go test ./....