From 3a0424a6b488fcb3cd84766b8a19199a87a9927e Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Wed, 22 Apr 2026 22:28:53 +0200 Subject: [PATCH] feat(ingestion): add wiki inventory loader --- ingestion/internal/wiki/inventory.go | 68 +++++++++++++++++++++++ ingestion/internal/wiki/inventory_test.go | 62 +++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 ingestion/internal/wiki/inventory.go create mode 100644 ingestion/internal/wiki/inventory_test.go diff --git a/ingestion/internal/wiki/inventory.go b/ingestion/internal/wiki/inventory.go new file mode 100644 index 0000000..42398d6 --- /dev/null +++ b/ingestion/internal/wiki/inventory.go @@ -0,0 +1,68 @@ +// ingestion/internal/wiki/inventory.go +package wiki + +import ( + "bufio" + "os" + "path/filepath" + "strings" +) + +// LoadInventory walks brain/wiki/ and returns all pages grouped by type. +// Missing subdirectories are silently skipped. +func LoadInventory(brainDir string) (map[PageType][]Entry, error) { + result := map[PageType][]Entry{ + PageTypeConcept: {}, + PageTypeEntity: {}, + PageTypeSource: {}, + } + for pt := range result { + dir := filepath.Join(brainDir, "wiki", string(pt)) + entries, err := os.ReadDir(dir) + if os.IsNotExist(err) { + continue + } + if err != nil { + return nil, err + } + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") { + continue + } + slug := strings.TrimSuffix(e.Name(), ".md") + path := filepath.Join(dir, e.Name()) + title := readTitle(path, slug) + result[pt] = append(result[pt], Entry{Slug: slug, Title: title, Type: pt}) + } + } + return result, nil +} + +// readTitle extracts the title from YAML frontmatter, falling back to slug. +func readTitle(path, fallback string) string { + f, err := os.Open(path) + if err != nil { + return fallback + } + defer f.Close() + + scanner := bufio.NewScanner(f) + inFM := false + for scanner.Scan() { + line := scanner.Text() + if strings.TrimSpace(line) == "---" { + if !inFM { + inFM = true + continue + } + break + } + if inFM { + key, val, ok := strings.Cut(line, ":") + if ok && strings.TrimSpace(key) == "title" { + return strings.Trim(strings.TrimSpace(val), `"'`) + } + } + } + return fallback +} diff --git a/ingestion/internal/wiki/inventory_test.go b/ingestion/internal/wiki/inventory_test.go new file mode 100644 index 0000000..7a485dc --- /dev/null +++ b/ingestion/internal/wiki/inventory_test.go @@ -0,0 +1,62 @@ +// ingestion/internal/wiki/inventory_test.go +package wiki + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadInventory(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "concepts"), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "entities"), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "sources"), 0o755)) + + require.NoError(t, os.WriteFile( + filepath.Join(dir, "wiki", "concepts", "domain-driven-design.md"), + []byte("---\ntitle: Domain Driven Design\n---\n\n## Definition\n\nA thing.\n"), + 0o644, + )) + require.NoError(t, os.WriteFile( + filepath.Join(dir, "wiki", "entities", "ryan-singer.md"), + []byte("---\ntitle: Ryan Singer\n---\n\n## Description\n\nDesigner.\n"), + 0o644, + )) + + inv, err := LoadInventory(dir) + require.NoError(t, err) + + assert.Len(t, inv[PageTypeConcept], 1) + assert.Equal(t, "domain-driven-design", inv[PageTypeConcept][0].Slug) + assert.Equal(t, "Domain Driven Design", inv[PageTypeConcept][0].Title) + + assert.Len(t, inv[PageTypeEntity], 1) + assert.Equal(t, "ryan-singer", inv[PageTypeEntity][0].Slug) + + assert.Empty(t, inv[PageTypeSource]) +} + +func TestLoadInventory_EmptyDirs(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "concepts"), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "entities"), 0o755)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "sources"), 0o755)) + + inv, err := LoadInventory(dir) + require.NoError(t, err) + assert.Empty(t, inv[PageTypeConcept]) + assert.Empty(t, inv[PageTypeEntity]) + assert.Empty(t, inv[PageTypeSource]) +} + +func TestLoadInventory_MissingDirsOk(t *testing.T) { + dir := t.TempDir() + // No wiki/ subdirs at all + inv, err := LoadInventory(dir) + require.NoError(t, err) + assert.NotNil(t, inv) +}