fix(ingest): CanonicalizeLinks strips wing:/wiki/ prefix and repairs path-style links (#87)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Roq1ajWKR5f1hG5Df9wC6A
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user