Writers that created or rewrote brain files by path followed symlinks: - promote/reject: os.MkdirAll + os.WriteFile of the destination, then os.Remove of the raw source. Now PutRegular + brainfs.Remove; the source is kept when the write is refused. - StampProvenanceFile (brain_edit's re-stamp): provenance.ApplyFile read and rewrote by path. Now ReadRegular + WriteRegular; ApplyFile, now unused, is removed. - BuildWingIndex (_index.md) and wiki.RebuildIndex (wiki/index.md): now PutRegular. - wiki.AppendLog (log.md) and AutoTunnel's fuzzy-candidate log (raw/tunnel-candidates-*.md): now AppendRegular; tunnel's fileExists, now unused, is removed. - New brainfs.Remove: clean path, real parent, removes a final symlink itself, never its target. brain_edit's own atomic rename was already refused behind a symlinked directory by #187's read check; a guard test pins it. Pipeline writes, BackfillRefs, the watcher's raw/ markers and the session archive in cmd/server follow in a separate PR (#158). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
76 lines
2.1 KiB
Go
76 lines
2.1 KiB
Go
// ingestion/internal/wiki/index.go
|
|
package wiki
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/textutil"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return fmt.Errorf("open brain: %w", err)
|
|
}
|
|
defer func() { _ = bfs.Close() }()
|
|
|
|
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(bfs, 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")
|
|
}
|
|
|
|
// Written through the brain root, never through a symlink (#158).
|
|
return bfs.PutRegular("wiki/index.md", []byte(sb.String()))
|
|
}
|
|
|
|
// pageFirstSentence reads the page through the brain root, regular files
|
|
// only (hyperguild#157).
|
|
func pageFirstSentence(bfs *brainfs.Brain, e Entry) string {
|
|
content, err := bfs.ReadRegular("wiki/" + string(e.Type) + "/" + e.Slug + ".md")
|
|
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
|
|
}
|
|
return textutil.Truncate(line, 100)
|
|
}
|
|
return ""
|
|
}
|