fix(frontmatter): consolidate 3 independent fence-parsers into one
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>
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
package api
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
// Aliased: this package's own frontmatter type (below) would collide
|
||||
// with the imported package's name otherwise.
|
||||
sharedfm "github.com/mathiasbq/hyperguild/ingestion/internal/frontmatter"
|
||||
)
|
||||
|
||||
// frontmatter is an ordered, line-preserving view of a note's YAML
|
||||
// frontmatter block. It deliberately avoids a full YAML round-trip: the
|
||||
@@ -25,19 +31,26 @@ type fmLine struct {
|
||||
// frontmatter block is recognised only when the file opens with a `---`
|
||||
// fence and a closing `---` fence follows. Otherwise the whole input is
|
||||
// the body and the returned frontmatter is empty.
|
||||
//
|
||||
// Fence detection itself is frontmatter.Split (hyperguild#138) — this
|
||||
// package's own contribution is purely the ordered, line-preserving fmLine
|
||||
// shape below, which a shared parser has no business owning since it's
|
||||
// specific to how brain_update does a surgical, byte-round-trip edit.
|
||||
func parseFrontmatter(src string) (frontmatter, string) {
|
||||
var fm frontmatter
|
||||
if !strings.HasPrefix(src, "---\n") {
|
||||
blockBytes, restBytes, ok := sharedfm.Split([]byte(src))
|
||||
if !ok {
|
||||
return fm, src
|
||||
}
|
||||
rest := src[len("---\n"):]
|
||||
end := strings.Index(rest, "\n---\n")
|
||||
if end < 0 {
|
||||
// Opening fence with no closing fence — treat as bodyless content.
|
||||
return fm, src
|
||||
}
|
||||
block := rest[:end]
|
||||
body := rest[end+len("\n---\n"):]
|
||||
// sharedfm.Split's block/rest boundary sits one byte earlier than this
|
||||
// package's always has: block keeps the "\n" immediately before the
|
||||
// closing fence, and rest keeps the "\n" immediately after it (both by
|
||||
// design, for Fix's own reconstruction). This package's block/body
|
||||
// never included either — TrimSuffix/TrimPrefix restore that exact
|
||||
// boundary. Split guarantees ok=true only when "\n---\n" matched, so
|
||||
// both trims are always exactly one byte, never ambiguous.
|
||||
block := strings.TrimSuffix(string(blockBytes), "\n")
|
||||
body := strings.TrimPrefix(string(restBytes), "\n")
|
||||
|
||||
for _, line := range strings.Split(block, "\n") {
|
||||
// An indented line is a continuation (a YAML block-list item, a
|
||||
|
||||
Reference in New Issue
Block a user