fix(ingest): merge existing frontmatter in writeHallNote instead of stacking (#86)
opts.Content can already carry its own "---\n...\n---" block (e.g. from an upstream extraction/promotion step). writeHallNote used to prepend a second block unconditionally, making a standard frontmatter parser blind to everything the first block declared (title, tags, ...). splitFrontmatter now pulls the existing block's fields out first; wing/hall/created_at are always fresh-injected, and type/domain/ source_type prefer the note's own existing value over the opts fallback. Remaining existing fields are carried through verbatim into the single merged block. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Roq1ajWKR5f1hG5Df9wC6A
This commit is contained in:
@@ -156,17 +156,40 @@ func writeHallNote(brainDir string, opts WriteNoteOptions) (string, error) {
|
||||
return "", fmt.Errorf("create hall dir: %w", err)
|
||||
}
|
||||
|
||||
existingFields, body := splitFrontmatter(opts.Content)
|
||||
existingByKey := make(map[string]frontmatterField, len(existingFields))
|
||||
for _, f := range existingFields {
|
||||
existingByKey[f.key] = f
|
||||
}
|
||||
emitted := make(map[string]bool, 6)
|
||||
|
||||
var fm strings.Builder
|
||||
fm.WriteString("---\n")
|
||||
fmt.Fprintf(&fm, "wing: %s\n", brain.Sanitise(opts.Wing))
|
||||
fmt.Fprintf(&fm, "hall: %s\n", opts.Hall)
|
||||
fmt.Fprintf(&fm, "created_at: %s\n", time.Now().UTC().Format(time.RFC3339))
|
||||
if opts.Type != "" {
|
||||
fmt.Fprintf(&fm, "type: %s\n", opts.Type)
|
||||
}
|
||||
if opts.Domain != "" {
|
||||
fmt.Fprintf(&fm, "domain: %s\n", opts.Domain)
|
||||
emitted["wing"], emitted["hall"], emitted["created_at"] = true, true, true
|
||||
|
||||
// writeField merges one key: opts.Content's own value (if the note already
|
||||
// carries this field in its own frontmatter) always wins over the fallback,
|
||||
// so promotion/extraction-step metadata survives verbatim instead of being
|
||||
// shadowed by a second, stacked frontmatter block (#86).
|
||||
writeField := func(key, fallback string) {
|
||||
emitted[key] = true
|
||||
if f, ok := existingByKey[key]; ok {
|
||||
for _, line := range f.lines {
|
||||
fm.WriteString(line)
|
||||
fm.WriteString("\n")
|
||||
}
|
||||
return
|
||||
}
|
||||
if fallback != "" {
|
||||
fmt.Fprintf(&fm, "%s: %s\n", key, fallback)
|
||||
}
|
||||
}
|
||||
writeField("type", opts.Type)
|
||||
writeField("domain", opts.Domain)
|
||||
|
||||
sourceType := opts.SourceType
|
||||
if sourceType == "" && opts.Hall == "facts" {
|
||||
// Most hall=facts entries are first-party (an eval/benchmark the
|
||||
@@ -175,18 +198,69 @@ func writeHallNote(brainDir string, opts WriteNoteOptions) (string, error) {
|
||||
// citation-needing entry (brain-gardener#7).
|
||||
sourceType = "internal"
|
||||
}
|
||||
if sourceType != "" {
|
||||
fmt.Fprintf(&fm, "source_type: %s\n", sourceType)
|
||||
writeField("source_type", sourceType)
|
||||
|
||||
for _, f := range existingFields {
|
||||
if emitted[f.key] {
|
||||
continue
|
||||
}
|
||||
for _, line := range f.lines {
|
||||
fm.WriteString(line)
|
||||
fm.WriteString("\n")
|
||||
}
|
||||
}
|
||||
fm.WriteString("---\n")
|
||||
|
||||
if err := os.WriteFile(dest, []byte(fm.String()+opts.Content), 0o644); err != nil {
|
||||
if err := os.WriteFile(dest, []byte(fm.String()+body), 0o644); err != nil {
|
||||
return "", fmt.Errorf("write: %w", err)
|
||||
}
|
||||
rel, _ := filepath.Rel(brainDir, dest)
|
||||
return filepath.ToSlash(rel), nil
|
||||
}
|
||||
|
||||
// frontmatterField is one top-level YAML key from a frontmatter block,
|
||||
// along with its raw line and any indented continuation lines (e.g. a
|
||||
// bulleted list value spanning multiple lines).
|
||||
type frontmatterField struct {
|
||||
key string
|
||||
lines []string
|
||||
}
|
||||
|
||||
// splitFrontmatter splits a leading "---\n...\n---\n" YAML block out of
|
||||
// content, returning its top-level fields in original order and the
|
||||
// remaining body. If content has no leading frontmatter block, fields is
|
||||
// nil and body is content unchanged.
|
||||
func splitFrontmatter(content string) (fields []frontmatterField, body string) {
|
||||
if !strings.HasPrefix(content, "---\n") {
|
||||
return nil, content
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
i := 1
|
||||
var cur *frontmatterField
|
||||
for ; i < len(lines); i++ {
|
||||
line := lines[i]
|
||||
if strings.TrimSpace(line) == "---" {
|
||||
i++
|
||||
break
|
||||
}
|
||||
if line != "" && !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
|
||||
if cur != nil {
|
||||
fields = append(fields, *cur)
|
||||
}
|
||||
key, _, _ := strings.Cut(line, ":")
|
||||
cur = &frontmatterField{key: strings.TrimSpace(key), lines: []string{line}}
|
||||
} else if cur != nil {
|
||||
cur.lines = append(cur.lines, line)
|
||||
}
|
||||
}
|
||||
if cur != nil {
|
||||
fields = append(fields, *cur)
|
||||
}
|
||||
body = strings.Join(lines[i:], "\n")
|
||||
return fields, body
|
||||
}
|
||||
|
||||
// writeLegacyNote preserves the original brain/knowledge/ behaviour for
|
||||
// callers that have not adopted the wing/hall taxonomy.
|
||||
func writeLegacyNote(brainDir string, opts WriteNoteOptions) (string, error) {
|
||||
|
||||
@@ -186,6 +186,48 @@ func TestWriteNote_HallRouteOmitsSourceTypeForNonFactsHalls(t *testing.T) {
|
||||
assert.NotContains(t, string(got), "source_type")
|
||||
}
|
||||
|
||||
func TestWriteNote_HallRouteMergesExistingFrontmatterInsteadOfStacking(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
rel, err := api.WriteNote(dir, api.WriteNoteOptions{
|
||||
Content: "---\ntitle: act_runner host-executor\ntags: [gitea-actions, act_runner]\n---\n\n# Body\n\nSome content.\n",
|
||||
Filename: "act-runner-host-executor",
|
||||
Wing: "homelab",
|
||||
Hall: "failures",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel)))
|
||||
require.NoError(t, err)
|
||||
body := string(got)
|
||||
|
||||
// exactly one frontmatter block: only two "---" delimiter lines total
|
||||
assert.Equal(t, 2, strings.Count(body, "---\n"), "expected a single merged frontmatter block, not stacked blocks")
|
||||
assert.Contains(t, body, "wing: homelab")
|
||||
assert.Contains(t, body, "hall: failures")
|
||||
assert.Contains(t, body, "title: act_runner host-executor")
|
||||
assert.Contains(t, body, "tags: [gitea-actions, act_runner]")
|
||||
assert.Contains(t, body, "# Body")
|
||||
}
|
||||
|
||||
func TestWriteNote_HallRouteExistingTypeWinsOverOptsType(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
rel, err := api.WriteNote(dir, api.WriteNoteOptions{
|
||||
Content: "---\ntype: hypothesis\n---\n\nBody.\n",
|
||||
Filename: "note",
|
||||
Wing: "agentsquad",
|
||||
Hall: "decisions",
|
||||
Type: "decision", // should lose to content's own "type: hypothesis"
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel)))
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(got), "type: hypothesis")
|
||||
assert.NotContains(t, string(got), "type: decision")
|
||||
}
|
||||
|
||||
func TestWrite_GeneratesFilenameIfAbsent(t *testing.T) {
|
||||
dir, h := setup(t)
|
||||
body, _ := json.Marshal(map[string]any{"content": "auto name"})
|
||||
|
||||
Reference in New Issue
Block a user