From d405346f07a110a5ef61d01b6a51e1c85dc692b7 Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Wed, 22 Apr 2026 22:36:55 +0200 Subject: [PATCH] feat(ingestion): add wiki index rebuilder and audit log --- ingestion/internal/wiki/index.go | 71 +++++++++++++++++++++++++ ingestion/internal/wiki/index_test.go | 76 +++++++++++++++++++++++++++ ingestion/internal/wiki/log.go | 38 ++++++++++++++ 3 files changed, 185 insertions(+) create mode 100644 ingestion/internal/wiki/index.go create mode 100644 ingestion/internal/wiki/index_test.go create mode 100644 ingestion/internal/wiki/log.go diff --git a/ingestion/internal/wiki/index.go b/ingestion/internal/wiki/index.go new file mode 100644 index 0000000..b80841a --- /dev/null +++ b/ingestion/internal/wiki/index.go @@ -0,0 +1,71 @@ +// ingestion/internal/wiki/index.go +package wiki + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// RebuildIndex writes brain/wiki/index.md from the current wiki contents. +func RebuildIndex(brainDir, date string) error { + inv, err := LoadInventory(brainDir) + if err != nil { + return fmt.Errorf("load inventory: %w", err) + } + + total := len(inv[PageTypeConcept]) + len(inv[PageTypeEntity]) + len(inv[PageTypeSource]) + var sb strings.Builder + fmt.Fprintf(&sb, "# Wiki Index\n\n") + fmt.Fprintf(&sb, "_Updated: %s — %d pages (%d concepts, %d entities, %d sources)_\n\n", + date, total, + len(inv[PageTypeConcept]), + len(inv[PageTypeEntity]), + len(inv[PageTypeSource])) + + for _, pt := range []PageType{PageTypeConcept, PageTypeEntity, PageTypeSource} { + entries := inv[pt] + if len(entries) == 0 { + continue + } + label := strings.ToUpper(string(pt)[:1]) + string(pt)[1:] + fmt.Fprintf(&sb, "## %s\n\n", label) + for _, e := range entries { + summary := pageFirstSentence(brainDir, e) + if summary != "" { + fmt.Fprintf(&sb, "- [[%s|%s]] — %s\n", e.Slug, e.Title, summary) + } else { + fmt.Fprintf(&sb, "- [[%s|%s]]\n", e.Slug, e.Title) + } + } + sb.WriteString("\n") + } + + dest := filepath.Join(brainDir, "wiki", "index.md") + return os.WriteFile(dest, []byte(sb.String()), 0o644) +} + +func pageFirstSentence(brainDir string, e Entry) string { + path := filepath.Join(brainDir, "wiki", string(e.Type), e.Slug+".md") + content, err := os.ReadFile(path) + if err != nil { + return "" + } + parts := strings.SplitN(string(content), "---", 3) + body := string(content) + if len(parts) == 3 { + body = parts[2] + } + for _, line := range strings.Split(body, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if len(line) > 100 { + return line[:100] + "…" + } + return line + } + return "" +} diff --git a/ingestion/internal/wiki/index_test.go b/ingestion/internal/wiki/index_test.go new file mode 100644 index 0000000..2f58c5f --- /dev/null +++ b/ingestion/internal/wiki/index_test.go @@ -0,0 +1,76 @@ +// ingestion/internal/wiki/index_test.go +package wiki + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupWikiDir(t *testing.T) string { + t.Helper() + 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", "tdd.md"), + []byte("---\ntitle: TDD\n---\n\n## Definition\n\nTest-driven development is a discipline.\n"), + 0o644, + )) + return dir +} + +func TestRebuildIndex(t *testing.T) { + dir := setupWikiDir(t) + require.NoError(t, RebuildIndex(dir, "2026-04-22")) + + content, err := os.ReadFile(filepath.Join(dir, "wiki", "index.md")) + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "# Wiki Index") + assert.Contains(t, s, "2026-04-22") + assert.Contains(t, s, "[[tdd|TDD]]") + assert.Contains(t, s, "## Concepts") +} + +func TestRebuildIndex_EmptyWiki(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, RebuildIndex(dir, "2026-04-22")) + content, err := os.ReadFile(filepath.Join(dir, "wiki", "index.md")) + require.NoError(t, err) + assert.Contains(t, string(content), "# Wiki Index") +} + +func TestAppendLog(t *testing.T) { + dir := t.TempDir() + require.NoError(t, AppendLog(dir, "shape-up-book", + []string{"wiki/sources/shape-up.md", "wiki/concepts/betting-table.md"}, + nil, "2026-04-22")) + + content, err := os.ReadFile(filepath.Join(dir, "log.md")) + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "shape-up-book") + assert.Contains(t, s, "wiki/sources/shape-up.md") + assert.True(t, strings.HasPrefix(s, "## 2026-04-22")) +} + +func TestAppendLog_AppendsOnSecondCall(t *testing.T) { + dir := t.TempDir() + require.NoError(t, AppendLog(dir, "source-a", []string{"wiki/sources/a.md"}, nil, "2026-04-22")) + require.NoError(t, AppendLog(dir, "source-b", []string{"wiki/sources/b.md"}, nil, "2026-04-22")) + + content, err := os.ReadFile(filepath.Join(dir, "log.md")) + require.NoError(t, err) + assert.Contains(t, string(content), "source-a") + assert.Contains(t, string(content), "source-b") +} diff --git a/ingestion/internal/wiki/log.go b/ingestion/internal/wiki/log.go new file mode 100644 index 0000000..283d3b8 --- /dev/null +++ b/ingestion/internal/wiki/log.go @@ -0,0 +1,38 @@ +// ingestion/internal/wiki/log.go +package wiki + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// AppendLog appends one ingestion record to brain/log.md. +func AppendLog(brainDir, source string, pages, warnings []string, date string) error { + var sb strings.Builder + fmt.Fprintf(&sb, "## %s — ingest\n\n", date) + fmt.Fprintf(&sb, "- **Source:** %s\n", source) + if len(pages) > 0 { + sb.WriteString("- **Pages written:**\n") + for _, p := range pages { + fmt.Fprintf(&sb, " - %s\n", p) + } + } + if len(warnings) > 0 { + sb.WriteString("- **Warnings:**\n") + for _, w := range warnings { + fmt.Fprintf(&sb, " - %s\n", w) + } + } + sb.WriteString("\n") + + logPath := filepath.Join(brainDir, "log.md") + f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return fmt.Errorf("open log: %w", err) + } + defer f.Close() + _, err = f.WriteString(sb.String()) + return err +}