fix(pipeline,api,wing): never archive a note as processed when nothing was ingested; titles do not merge sources; ties prefer the page on disk (review of #211, pass 3)
CI / Lint / Test / Vet (pull_request) Successful in 1m6s
CI / Mirror to GitHub (pull_request) Skipped

- in a wing, an ingest that writes no source page is an error: the note goes to
  raw/failed with the warnings in log.md instead of leaving the inbox as
  'processed' (model returned [], only invalid types, only clashing entities)
- the wing's source path is deterministic, so Resolve no longer redirects a
  source onto another note's page because the model gave both the same title
  ('Home'); that overwrote the hash and the text
- a batch tie between concept and entity prefers the page already on disk, so
  its update is not lost; the wing's own source page keeps its slug even when a
  foreign page shares it
- brain_pending lists an unreadable (over-cap) note with the reason instead of
  dropping it; the client filter's 4 KiB match-span limit is documented
Each has a test that fails without it (mutation-checked).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 20:56:03 +02:00
co-authored by Claude Sonnet 5.5
parent 49286ba8b8
commit 27b1579b80
8 changed files with 214 additions and 10 deletions
+13
View File
@@ -32,6 +32,13 @@ func Resolve(proposed []wiki.Page, inventory map[wiki.PageType][]wiki.Entry) []w
out := make([]wiki.Page, 0, len(proposed))
for _, page := range proposed {
if isWingSourcePath(page.Path) {
// In a wing the source's path is its filename-derived slug. Matching
// by TITLE would merge two different notes the model happened to
// title alike ("Home", "Untitled") into one page.
out = append(out, page)
continue
}
pt := pageTypeOf(page)
title := extractTitle(page.Content)
k := key{pt: pt, normalized: normalizeTitle(title)}
@@ -97,3 +104,9 @@ func extractTitle(content string) string {
}
return ""
}
// isWingSourcePath reports whether path is wiki/<wing>/sources/<slug>.md.
func isWingSourcePath(path string) bool {
parts := strings.Split(filepath.ToSlash(path), "/")
return len(parts) == 4 && parts[0] == "wiki" && parts[2] == "sources"
}