From eb2acfeeb8888ba3936250424e4309c6d49c57b1 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 16 Sep 2026 23:59:44 +0200 Subject: [PATCH] fix(brain): move the wikilink strip into WriteNote/UpdateNote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BrainStore is not the only door. POST /write — the HTTP path CLAUDE.md hands to curl and scripts — calls api.WriteNote directly, so the strip shipped one commit ago covered brain_write and capture while leaving the raw door ungated. A gate one door wide is not a gate. The BrainStore keeps its own call so it can still report Ref.Unlinked to the MCP caller; running the strip twice is a no-op on content the primitive has already cleaned. Co-Authored-By: Claude Opus 5 --- ingestion/internal/api/handler.go | 10 ++++++ ingestion/internal/api/handler_test.go | 42 ++++++++++++++++++++++++++ ingestion/internal/api/update.go | 8 +++++ 3 files changed, 60 insertions(+) diff --git a/ingestion/internal/api/handler.go b/ingestion/internal/api/handler.go index d6cbb2a..7e75bc3 100644 --- a/ingestion/internal/api/handler.go +++ b/ingestion/internal/api/handler.go @@ -133,6 +133,16 @@ func WriteNote(brainDir string, opts WriteNoteOptions) (string, error) { return "", fmt.Errorf("content is required") } + // In the primitive, not in a caller: POST /write reaches here directly, + // without the BrainStore, and a gate one door wide is not a gate. + // BrainStore still calls the strip itself to report Ref.Unlinked; the + // second pass is a no-op on already-clean content. + clean, _, err := brain.StripUnresolvedLinks(brainDir, opts.Content) + if err != nil { + return "", err + } + opts.Content = clean + if opts.Wing != "" && opts.Hall != "" { return writeHallNote(brainDir, opts) } diff --git a/ingestion/internal/api/handler_test.go b/ingestion/internal/api/handler_test.go index 2c4e6dc..bd5acc7 100644 --- a/ingestion/internal/api/handler_test.go +++ b/ingestion/internal/api/handler_test.go @@ -497,3 +497,45 @@ func TestWriteNote_RejectsClaimFieldWithoutProvenance(t *testing.T) { _, statErr := os.Stat(filepath.Join(dir, "wiki/homelab/facts/no-provenance.md")) assert.True(t, os.IsNotExist(statErr)) } + +// The HTTP POST /write door reaches WriteNote directly, without the +// BrainStore, so the unresolved-wikilink strip has to live in the primitive +// or that door stays ungated. +func TestWriteNote_UnwrapsUnresolvedWikilinks(t *testing.T) { + dir := t.TempDir() + _, err := api.WriteNote(dir, api.WriteNoteOptions{ + Content: "# Target\n\nbody\n", Filename: "target", Wing: "a", Hall: "facts", + }) + require.NoError(t, err) + + rel, err := api.WriteNote(dir, api.WriteNoteOptions{ + Content: "See [[target]] and [[gitea-ci]].\n", + Filename: "src", Wing: "a", Hall: "facts", + }) + require.NoError(t, err) + + got, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel))) + require.NoError(t, err) + assert.Contains(t, string(got), "[[target]]") + assert.Contains(t, string(got), "and gitea-ci.") + assert.NotContains(t, string(got), "[[gitea-ci]]") +} + +func TestUpdateNote_UnwrapsUnresolvedWikilinks(t *testing.T) { + dir := t.TempDir() + _, err := api.WriteNote(dir, api.WriteNoteOptions{ + Content: "old\n", Filename: "n", Wing: "a", Hall: "facts", + }) + require.NoError(t, err) + + rel, _, _, err := api.UpdateNote(dir, api.UpdateNoteOptions{ + Wing: "a", Hall: "facts", Slug: "n", + Content: "Related: [[secret-hygiene]].\n", Reason: "r", + }) + require.NoError(t, err) + + got, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel))) + require.NoError(t, err) + assert.Contains(t, string(got), "Related: secret-hygiene.") + assert.NotContains(t, string(got), "[[secret-hygiene]]") +} diff --git a/ingestion/internal/api/update.go b/ingestion/internal/api/update.go index 9fd88f8..e493006 100644 --- a/ingestion/internal/api/update.go +++ b/ingestion/internal/api/update.go @@ -64,6 +64,14 @@ func UpdateNote(brainDir string, opts UpdateNoteOptions) (relPath, contentHash, return "", "", "", fmt.Errorf("content is required") } + // Same reason as WriteNote: the strip belongs in the primitive so every + // door inherits it. + clean, _, err := brain.StripUnresolvedLinks(brainDir, opts.Content) + if err != nil { + return "", "", "", err + } + opts.Content = clean + var rel string if opts.Path != "" { rel = opts.Path