// 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 the pages of ONE wing, grouped by type. Sources // come from wiki//sources/; concepts and entities live together in // wiki//facts/ and are told apart by their `kind:` frontmatter. A facts // page without a kind (a hand-written note) is left out, so extraction can // never merge into something it did not create. Other wings are never read. func LoadWingInventory(brainDir, wing 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 _, hall := range []string{"sources", "facts"} { dir := "wiki/" + wing + "/" + hall 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 } rel := dir + "/" + name slug := strings.TrimSuffix(name, ".md") pt := PageTypeSource if hall == "facts" { var ok bool if pt, ok = kindOf(bfs, rel); !ok { continue } } title, aliases := readFrontmatter(bfs, rel, slug) result[pt] = append(result[pt], Entry{Slug: slug, Title: title, Aliases: aliases, Type: pt, Path: rel}) } } return result, nil } // kindOf reads `kind:` from a facts page's frontmatter. func kindOf(bfs *brainfs.Brain, rel string) (PageType, bool) { content, err := bfs.ReadRegular(rel) if err != nil { return "", false } return KindFromContent(string(content)) } // KindFromContent maps frontmatter `kind: concept|entity` to a PageType. func KindFromContent(content string) (PageType, bool) { inFM := false for _, line := range strings.Split(content, "\n") { if strings.TrimSpace(line) == "---" { if inFM { break } inFM = true continue } if !inFM { continue } if key, val, ok := strings.Cut(line, ":"); ok && strings.TrimSpace(key) == "kind" { switch strings.Trim(strings.TrimSpace(val), `"'`) { case "concept": return PageTypeConcept, true case "entity": return PageTypeEntity, true } } } return "", false }