Hand-authored and promoted notes skip pipeline.BuildPages, so nothing quotes their scalars. A title carrying an unescaped ": " yields a block yaml.Unmarshal rejects, and every consumer that reads frontmatter drops the note silently — 18 files repo-wide today, up from the 8 in #90. frontmatter.Lint reports it; frontmatter.Fix quotes only what quoting can rescue and refuses to return a document that still fails to parse, so an unfixable file comes back byte-identical rather than half-repaired. Scan walks a tree and reports an unreadable file as a finding instead of aborting, so one poisoned note cannot hide the rest. Sequence items are deliberately left alone: "- foo: bar" is valid YAML that silently becomes a map, which is a type corruption rather than a parse failure, and repairing it would rewrite notes that currently load. task lint:frontmatter # report task lint:frontmatter FIX=1 # repair in place Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
85 lines
2.2 KiB
Go
85 lines
2.2 KiB
Go
// ingestion/internal/frontmatter/scan.go
|
|
package frontmatter
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Finding is one markdown file whose frontmatter does not parse.
|
|
type Finding struct {
|
|
Path string // relative to the scanned root
|
|
Err error // why it does not parse, or why the repair was refused
|
|
Repaired bool // true only when fix was requested and the file was rewritten
|
|
}
|
|
|
|
// Scan walks root for *.md files whose frontmatter fails to parse.
|
|
//
|
|
// With fix set, each finding that quoting can rescue is rewritten in place;
|
|
// anything quoting cannot rescue is reported and left byte-identical. A file
|
|
// that cannot be read or decoded is reported as a finding rather than aborting
|
|
// the walk — one poisoned note must not hide the other findings.
|
|
func Scan(root string, fix bool) ([]Finding, error) {
|
|
var found []Finding
|
|
|
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() || !strings.EqualFold(filepath.Ext(path), ".md") {
|
|
return nil
|
|
}
|
|
|
|
rel, relErr := filepath.Rel(root, path)
|
|
if relErr != nil {
|
|
rel = path
|
|
}
|
|
|
|
doc, readErr := os.ReadFile(path)
|
|
if readErr != nil {
|
|
found = append(found, Finding{Path: rel, Err: readErr})
|
|
return nil
|
|
}
|
|
if !utf8.Valid(doc) {
|
|
found = append(found, Finding{Path: rel, Err: fmt.Errorf("not valid UTF-8")})
|
|
return nil
|
|
}
|
|
if lintErr := Lint(doc); lintErr == nil {
|
|
return nil
|
|
}
|
|
|
|
if !fix {
|
|
found = append(found, Finding{Path: rel, Err: Lint(doc)})
|
|
return nil
|
|
}
|
|
|
|
repaired, changed, fixErr := Fix(doc)
|
|
if fixErr != nil || !changed {
|
|
found = append(found, Finding{Path: rel, Err: fixErr})
|
|
return nil
|
|
}
|
|
if writeErr := writeFilePreservingMode(path, repaired); writeErr != nil {
|
|
found = append(found, Finding{Path: rel, Err: writeErr})
|
|
return nil
|
|
}
|
|
found = append(found, Finding{Path: rel, Repaired: true})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan %s: %w", root, err)
|
|
}
|
|
return found, nil
|
|
}
|
|
|
|
func writeFilePreservingMode(path string, content []byte) error {
|
|
mode := fs.FileMode(0o644)
|
|
if info, err := os.Stat(path); err == nil {
|
|
mode = info.Mode().Perm()
|
|
}
|
|
return os.WriteFile(path, content, mode)
|
|
}
|