Review round 1 on PR #186: - The swap test's static note had the same title as the raced note, so the harness's "inside was read" floor passed without reading r.md (skipping r.md stayed green). The static note now has its own title. - An unreadable directory under wiki/ reaches the walk-error path, and swallowing that error stayed green. New test asserts the error. - The FIFO test now runs with and without a writer held open. - _index.md skip had no test; new test. - index.go: the double blank line left by removing readTitleAndCreated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
4.3 KiB
Go
175 lines
4.3 KiB
Go
package brain
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
|
|
)
|
|
|
|
// noteEntry is one row in a Wing _index.md.
|
|
type noteEntry struct {
|
|
Hall string
|
|
Slug string
|
|
Title string
|
|
Created string
|
|
}
|
|
|
|
// BuildWingIndex regenerates brain/wiki/<wing>/_index.md as a Map of
|
|
// Content listing every note in that wing with its Hall and creation
|
|
// date. Returns nil if the wing directory does not exist.
|
|
func BuildWingIndex(brainDir, wing string) error {
|
|
w := Sanitise(wing)
|
|
if w == "" {
|
|
return fmt.Errorf("invalid wing %q", wing)
|
|
}
|
|
wingDir := filepath.Join(brainDir, "wiki", w)
|
|
// Listed and read through the brain root (hyperguild#157): a wing or
|
|
// hall that is a symlink is not followed, and notes are regular files.
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return fmt.Errorf("open brain: %w", err)
|
|
}
|
|
defer func() { _ = bfs.Close() }()
|
|
if _, err := bfs.Lstat("wiki/" + w); errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
|
|
entries, err := collectWingEntries(bfs, "wiki/"+w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
if entries[i].Hall != entries[j].Hall {
|
|
return entries[i].Hall < entries[j].Hall
|
|
}
|
|
return entries[i].Slug < entries[j].Slug
|
|
})
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# %s\n\n", w)
|
|
b.WriteString("| Hall | Note | Created |\n")
|
|
b.WriteString("|------|------|---------|\n")
|
|
for _, e := range entries {
|
|
fmt.Fprintf(&b, "| %s | [%s](%s/%s.md) | %s |\n", e.Hall, e.Title, e.Hall, e.Slug, e.Created)
|
|
}
|
|
|
|
dest := filepath.Join(wingDir, "_index.md")
|
|
return os.WriteFile(dest, []byte(b.String()), 0o644)
|
|
}
|
|
|
|
// BuildAllWingIndexes regenerates _index.md for every wing under brain/wiki/.
|
|
func BuildAllWingIndexes(brainDir string) error {
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return fmt.Errorf("open brain: %w", err)
|
|
}
|
|
wings, err := bfs.ReadDirDirs("wiki")
|
|
_ = bfs.Close()
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("read wiki: %w", err)
|
|
}
|
|
for _, w := range wings {
|
|
if err := BuildWingIndex(brainDir, w); err != nil {
|
|
return fmt.Errorf("index %s: %w", w, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func collectWingEntries(bfs *brainfs.Brain, wingRel string) ([]noteEntry, error) {
|
|
var out []noteEntry
|
|
halls, err := bfs.ReadDirDirs(wingRel)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read wing: %w", err)
|
|
}
|
|
for _, hall := range halls {
|
|
if !IsValidHall(hall) {
|
|
continue
|
|
}
|
|
hallRel := wingRel + "/" + hall
|
|
names, err := bfs.ReadDirRegular(hallRel)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read hall %s: %w", hall, err)
|
|
}
|
|
for _, name := range names {
|
|
if !strings.HasSuffix(name, ".md") || name == "_index.md" {
|
|
continue
|
|
}
|
|
slug := strings.TrimSuffix(name, ".md")
|
|
title, created := readTitleAndCreatedIn(bfs, hallRel+"/"+name, slug)
|
|
out = append(out, noteEntry{Hall: hall, Slug: slug, Title: title, Created: created})
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// readTitleAndCreatedIn is readTitleAndCreated through the brain root,
|
|
// regular files only (hyperguild#157).
|
|
func readTitleAndCreatedIn(bfs *brainfs.Brain, rel, slug string) (string, string) {
|
|
content, err := bfs.ReadRegular(rel)
|
|
if err != nil {
|
|
return slug, ""
|
|
}
|
|
title, created := parseTitleAndCreated(bytes.NewReader(content))
|
|
if created == "" {
|
|
if info, err := bfs.Lstat(rel); err == nil {
|
|
created = info.ModTime().UTC().Format("2006-01-02")
|
|
}
|
|
}
|
|
return titleOrSlug(title, slug), created
|
|
}
|
|
|
|
func titleOrSlug(title, slug string) string {
|
|
if title == "" {
|
|
return strings.ReplaceAll(slug, "-", " ")
|
|
}
|
|
return title
|
|
}
|
|
|
|
func parseTitleAndCreated(r io.Reader) (title, created string) {
|
|
scanner := bufio.NewScanner(r)
|
|
inFrontmatter := false
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
if strings.TrimSpace(line) == "---" {
|
|
if !inFrontmatter {
|
|
inFrontmatter = true
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
if !inFrontmatter {
|
|
continue
|
|
}
|
|
key, val, ok := strings.Cut(line, ":")
|
|
if !ok {
|
|
continue
|
|
}
|
|
v := strings.Trim(strings.TrimSpace(val), `"'`)
|
|
switch strings.TrimSpace(key) {
|
|
case "title":
|
|
title = v
|
|
case "created_at":
|
|
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
|
created = t.UTC().Format("2006-01-02")
|
|
} else {
|
|
created = v
|
|
}
|
|
}
|
|
}
|
|
return title, created
|
|
}
|