With Config.Wing a source goes to wiki/<wing>/sources/ (wing/hall frontmatter); concepts and entities stay in the shared wiki/concepts and wiki/entities, so an entity cited from two wings is ONE page with both sources (the per-wing facts/ layout duplicated it). A source slug already used anywhere but its own wing's sources/ is refused (slug is the graph identity). wiki.FindSlug added. Mutation-checked: collision guard, shared inventory, per-wing source path. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
194 lines
5.1 KiB
Go
194 lines
5.1 KiB
Go
// ingestion/internal/wiki/inventory.go
|
|
package wiki
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"strings"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
|
|
)
|
|
|
|
// LoadInventory walks brain/wiki/ and returns all pages grouped by type.
|
|
// Missing subdirectories are silently skipped. Pages are listed and read
|
|
// through the brain root, regular files only (hyperguild#157); a type
|
|
// directory that is a symlink is an error.
|
|
func LoadInventory(brainDir string) (map[PageType][]Entry, error) {
|
|
result := map[PageType][]Entry{
|
|
PageTypeConcept: {},
|
|
PageTypeEntity: {},
|
|
PageTypeSource: {},
|
|
}
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open brain: %w", err)
|
|
}
|
|
defer func() { _ = bfs.Close() }()
|
|
for pt := range result {
|
|
dir := "wiki/" + string(pt)
|
|
names, err := bfs.ReadDirRegular(dir)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read dir %s: %w", dir, err)
|
|
}
|
|
for _, name := range names {
|
|
if !strings.HasSuffix(name, ".md") {
|
|
continue
|
|
}
|
|
slug := strings.TrimSuffix(name, ".md")
|
|
title, aliases := readFrontmatter(bfs, dir+"/"+name, slug)
|
|
result[pt] = append(result[pt], Entry{Slug: slug, Title: title, Aliases: aliases, Type: pt})
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// readFrontmatter extracts title and aliases from YAML frontmatter.
|
|
// Falls back to slug for title and empty aliases on any error.
|
|
func readFrontmatter(bfs *brainfs.Brain, rel, fallbackSlug string) (title string, aliases []string) {
|
|
title = fallbackSlug
|
|
content, err := bfs.ReadRegular(rel)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
scanner := bufio.NewScanner(bytes.NewReader(content))
|
|
inFM := false
|
|
inAliases := false
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.TrimSpace(line) == "---" {
|
|
if !inFM {
|
|
inFM = true
|
|
continue
|
|
}
|
|
break // end of frontmatter
|
|
}
|
|
if !inFM {
|
|
continue
|
|
}
|
|
|
|
// Detect alias list items (lines starting with " - ").
|
|
if inAliases {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "- ") {
|
|
aliases = append(aliases, strings.TrimPrefix(trimmed, "- "))
|
|
continue
|
|
}
|
|
inAliases = false // end of alias block
|
|
}
|
|
|
|
key, val, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch strings.TrimSpace(key) {
|
|
case "title":
|
|
title = strings.Trim(strings.TrimSpace(val), `"'`)
|
|
case "aliases":
|
|
inAliases = true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// LoadWingInventory returns what an ingest into one wing can match against:
|
|
// that wing's sources (wiki/<wing>/sources/) and the SHARED concepts and
|
|
// entities (wiki/concepts/, wiki/entities/), which belong to no single wing
|
|
// because their slug is the graph's global identity. Other wings' sources are
|
|
// not read.
|
|
func LoadWingInventory(brainDir, wing string) (map[PageType][]Entry, error) {
|
|
result, err := LoadInventory(brainDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[PageTypeSource] = []Entry{} // the retired source layout is not this wing's
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open brain: %w", err)
|
|
}
|
|
defer func() { _ = bfs.Close() }()
|
|
dir := "wiki/" + wing + "/sources"
|
|
names, err := bfs.ReadDirRegular(dir)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return result, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read dir %s: %w", dir, err)
|
|
}
|
|
for _, name := range names {
|
|
if !strings.HasSuffix(name, ".md") {
|
|
continue
|
|
}
|
|
rel := dir + "/" + name
|
|
slug := strings.TrimSuffix(name, ".md")
|
|
title, aliases := readFrontmatter(bfs, rel, slug)
|
|
result[PageTypeSource] = append(result[PageTypeSource], Entry{Slug: slug, Title: title, Aliases: aliases, Type: PageTypeSource, Path: rel})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// FindSlug returns the brain-relative path of an existing page whose basename is
|
|
// slug, searching the retired dirs (wiki/{sources,concepts,entities}/) and every
|
|
// wing hall (wiki/<wing>/<hall>/). The slug is a page's graph identity, so two
|
|
// pages must not share one.
|
|
func FindSlug(brainDir, slug string) (string, bool, error) {
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return "", false, fmt.Errorf("open brain: %w", err)
|
|
}
|
|
defer func() { _ = bfs.Close() }()
|
|
file := slug + ".md"
|
|
has := func(dir string) (bool, error) {
|
|
names, err := bfs.ReadDirRegular(dir)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, n := range names {
|
|
if n == file {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
for _, d := range []string{"wiki/sources", "wiki/concepts", "wiki/entities"} {
|
|
if ok, err := has(d); err != nil {
|
|
return "", false, err
|
|
} else if ok {
|
|
return d + "/" + file, true, nil
|
|
}
|
|
}
|
|
wings, err := bfs.ReadDirDirs("wiki")
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return "", false, nil
|
|
}
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
for _, w := range wings {
|
|
if w == "sources" || w == "concepts" || w == "entities" {
|
|
continue
|
|
}
|
|
halls, err := bfs.ReadDirDirs("wiki/" + w)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, h := range halls {
|
|
if ok, err := has("wiki/" + w + "/" + h); err != nil {
|
|
return "", false, err
|
|
} else if ok {
|
|
return "wiki/" + w + "/" + h + "/" + file, true, nil
|
|
}
|
|
}
|
|
}
|
|
return "", false, nil
|
|
}
|