From 6d014c1d0fcd146ee868b6e7430c34b72299b525 Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 27 Jul 2026 14:32:31 +0200 Subject: [PATCH] fix(ingest): CanonicalizeLinks strips wing:/wiki/ prefix and repairs path-style links (#87) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plainLinkRE matches any [[...]] without a pipe, including path-prefixed forms like [[wing:homelab/failures/foo]] or [[wiki/agentsquad/decisions/bar]] that the LLM extraction step occasionally emits. Title-lookup against titleToSlug always fails for these (they're paths, not titles), so they were silently left broken. Before falling to the unknown-wikilink warning, strip a known wing:/wiki/ root prefix and emit the clean wing/hall/slug path directly — matching the brain-graph path-style link convention. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Roq1ajWKR5f1hG5Df9wC6A --- ingestion/internal/pipeline/links.go | 30 ++++++++++++--- ingestion/internal/pipeline/links_test.go | 47 +++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/ingestion/internal/pipeline/links.go b/ingestion/internal/pipeline/links.go index 92814e6..9196f26 100644 --- a/ingestion/internal/pipeline/links.go +++ b/ingestion/internal/pipeline/links.go @@ -51,6 +51,21 @@ func buildTitleMap(pages []wiki.Page, inventory map[wiki.PageType][]wiki.Entry) return m } +// pathStylePrefixes are known root prefixes the LLM extraction step +// sometimes bakes into a wikilink target instead of emitting a clean +// wing/hall/slug path (or bare title). Stripping them repairs the link +// in place — see hyperguild#87. +var pathStylePrefixes = []string{"wing:", "wiki/"} + +func stripPathStylePrefix(displayName string) (string, bool) { + for _, prefix := range pathStylePrefixes { + if stripped, ok := strings.CutPrefix(displayName, prefix); ok { + return stripped, true + } + } + return displayName, false +} + func canonicalizeContent(content string, titleToSlug map[string]string) (string, []string) { var warnings []string result := plainLinkRE.ReplaceAllStringFunc(content, func(match string) string { @@ -59,12 +74,17 @@ func canonicalizeContent(content string, titleToSlug map[string]string) (string, return match } displayName := sub[1] - slug, ok := titleToSlug[strings.ToLower(displayName)] - if !ok { - warnings = append(warnings, fmt.Sprintf("unknown wikilink: [[%s]]", displayName)) - return match + + if slug, ok := titleToSlug[strings.ToLower(displayName)]; ok { + return "[[" + slug + "|" + displayName + "]]" } - return "[[" + slug + "|" + displayName + "]]" + + if stripped, hadPrefix := stripPathStylePrefix(displayName); hadPrefix { + return "[[" + stripped + "]]" + } + + warnings = append(warnings, fmt.Sprintf("unknown wikilink: [[%s]]", displayName)) + return match }) return result, warnings } diff --git a/ingestion/internal/pipeline/links_test.go b/ingestion/internal/pipeline/links_test.go index 340ab38..86125dc 100644 --- a/ingestion/internal/pipeline/links_test.go +++ b/ingestion/internal/pipeline/links_test.go @@ -102,6 +102,53 @@ func TestCanonicalizeLinks_CurrentBatchPagesResolved(t *testing.T) { assert.Contains(t, got[0].Content, "[[betting|Betting]]") } +func TestCanonicalizeLinks_StripsWingColonPrefix(t *testing.T) { + pages := []wiki.Page{ + { + Path: "wiki/homelab/failures/act-runner-host-mode-container-needs-node-and-libatomic.md", + Content: "---\ntitle: 'act_runner host-mode'\n---\n\nSee [[wing:homelab/failures/rootless-buildah-act-runner-run-containers-denied]].\n", + }, + } + got, warnings := CanonicalizeLinks(pages, map[wiki.PageType][]wiki.Entry{}) + require.Len(t, got, 1) + assert.Empty(t, warnings) + assert.Contains(t, got[0].Content, "[[homelab/failures/rootless-buildah-act-runner-run-containers-denied]]") + assert.NotContains(t, got[0].Content, "wing:") +} + +func TestCanonicalizeLinks_StripsWikiSlashPrefix(t *testing.T) { + pages := []wiki.Page{ + { + Path: "wiki/agentsquad/hypotheses/council-consolidation-standalone-deliberation-service.md", + Content: "---\ntitle: 'council consolidation'\n---\n\nSee [[wiki/agentsquad/decisions/autoresearch-council-sibling-pipe]].\n", + }, + } + got, warnings := CanonicalizeLinks(pages, map[wiki.PageType][]wiki.Entry{}) + require.Len(t, got, 1) + assert.Empty(t, warnings) + assert.Contains(t, got[0].Content, "[[agentsquad/decisions/autoresearch-council-sibling-pipe]]") + assert.NotContains(t, got[0].Content, "wiki/agentsquad/decisions/autoresearch-council-sibling-pipe]]\n\n") // no leftover wiki/ prefix + assert.NotContains(t, got[0].Content, "[[wiki/") +} + +func TestCanonicalizeLinks_TitleLookupStillTakesPriorityOverPrefixStrip(t *testing.T) { + // A plain link that resolves via the title map must still use the + // normal slug|Display form, not fall through to prefix-strip repair. + pages := []wiki.Page{ + { + Path: "wiki/sources/shape-up.md", + Content: "---\ntitle: 'Shape Up'\n---\n\nSee [[Betting]].\n", + }, + } + inventory := map[wiki.PageType][]wiki.Entry{ + wiki.PageTypeConcept: {{Slug: "betting", Title: "Betting"}}, + } + got, warnings := CanonicalizeLinks(pages, inventory) + require.Len(t, got, 1) + assert.Empty(t, warnings) + assert.Contains(t, got[0].Content, "[[betting|Betting]]") +} + func TestCanonicalizeLinks_MultipleLinksInOnePage(t *testing.T) { pages := []wiki.Page{ {