Round-1 review found a real regression: tightening Split's closing-fence
match to require a trailing newline (closing the "\n---extra" bypass)
also silently narrowed internal/frontmatter's own Lint/Fix behavior — a
document whose closing fence is literally the last bytes of the file
("---\ntitle: x\n---", no body at all) used to parse fine via this
package's private split, and now returns ok=false, contradicting this
PR's own "zero behavior change for Lint/Fix" claim. internal/api and
internal/mcp were unaffected (both already required the trailing newline
pre-consolidation), but cmd/frontmatterlint's Lint/Fix relied on the old,
looser acceptance.
Split now distinguishes "fence followed by a newline" (real body follows)
and "fence is the last bytes of the document" (no body, not a bypass
attempt) — both valid — from "fence followed by anything else" (the
actual bypass shape, still rejected). Regression test pins the
literal-EOF case; flip-tested to confirm it's load-bearing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hyperguild#138: internal/mcp/tools_notes.go's frontmatterBlock,
internal/api/frontmatter.go's parseFrontmatter, and
internal/frontmatter/frontmatter.go's split each independently detected a
note's --- fence, converging on the same shape through parallel bug-fixing
across today's #123/#126/#128 sagas. Each carried a subtly different gap:
internal/frontmatter's closing-fence match had no trailing-newline
requirement (a document ending "\n---extra" was treated as validly closed);
internal/mcp needed CRLF/BOM guards bolted on across two separate review
rounds after both silently defeated its own HasPrefix check; internal/api
never gained either fix, so the same encoding-dependent bypass was live
there too, just never triggered by a test.
internal/frontmatter is the leaf package (imports nothing internal), so it
becomes the canonical fence detector: exported Split(doc []byte) (block,
rest []byte, ok bool), BOM-stripped and CRLF-normalized, with the stricter
trailing-newline-required close. internal/api and internal/mcp now call it
instead of re-detecting the fence themselves, keeping only what's genuinely
package-specific (api's ordered fmLine round-trip shape, mcp's map + block-
scalar-fold logic).
Found two real regressions while wiring the callers, caught by each
package's own pre-existing byte-exactness tests going red immediately:
Split's block/rest boundary sits one byte differently than either caller's
prior contract (it keeps the fence-adjacent newline on each side, by design
for Fix's own reconstruction) — both callers now TrimSuffix/TrimPrefix
exactly that one byte to restore their exact prior semantics. Neither
regression would have been visible without those tests already existing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>