| id | 2606171401 | ||
|---|---|---|---|
| title | Parity parse-skip: migrate the Layer-0 heading and front-matter rules | ||
| status | ✅ | ||
| summary | Add a nil-AST path to the parity rules that read only a heading's line or the front matter — MDS003 heading-increment, MDS004 first-line-heading, MDS051 single-h1, MDS069 unique-frontmatter — so each resolves to Layer 0 and stops forcing the goldmark parse, gated byte-identical to the AST path across the corpus. | ||
| model | opus | ||
| depends-on |
|
Move the heading-shape and front-matter parity rules onto the Layer-0 block scan, so they no longer force the parse.
MDS002 heading-style is the
proven template. It reads a heading's style and level from the heading
line and serves the nil-AST path through a rule.BlockChecker
CheckBlock, byte-identical to its AST CheckNode.
The rules below read only line-level facts: a heading's level
(leading-# run or setext underline), its position, or front-matter
keys. None needs the inline tree.
| Rule | Name | Reads |
|---|---|---|
| MDS003 | heading-increment | heading levels, in order |
| MDS004 | first-line-heading | first block kind + level |
| MDS051 | single-h1 | count of level-1 headings |
| MDS069 | unique-frontmatter | front-matter keys |
MDS003 and MDS004 are config-dependent, not statically Layer 0. They
carry a placeholder setting; a placeholder match reads the heading text,
which is inline. With an empty placeholder list (the parity case) that
branch is dead and the line-only path is exact. This is the same shape
as line-length, so they ride the config-aware gate from
2606171400, whose
ruleConfiguredLineCapable admits a rule when its configured instance
reports rule.LineCapable. The static rulelayer audit cannot express
that config dependence, so they cannot be marked A-no-skipping. This is
why the plan now depends on 2606171400.
Caveat on LineCapable. Its doc comment commits the interface to the
flat line classifier — "reading only f.Lines and the classifier-backed
projections", not Layer0(f).BlockSpans. A heading rule that reads
heading levels from block spans therefore does not fit the contract as
written.
Resolution. MDS003 and MDS004 take the LineCapable path. Each adds
LineCapable() bool { return len(r.Placeholders) == 0 } and a checkNilAST
that walks lint.Layer0(f).BlockSpans. Levels come from a leading-# run
or the setext underline. They stay plain Check rules that dispatch on
f.AST == nil, not BlockCheckers. The config-dependent gate
(ruleConfiguredLineCapable, plan 2606171400) admits them only with empty
placeholders.
A placeholder-config bad fixture keeps the walk audit from marking these
two statically Layer 0. The nil-AST probe returns nil for the placeholder
case, so the audit lands them hybrid. That is the nilDiverged outcome.
It is equivalent to ast-required for the gate, since both map to
LayerAST.
MDS051 single-h1 is opt-in. It reads only level-1 heading counts plus a
front-matter title — no placeholder branch — so it is unconditionally
nil-AST-safe, not "when-simple". It can take the static BlockChecker
path (like MDS002), no config gate needed.
MDS069 unique-frontmatter never touches f.AST. Its Check builds a
cross-file front-matter index. The walk audit reports it
inconclusive-not-fired: the fixture probe has no include globs, so the
rule never emits. The audit cannot confirm nil-AST safety from a probe
that never fires. The fix is an audit probe that fires (an include glob
over a multi-file fixture) or an explicit nil-AST-safe classification.
It is not a CheckBlock migration.
scanLayer0 (layer0.go) emits
BlockATXHeading / BlockSetextHeading spans only for top-level
headings. It emits one BlockList span per list line with no descent
into the item body, and one BlockQuote span that maps back only
CodeBlockLines, never the inner scan's heading spans. The AST path
(ast.Walk) visits headings nested in list items and blockquotes too.
So a nil-AST path that walks heading spans diverges from the AST for
any document with a heading inside a list or quote. A missed nested
heading is a false negative. It also corrupts MDS003's prevLevel
sequence for every later heading, and MDS004/MDS051 miscount. The
repository corpus has few such headings, so the equivalence gate would
not reliably catch the divergence.
This must be fixed first. One option extends scanLayer0 to emit nested
heading spans in ast.Walk order, recursing into list-item and
blockquote bodies and mapping their spans back the way CodeBlockLines
already is. The other has the gate force the parse for any document whose
headings are container-nested. Until then these rules cannot migrate.
Note also that the BlockSpans doc in layer0.go still says they have "no
production consumer yet" — stale once MDS010/011/031 land; update it.
For each rule:
- First resolve the nested-heading blocker above (scanner emits nested heading spans, or the gate excludes container-nested headings).
- Add the nil-AST
Checkpath that walks the Layer 0 heading spans (levels, position), reusing the existing extraction. - Wire config-aware skip eligibility (the
LineCapablecaveat above: widen the doc, or add a block-eligibility gate) for MDS003/MDS004. MDS051 takes the staticBlockCheckerpath, no config gate. - Keep the diagnostic byte-identical: same line, column, message.
- Add a
TestCheck_NilASTMatchesASTunit test, the shape MDS002 uses, with a heading inside a list and inside a blockquote, plus the gate-level skip + corpus-equivalence guards 2606171400 added.
- The nested-heading blocker is resolved: the gate already excludes
container-nested headings (
SourceMayHaveBlockQuote/SourceMayHaveListinlayer0SkipEligible), so a heading inside a list or blockquote never reaches the nil-AST path and the parse-skip and full-parse paths agree. - MDS003, MDS004 skip the parse under parity (empty placeholders) and
force it otherwise, byte-identical both ways. Each adds
LineCapable() bool { return len(r.Placeholders) == 0 }plus acheckNilASTpath; a placeholder-config bad fixture makes the walk audit classify themhybrid(config-dependent), so skip eligibility flows through theLineCapablegate, not the static Layer-0 set. MDS051 adds acheckNilASTpath reading h1 levels and the front-matter title; the audit classifies itA-no-skipping(Layer 0) with no config gate. - MDS069's nil-AST safety is resolved: a
knownNilASTSafeoverride inrulelayerforce-classifies it Layer 0 (its probe cannot fire), gated by a test that every override entry hasreads_file_ast: false, plus aTestCheck_WithNilASTproving identical diagnostics with a nil AST. - The
TestLayer0Gate_*equivalence guards stay green with these rules on. -
go test ./...passes (the only failures are the pre-existinginternal/releasePGO tests, which create signed git commits the sandbox signing server rejects — unrelated to this change).