Sources -> wiki/<wing>/sources, concepts/entities -> wiki/<wing>/facts with kind: frontmatter; inventory, title resolution and back-references are confined to the target wing. Empty Wing keeps the retired layout unchanged. Mutation-checked: widening the inventory beyond the wing fails the cross-wing back-reference test; a non-plain wing name is rejected. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
139 lines
3.8 KiB
Go
139 lines
3.8 KiB
Go
// ingestion/internal/pipeline/refs.go
|
|
package pipeline
|
|
|
|
import (
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/wiki"
|
|
)
|
|
|
|
var wikilinkRE = regexp.MustCompile(`\[\[([^|\]]+)\|`)
|
|
|
|
// injectSourceRefs finds the source page in the proposed batch, extracts its
|
|
// wikilinks, and injects a back-reference into every linked concept or entity page.
|
|
// Pages that exist on disk but are not in the current batch are loaded and
|
|
// appended so they will be updated on write.
|
|
func injectSourceRefs(pages []wiki.Page, inventory map[wiki.PageType][]wiki.Entry, brainDir string) []wiki.Page {
|
|
sourceSlug, sourceTitle, found := findSourcePage(pages)
|
|
if !found {
|
|
return pages
|
|
}
|
|
|
|
var sourceContent string
|
|
for _, p := range pages {
|
|
if isSourcePath(p.Path) &&
|
|
strings.TrimSuffix(filepath.Base(p.Path), ".md") == sourceSlug {
|
|
sourceContent = p.Content
|
|
break
|
|
}
|
|
}
|
|
|
|
linkedSlugs := extractWikilinks(sourceContent)
|
|
sourceRef := "- [[" + sourceSlug + "|" + sourceTitle + "]]"
|
|
|
|
bySlug := make(map[string]int, len(pages))
|
|
for i, p := range pages {
|
|
if !isSourcePath(p.Path) {
|
|
bySlug[strings.TrimSuffix(filepath.Base(p.Path), ".md")] = i
|
|
}
|
|
}
|
|
|
|
for slug := range linkedSlugs {
|
|
if slug == sourceSlug {
|
|
continue
|
|
}
|
|
if idx, ok := bySlug[slug]; ok {
|
|
pages[idx] = addSourceRef(pages[idx], sourceRef)
|
|
continue
|
|
}
|
|
pt, ok := findInInventory(slug, inventory)
|
|
if !ok {
|
|
continue
|
|
}
|
|
path := entryPath(slug, pt, inventory)
|
|
b, err := readBrainFile(brainDir, path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
page := wiki.Page{
|
|
Path: path,
|
|
Content: string(b),
|
|
}
|
|
pages = append(pages, addSourceRef(page, sourceRef))
|
|
}
|
|
|
|
return pages
|
|
}
|
|
|
|
// addSourceRef injects sourceRef into the ## Sources bullet section of page
|
|
// using wiki.Merge, which deduplicates bullets automatically.
|
|
func addSourceRef(page wiki.Page, sourceRef string) wiki.Page {
|
|
patch := wiki.Page{
|
|
Path: page.Path,
|
|
Content: "\n## Sources\n\n" + sourceRef + "\n",
|
|
}
|
|
return wiki.Merge(page, patch)
|
|
}
|
|
|
|
// extractWikilinks returns the set of slugs referenced as [[slug|...]] in content.
|
|
func extractWikilinks(content string) map[string]bool {
|
|
slugs := make(map[string]bool)
|
|
for _, m := range wikilinkRE.FindAllStringSubmatch(content, -1) {
|
|
slugs[m[1]] = true
|
|
}
|
|
return slugs
|
|
}
|
|
|
|
// findSourcePage returns the slug and title of the first wiki/sources/ page in pages.
|
|
func findSourcePage(pages []wiki.Page) (slug, title string, found bool) {
|
|
for _, p := range pages {
|
|
if isSourcePath(p.Path) {
|
|
slug = strings.TrimSuffix(filepath.Base(p.Path), ".md")
|
|
title = extractTitle(p.Content)
|
|
if title == "" {
|
|
title = slug
|
|
}
|
|
return slug, title, true
|
|
}
|
|
}
|
|
return "", "", false
|
|
}
|
|
|
|
// findInInventory returns the PageType for a slug if it appears in the inventory.
|
|
func findInInventory(slug string, inventory map[wiki.PageType][]wiki.Entry) (wiki.PageType, bool) {
|
|
for pt, entries := range inventory {
|
|
for _, e := range entries {
|
|
if e.Slug == slug {
|
|
return pt, true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// isSourcePath reports whether path is a source page in either layout:
|
|
// wiki/sources/<slug>.md or wiki/<wing>/sources/<slug>.md.
|
|
func isSourcePath(path string) bool {
|
|
parts := strings.Split(filepath.ToSlash(path), "/")
|
|
switch {
|
|
case len(parts) == 3 && parts[0] == "wiki" && parts[1] == "sources":
|
|
return true
|
|
case len(parts) == 4 && parts[0] == "wiki" && parts[2] == "sources":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// entryPath is the brain-relative path of an inventory page: the recorded Path
|
|
// for a wing-layout entry, else the retired wiki/<type>/<slug>.md.
|
|
func entryPath(slug string, pt wiki.PageType, inventory map[wiki.PageType][]wiki.Entry) string {
|
|
for _, e := range inventory[pt] {
|
|
if e.Slug == slug && e.Path != "" {
|
|
return e.Path
|
|
}
|
|
}
|
|
return "wiki/" + string(pt) + "/" + slug + ".md"
|
|
}
|