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
91 lines
2.8 KiB
Go
91 lines
2.8 KiB
Go
// ingestion/internal/pipeline/links.go
|
|
package pipeline
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/wiki"
|
|
)
|
|
|
|
// plainLinkRE matches [[Display Name]] — wikilinks without a slug pipe.
|
|
// It does NOT match [[slug|Display]] (those already have a pipe).
|
|
var plainLinkRE = regexp.MustCompile(`\[\[([^\]|]+)\]\]`)
|
|
|
|
// CanonicalizeLinks converts [[Display Name]] wikilinks to [[slug|Display Name]]
|
|
// using a title→slug map built from the inventory and current batch.
|
|
// Unknown titles are left as-is and returned as warnings.
|
|
func CanonicalizeLinks(pages []wiki.Page, inventory map[wiki.PageType][]wiki.Entry) ([]wiki.Page, []string) {
|
|
titleToSlug := buildTitleMap(pages, inventory)
|
|
|
|
var allWarnings []string
|
|
out := make([]wiki.Page, len(pages))
|
|
for i, p := range pages {
|
|
newContent, warnings := canonicalizeContent(p.Content, titleToSlug)
|
|
p.Content = newContent
|
|
out[i] = p
|
|
allWarnings = append(allWarnings, warnings...)
|
|
}
|
|
return out, allWarnings
|
|
}
|
|
|
|
// buildTitleMap builds a lowercase-title → slug map from inventory and current batch.
|
|
// Current batch entries take precedence over inventory (they may be updates).
|
|
func buildTitleMap(pages []wiki.Page, inventory map[wiki.PageType][]wiki.Entry) map[string]string {
|
|
m := make(map[string]string)
|
|
for _, entries := range inventory {
|
|
for _, e := range entries {
|
|
m[strings.ToLower(e.Title)] = e.Slug
|
|
}
|
|
}
|
|
// Current batch overrides inventory
|
|
for _, p := range pages {
|
|
title := extractTitle(p.Content)
|
|
slug := strings.TrimSuffix(filepath.Base(p.Path), ".md")
|
|
if title != "" && slug != "" {
|
|
m[strings.ToLower(title)] = slug
|
|
}
|
|
}
|
|
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 {
|
|
sub := plainLinkRE.FindStringSubmatch(match)
|
|
if len(sub) < 2 {
|
|
return match
|
|
}
|
|
displayName := sub[1]
|
|
|
|
if slug, ok := titleToSlug[strings.ToLower(displayName)]; ok {
|
|
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
|
|
}
|