package brain import ( "fmt" "os" "path/filepath" "regexp" "sort" "strings" ) // wikilinkRE matches [[target]] and [[target|Display Text]]. var wikilinkRE = regexp.MustCompile(`\[\[([^\]|]+)(?:\|([^\]]*))?\]\]`) // fencedCodeRE and inlineCodeRE mark the regions where a [[...]] is a // syntax example rather than a link. Same exclusion brain-gardener's audit // applies on the read side, so the write gate and the audit agree on what // counts as a link at all. var ( fencedCodeRE = regexp.MustCompile("(?s)```.*?```") inlineCodeRE = regexp.MustCompile("`[^`\n]*`") ) // StripUnresolvedLinks unwraps every [[wikilink]] in content whose target // resolves to no entry under knowledge/ or wiki/, leaving the link's display // text (or its target) as plain prose. It returns the rewritten content and // the sorted, de-duplicated targets it unwrapped. // // WHY UNWRAP RATHER THAN REJECT THE WRITE // Agents write [[...]] around anything that feels related — a skill name, a // repo, an agent-local memory file — and none of those are brain entries. // Every one of those becomes a permanent [high] broken-link finding that no // deterministic Fixer can repair, because there is no target to repoint to: // the brain-audit digest grew from 19 to 63 high findings between 2026-08-26 // and 2026-09-16, and 53 of the 63 were exactly this. Rejecting the write // instead would push the failure onto claudewatcher's unattended ingest path, // where a refused write is a lost learning. Unwrapping keeps the author's // words, keeps the write, and keeps the graph honest. // // An empty corpus (no knowledge/ or wiki/ yet, or a misconfigured brainDir) // returns content unchanged: nothing resolving is a bootstrap state, not // evidence that every target was invented. func StripUnresolvedLinks(brainDir, content string) (string, []string, error) { index, err := entryIndex(brainDir) if err != nil { return "", nil, err } if len(index) == 0 { return content, nil, nil } code := codeSpans(content) stripped := map[string]struct{}{} var b strings.Builder last := 0 for _, m := range wikilinkRE.FindAllStringSubmatchIndex(content, -1) { start, end := m[0], m[1] if overlaps(start, end, code) { continue } target := strings.TrimSpace(content[m[2]:m[3]]) if _, ok := index[linkKey(target)]; ok { continue } display := target if m[4] >= 0 { if d := strings.TrimSpace(content[m[4]:m[5]]); d != "" { display = d } } b.WriteString(content[last:start]) b.WriteString(display) last = end stripped[target] = struct{}{} } if last == 0 { return content, nil, nil } b.WriteString(content[last:]) targets := make([]string, 0, len(stripped)) for t := range stripped { targets = append(targets, t) } sort.Strings(targets) return b.String(), targets, nil } // linkedTrees are the two trees that hold linkable entries — the same pair // package notes admits under ScopeNotes. Duplicated rather than imported // because that list is unexported there and this is a read-only mirror. var linkedTrees = []string{"knowledge", "wiki"} // entryIndex maps every form a wikilink may name an entry by — its bare slug // and its path form (wiki///.md -> wing/hall/slug) — to a // present marker. Mirrors brain-gardener's resolver so an accepted link is // one the audit will also resolve. func entryIndex(brainDir string) (map[string]struct{}, error) { index := map[string]struct{}{} for _, dir := range linkedTrees { root := filepath.Join(brainDir, dir) if _, err := os.Stat(root); os.IsNotExist(err) { continue } else if err != nil { return nil, fmt.Errorf("stat %s: %w", dir, err) } err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(path, ".md") { return nil } rel, err := filepath.Rel(brainDir, path) if err != nil { return err } rel = filepath.ToSlash(rel) index[strings.TrimSuffix(filepath.Base(rel), ".md")] = struct{}{} index[strings.TrimPrefix(strings.TrimSuffix(rel, ".md"), "wiki/")] = struct{}{} return nil }) if err != nil { return nil, fmt.Errorf("walk %s: %w", dir, err) } } return index, nil } // linkKey normalises a link target for index lookup: an exact path form is // tried as written, and anything else falls back to its final segment. func linkKey(target string) string { return strings.TrimSuffix(target, ".md") } // codeSpans returns the byte ranges of fenced blocks and inline code spans. func codeSpans(content string) [][2]int { var spans [][2]int for _, m := range fencedCodeRE.FindAllStringIndex(content, -1) { spans = append(spans, [2]int{m[0], m[1]}) } for _, m := range inlineCodeRE.FindAllStringIndex(content, -1) { spans = append(spans, [2]int{m[0], m[1]}) } return spans } // overlaps reports whether [start,end) intersects any span. func overlaps(start, end int, spans [][2]int) bool { for _, s := range spans { if start < s[1] && end > s[0] { return true } } return false }