coverage~bigbes/sr-ht-spec64cae3afsearch/extract.go

Coverage
100.0% 31/31 statements
Δ
+0.0
Blob
f55973f
Uncovered nothing — every instrumented line ran
1 package search
2
3 import (
4 "fmt"
5 "strings"
6 "sync"
7
8 "sourcecraft.dev/bigbes/sr-ht-spec/doc"
9 "sourcecraft.dev/bigbes/sr-ht-spec/gitx"
10 )
11
12 // renderer is shared: doc.Renderer is documented as reusable and
13 // concurrency-safe, and building one per extraction would rebuild the whole
14 // goldmark pipeline for every space.
15 var renderer = sync.OnceValue(doc.NewRenderer)
16
17 // Bodies keys a revision's documents by tree path, which is the shape Extract
18 // wants them in. It pairs with doc.FromDocuments: the same []gitx.Document
19 // builds the Archive and supplies the text.
20 51 func Bodies(docs []gitx.Document) map[string][]byte {
21 51 m := make(map[string][]byte, len(docs))
22 1161 for _, d := range docs {
23 1161 m[d.Path] = d.Data
24 1161 }
25 51 return m
26 }
27
28 // Extract projects one space at one revision into the documents the index
29 // stores. bodies holds each page's raw markdown — frontmatter included, exactly
30 // gitx.Document.Data — keyed by doc.Page.Path.
31 //
32 // A page in the archive with no body in bodies is an error, not a page indexed
33 // with an empty body. The two are indistinguishable once indexed, and the
34 // second is how a document silently stops being findable.
35 //
36 // Three document shapes come out, matching what doc/ models:
37 //
38 // - an ordinary document, indexed as its frontmatter projected to "key:
39 // value" lines followed by its rendered plain text. The frontmatter is in
40 // there because tags, owners and summaries render as chips rather than
41 // prose, and a search for one of them should still find the document.
42 // - a catalog (`type: catalog`, or index.md), indexed by title and section
43 // only. A catalog is a page of one-line descriptions of other documents;
44 // indexed whole, a query lands on the description instead of on the
45 // document that owns it.
46 // - an activity log (`type: log`, or log.md), which contributes its own
47 // title-only document plus one document per dated entry. A hit anywhere in
48 // a log otherwise resolves to the whole log; split, each entry is the size
49 // of the thing it describes and carries an anchor into it.
50 52 func Extract(arc *doc.Archive, bodies map[string][]byte) ([]Document, error) {
51 52 if arc == nil {
52 1 return nil, fmt.Errorf("search: Extract needs an archive")
53 1 }
54 51 pages := arc.All()
55 51 out := make([]Document, 0, len(pages))
56 51 r := renderer()
57 51
58 1160 for _, p := range pages {
59 1160 src, ok := bodies[p.Path]
60 1160 if !ok {
61 1 return nil, fmt.Errorf("search: no body supplied for %s in %s", p.Path, arc.Space)
62 1 }
63 1159 front, body := doc.ParseFront(src)
64 1159 res := r.Render(body, doc.DirOf(p.Path), arc)
65 1159
66 1159 d := Document{
67 1159 Space: arc.Space,
68 1159 ID: p.ID,
69 1159 Rev: arc.Rev,
70 1159 Path: p.Path,
71 1159 Section: p.Section,
72 1159 Title: p.Title,
73 1159 Text: front.SearchText() + res.PlainText,
74 1159 }
75 1159 switch p.Kind {
76 1 case doc.KindCatalog:
77 1 d.Text = ""
78 4 case doc.KindLog:
79 4 d.Section = doc.LogSection
80 4 d.Text = ""
81 4 out = append(out, d)
82 4 out = append(out, logEntries(arc, p, body)...)
83 4 continue
84 }
85 1155 out = append(out, d)
86 }
87 50 return out, nil
88 }
89
90 // logEntries splits an activity log into one indexable document per dated
91 // entry.
92 //
93 // The entry ids doc.SplitLog produces are "log#<date>-<n>", named after
94 // warren's single vault-wide log. In a space that is not unique: a second
95 // document marked `type: log` — or simply a second file named log.md in another
96 // directory — produces the same ids, and in one index the same ids are the same
97 // documents, so one log would silently overwrite the other. The owning page's
98 // id is therefore substituted for the "log" prefix, which is a no-op for a log
99 // whose page id is in fact "log" and disambiguates every other case. The result
100 // also resolves better: "notes/dev-log#2026-05-31-1" names the document the
101 // entry is in.
102 4 func logEntries(arc *doc.Archive, p *doc.Page, body []byte) []Document {
103 4 entries := doc.SplitLog(body)
104 4 out := make([]Document, 0, len(entries))
105 5 for _, e := range entries {
106 5 suffix := strings.TrimPrefix(e.ID, "log#")
107 5 out = append(out, Document{
108 5 Space: arc.Space,
109 5 ID: p.ID + "#" + suffix,
110 5 Rev: arc.Rev,
111 5 Path: p.Path,
112 5 Anchor: e.Anchor,
113 5 Section: doc.LogSection,
114 5 Title: e.Date + " " + e.Title,
115 5 Text: e.SearchText(),
116 5 })
117 5 }
118 4 return out
119 }