feat(brain): cross-wing tunnels — bidirectional wikilinks + auto-detect
All checks were successful
CI / Lint / Test / Vet (push) Successful in 11s
CI / Mirror to GitHub (push) Successful in 3s

Adds the `brain_tunnel` MCP tool and auto-tunnel behaviour for
`brain_write`, so concepts that appear in multiple wings become
navigable from any of them.

New surface in package brain:
- WriteTunnel(brainDir, src, tgt) — appends a `## See also` bidirectional
  wikilink between two notes in different wings. Idempotent (link not
  duplicated on re-call) and reuses an existing See also section.
- DetectTunnels(brainDir, content) — walks brain/wiki/, returns
  TunnelCandidates for notes whose title appears in content. Tags
  whole-word case-insensitive hits as Exact=true and substring-only hits
  as Exact=false.
- AutoTunnel(brainDir, src, content) — wraps DetectTunnels: writes
  cross-wing exact matches, stages fuzzy matches into
  brain/raw/tunnel-candidates-<YYYY-MM-DD>.md for human review.

MCP wiring:
- `brain_tunnel` tool: explicit manual link (source, target).
- `brain_write` with wing+hall now triggers AutoTunnel on the new
  content. Failures are logged and never abort the primary write.

readTitleAndCreated also humanises the slug fallback (hyphens → spaces)
so titleless notes participate in content matching.

Closes hyperguild#16.

Tests: idempotency, same-wing rejection, missing-note rejection,
See-also reuse, exact/fuzzy detection, slug fallback, MCP tool happy
path, auto-tunnel hook (cross-wing exact → linked; same-wing → skipped;
fuzzy → candidates file).
This commit is contained in:
Mathias
2026-05-18 21:32:49 +02:00
parent 61b6247df9
commit ddd07ae7eb
7 changed files with 588 additions and 6 deletions

View File

@@ -122,6 +122,92 @@ func TestBrainQueryWingScope(t *testing.T) {
assert.NotContains(t, text, "wiki/other/facts/y.md")
}
func TestBrainWriteAutoTunnelsOnExactMatch(t *testing.T) {
brainDir := t.TempDir()
// Seed a pre-existing note in wing "other".
existing := filepath.Join(brainDir, "wiki/other/facts/widget.md")
require.NoError(t, os.MkdirAll(filepath.Dir(existing), 0o755))
require.NoError(t, os.WriteFile(existing,
[]byte("---\nwing: other\nhall: facts\ntitle: Widget\n---\nbody.\n"), 0o644))
srv := mcp.NewServer(brainDir, nil, nil, nil)
// Write a new note in a *different* wing whose content references "Widget".
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "# Notes\n\nThis note discusses the Widget concept.\n",
"filename": "notes",
"wing": "jepa-fx",
"hall": "facts",
})
require.Nil(t, resp["error"])
newNote := filepath.Join(brainDir, "wiki/jepa-fx/facts/notes.md")
got, err := os.ReadFile(newNote)
require.NoError(t, err)
assert.Contains(t, string(got), "[[other/facts/widget]]", "new note should link to existing")
gotTgt, err := os.ReadFile(existing)
require.NoError(t, err)
assert.Contains(t, string(gotTgt), "[[jepa-fx/facts/notes]]", "existing note should backlink")
}
func TestBrainWriteAutoTunnelSkipsSameWing(t *testing.T) {
brainDir := t.TempDir()
existing := filepath.Join(brainDir, "wiki/jepa-fx/facts/widget.md")
require.NoError(t, os.MkdirAll(filepath.Dir(existing), 0o755))
require.NoError(t, os.WriteFile(existing,
[]byte("---\nwing: jepa-fx\nhall: facts\ntitle: Widget\n---\nbody.\n"), 0o644))
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "Same wing reference to Widget here.\n",
"filename": "notes",
"wing": "jepa-fx",
"hall": "facts",
})
require.Nil(t, resp["error"])
newNote := filepath.Join(brainDir, "wiki/jepa-fx/facts/notes.md")
got, err := os.ReadFile(newNote)
require.NoError(t, err)
assert.NotContains(t, string(got), "[[jepa-fx/facts/widget]]", "same-wing match must not auto-tunnel")
}
func TestBrainTunnelLinksTwoNotes(t *testing.T) {
brainDir := t.TempDir()
for _, p := range []struct{ rel, body string }{
{"wiki/jepa-fx/decisions/val-vol.md", "---\nwing: jepa-fx\nhall: decisions\n---\n# Val Vol\n"},
{"wiki/hyperguild/decisions/routing.md", "---\nwing: hyperguild\nhall: decisions\n---\n# Routing\n"},
} {
full := filepath.Join(brainDir, p.rel)
require.NoError(t, os.MkdirAll(filepath.Dir(full), 0o755))
require.NoError(t, os.WriteFile(full, []byte(p.body), 0o644))
}
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_tunnel", map[string]any{
"source": "wiki/jepa-fx/decisions/val-vol.md",
"target": "wiki/hyperguild/decisions/routing.md",
})
require.Nil(t, resp["error"])
src, err := os.ReadFile(filepath.Join(brainDir, "wiki/jepa-fx/decisions/val-vol.md"))
require.NoError(t, err)
assert.Contains(t, string(src), "[[hyperguild/decisions/routing]]")
tgt, err := os.ReadFile(filepath.Join(brainDir, "wiki/hyperguild/decisions/routing.md"))
require.NoError(t, err)
assert.Contains(t, string(tgt), "[[jepa-fx/decisions/val-vol]]")
}
func TestBrainTunnelRejectsMissing(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_tunnel", map[string]any{
"source": "wiki/a/facts/ghost.md",
"target": "wiki/b/facts/ghost.md",
})
require.NotNil(t, resp["error"])
}
func TestBrainWriteRejectsTraversal(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)