Compare commits

...
2 Commits
Author SHA1 Message Date
mathiasandClaude Opus 5 6e69698343 fix(brain): a one-word tunnel title must match the prose case-exactly
CI / Mirror to GitHub (push) Successful in 4s
CI / Lint / Test / Vet (push) Successful in 31s
AutoTunnel matched note titles case-insensitively, so a one-word title linked
every entry that happened to use that word. Measured on the live corpus: 185
entries carry an auto-written See-also link to wiki/telos/decisions/wrong.md
(title "WRONG") and 136 to .../status.md — the two most-linked nodes in the
whole graph, and both of them artifacts of the English language rather than
references. telos's projects/problems/strategies/goals/beliefs entries account
for another ~60 between them.

That pollution is not cosmetic: inbound-link count is what the duplicate
Fixer picks survivors by, what orphan detection reads, and what brain_graph
shows.

One-word titles now need the title's own casing in the prose ("WRONG", "BERT",
"NLP" — an entity name in a sentence still links). Multi-word titles keep the
case-insensitive match: "risk tier middleware" is a real mention however it is
capitalised, and a three-word collision is not an accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 00:08:40 +02:00
mathiasandClaude Opus 5 eb2acfeeb8 fix(brain): move the wikilink strip into WriteNote/UpdateNote
CI / Mirror to GitHub (push) Successful in 4s
CI / Lint / Test / Vet (push) Successful in 32s
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 <noreply@anthropic.com>
2026-09-16 23:59:44 +02:00
5 changed files with 139 additions and 15 deletions
+10
View File
@@ -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)
}
+42
View File
@@ -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]]")
}
+8
View File
@@ -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
+22 -4
View File
@@ -54,11 +54,24 @@ func DetectTunnels(brainDir, content string) ([]TunnelCandidate, error) {
return nil
}
title, _ := readTitleAndCreated(path, strings.TrimSuffix(d.Name(), ".md"))
needle := strings.ToLower(strings.TrimSpace(title))
if needle == "" {
title = strings.TrimSpace(title)
if title == "" {
return nil
}
idx := strings.Index(lowerContent, needle)
// A one-word title has to match the prose case-exactly. Matched
// case-insensitively, it links every entry that happens to use the
// word: "WRONG" (wiki/telos/decisions/wrong.md) collected 185 inbound
// See-also links and "Status" 136, making the two most-linked nodes in
// the graph artifacts of the English language rather than references.
// Multi-word titles keep the case-insensitive match — "risk tier
// middleware" in prose is a real mention however it is capitalised,
// and a three-word collision is not an accident.
haystack, needle := lowerContent, strings.ToLower(title)
if isSingleWord(title) {
haystack, needle = content, title
}
idx := strings.Index(haystack, needle)
if idx == -1 {
return nil
}
@@ -69,7 +82,7 @@ func DetectTunnels(brainDir, content string) ([]TunnelCandidate, error) {
out = append(out, TunnelCandidate{
TargetPath: filepath.ToSlash(rel),
MatchedTerm: title,
Exact: isWholeWord(lowerContent, idx, len(needle)),
Exact: isWholeWord(haystack, idx, len(needle)),
})
return nil
})
@@ -79,6 +92,11 @@ func DetectTunnels(brainDir, content string) ([]TunnelCandidate, error) {
return out, nil
}
// isSingleWord reports whether title is one whitespace-free token.
func isSingleWord(title string) bool {
return !strings.ContainsAny(title, " \t")
}
// isWholeWord reports whether the substring at [idx, idx+n) in s is
// bounded by non-alphanumeric characters (or string edges).
func isWholeWord(s string, idx, n int) bool {
+57 -11
View File
@@ -102,16 +102,29 @@ func TestDetectTunnels_ExactTitleMatch(t *testing.T) {
func TestDetectTunnels_FuzzyMatch(t *testing.T) {
dir := t.TempDir()
seedNote(t, dir, "wiki/x/facts/routing.md",
"---\ntitle: Routing\n---\nbody.\n")
seedNote(t, dir, "wiki/x/facts/routing-table.md",
"---\ntitle: 'Routing Table'\n---\nbody.\n")
// Substring of title appears in content, but not as a whole word.
candidates, err := brain.DetectTunnels(dir, "rerouting handles failover")
candidates, err := brain.DetectTunnels(dir, "the routing tables were rebuilt")
require.NoError(t, err)
require.Len(t, candidates, 1)
assert.False(t, candidates[0].Exact, "substring-only match should be fuzzy")
}
// A one-word title whose only appearance is lowercase inside another word is
// not even a candidate: it would fill the review file with English rather than
// with references.
func TestDetectTunnels_SingleWordSubstringIsNotACandidate(t *testing.T) {
dir := t.TempDir()
seedNote(t, dir, "wiki/x/facts/routing.md",
"---\ntitle: Routing\n---\nbody.\n")
candidates, err := brain.DetectTunnels(dir, "rerouting handles failover")
require.NoError(t, err)
assert.Empty(t, candidates)
}
func TestDetectTunnels_NoFrontmatterFallsBackToSlug(t *testing.T) {
dir := t.TempDir()
seedNote(t, dir, "wiki/x/facts/widget-flags.md", "# widget flags\n\nbody.\n")
@@ -126,20 +139,21 @@ func TestDetectTunnels_NoFrontmatterFallsBackToSlug(t *testing.T) {
func TestAutoTunnel_FuzzyGoesToCandidatesFile(t *testing.T) {
dir := t.TempDir()
// Existing note in a different wing whose title is "Routing".
seedNote(t, dir, "wiki/other/facts/routing.md",
"---\nwing: other\nhall: facts\ntitle: Routing\n---\nbody.\n")
// Source note in another wing whose body mentions "rerouting" (substring match only).
// Existing note in a different wing whose title is "Routing Table".
seedNote(t, dir, "wiki/other/facts/routing-table.md",
"---\nwing: other\nhall: facts\ntitle: 'Routing Table'\n---\nbody.\n")
// Source note in another wing whose body mentions "routing tables"
// (substring match only — the title is not there as a whole word).
seedNote(t, dir, "wiki/jepa-fx/facts/new.md",
"---\nwing: jepa-fx\nhall: facts\n---\nrerouting traffic\n")
"---\nwing: jepa-fx\nhall: facts\n---\nrouting tables rebuilt\n")
require.NoError(t, brain.AutoTunnel(dir,
"wiki/jepa-fx/facts/new.md", "rerouting traffic"))
"wiki/jepa-fx/facts/new.md", "routing tables rebuilt"))
// Source must not get auto-linked (fuzzy).
got, err := os.ReadFile(filepath.Join(dir, "wiki/jepa-fx/facts/new.md"))
require.NoError(t, err)
assert.NotContains(t, string(got), "[[other/facts/routing]]")
assert.NotContains(t, string(got), "[[other/facts/routing-table]]")
// Candidates file must list the pair.
matches, err := filepath.Glob(filepath.Join(dir, "raw", "tunnel-candidates-*.md"))
@@ -148,7 +162,7 @@ func TestAutoTunnel_FuzzyGoesToCandidatesFile(t *testing.T) {
body, err := os.ReadFile(matches[0])
require.NoError(t, err)
assert.Contains(t, string(body), "wiki/jepa-fx/facts/new.md")
assert.Contains(t, string(body), "wiki/other/facts/routing.md")
assert.Contains(t, string(body), "wiki/other/facts/routing-table.md")
assert.Contains(t, string(body), "Routing")
}
@@ -175,3 +189,35 @@ func TestWriteTunnel_AppendsToExistingSeeAlso(t *testing.T) {
assert.Contains(t, s, "[[a/facts/old]]")
assert.Contains(t, s, "[[b/facts/y]]")
}
// A one-word title matched case-insensitively links every entry that happens
// to use that word. Live cost before this rule: 185 entries auto-linked to
// wiki/telos/decisions/wrong.md (title "WRONG") and 136 to .../status.md,
// making the two most-linked nodes in the graph pure artifacts.
func TestDetectTunnels_SingleWordTitleNeedsAnExactCaseMatch(t *testing.T) {
dir := t.TempDir()
seedNote(t, dir, "wiki/telos/decisions/wrong.md",
"---\nwing: telos\nhall: decisions\ntitle: WRONG\n---\n# WRONG\n\nRetired beliefs.\n")
got, err := brain.DetectTunnels(dir, "The obvious fix was to reject the write. Wrong here.")
require.NoError(t, err)
for _, c := range got {
assert.False(t, c.Exact, "a lowercase prose word must not auto-link a one-word title: %+v", c)
}
got, err = brain.DetectTunnels(dir, "The beliefs listed under WRONG were retired.")
require.NoError(t, err)
require.Len(t, got, 1)
assert.True(t, got[0].Exact, "the title's own casing appearing in prose is a real reference")
}
func TestDetectTunnels_MultiWordTitleStaysCaseInsensitive(t *testing.T) {
dir := t.TempDir()
seedNote(t, dir, "wiki/homelab/facts/risk-tier.md",
"---\nwing: homelab\nhall: facts\ntitle: 'Risk Tier Middleware'\n---\nbody\n")
got, err := brain.DetectTunnels(dir, "We rebuilt the risk tier middleware last week.")
require.NoError(t, err)
require.Len(t, got, 1)
assert.True(t, got[0].Exact)
}