coverage~bigbes/sr-ht-spec3cb1c03dservice/archive.go

Coverage
80.0% 16/20 statements
Δ
Blob
ebcd24d
1 package service
2
3 import (
4 "context"
5 "fmt"
6 "sync"
7
8 "github.com/go-git/go-git/v5/plumbing"
9
10 "sourcecraft.dev/bigbes/sr-ht-spec/core"
11 "sourcecraft.dev/bigbes/sr-ht-spec/doc"
12 "sourcecraft.dev/bigbes/sr-ht-spec/gitx"
13 )
14
15 // archiveRenderer is shared across calls: doc.Renderer is documented as
16 // reusable and concurrency-safe, and building one per archive would rebuild the
17 // whole goldmark pipeline on every read. search/ shares one the same way.
18 var archiveRenderer = sync.OnceValue(doc.NewRenderer)
19
20 // Archive builds a space's addressable document set at a revision, together
21 // with every document's bytes, from one tree walk.
22 //
23 // This is the read every surface above this layer needs: the archive owns the
24 // design's addressing rule (a document's id when it is well-formed and unique
25 // in the space, otherwise its path), resolves the [[wikilinks]] between
26 // documents, and carries the link graph backlinks are read from. Before it
27 // existed, web/ scanned the git tree itself — reaching past this layer into
28 // gitx, which the layering rule forbids — and mcpsrv/ converted this package's
29 // documents back into git ones. Two workarounds around one missing accessor is
30 // how three agent-facing surfaces stop being behaviourally identical.
31 //
32 // The revision is resolved first and the documents are read at the resolved
33 // sha, so the archive and the bodies are the same revision by construction: a
34 // merge landing mid-read cannot make one answer describe two revisions. The
35 // resolved sha is the archive's Rev, and is what a caller hands back as the
36 // pinned ?rev=.
37 //
38 // bodies is keyed by tree path — the key doc.Page.Path carries — and holds the
39 // whole document, frontmatter included.
40 3 func (s *Service) Archive(ctx context.Context, sp *Space, rev string) (*doc.Archive, map[string][]byte, error) {
41 3 commit, resolved, err := s.resolveRev(ctx, sp, rev)
42 3 if err != nil {
43 1 return nil, nil, err
44 1 }
45 2 docs, err := sp.Repo.ListDocuments(ctx, resolved)
46 2 if err != nil {
47 0 return nil, nil, readErr(err, "list documents at %s in %s", resolved, sp.Ref)
48 0 }
49 2 return archiveOf(sp.Ref, commit.String(), docs)
50 }
51
52 // ArchiveFrom builds the same archive out of documents this package already
53 // returned, without touching git.
54 //
55 // It is the seam for a caller that holds a [Document] set — a test double
56 // standing in for this service, a future surface that has already listed a
57 // revision — and it exists so that the one conversion from a hex object name
58 // back to a git hash lives here rather than in each of them. A malformed sha is
59 // an error, not a zero hash: plumbing.NewHash silently yields the zero value
60 // for anything it cannot parse, and a document whose render-cache key is zero
61 // is a cache collision waiting to happen.
62 //
63 // rev must be the resolved commit the documents were read at, for the same
64 // reason [Service.Archive] resolves before it reads.
65 1 func ArchiveFrom(sp core.SpaceRef, rev string, docs []Document) (*doc.Archive, map[string][]byte, error) {
66 1 converted := make([]gitx.Document, 0, len(docs))
67 1 for _, d := range docs {
68 1 if !plumbing.IsHash(d.Blob) {
69 1 return nil, nil, fmt.Errorf("service: document %q carries a malformed blob id %q", d.Path, d.Blob)
70 1 }
71 0 converted = append(converted, gitx.Document{Path: d.Path, Blob: plumbing.NewHash(d.Blob), Data: d.Data})
72 }
73 0 return archiveOf(sp, rev, converted)
74 }
75
76 // archiveOf assembles the archive, the bodies and the link graph out of one
77 // revision's documents.
78 //
79 // The link pass runs here rather than at each caller. doc.Page.Links is what
80 // Archive.Backlinks reads, and an archive handed out without it looks like a
81 // space where nothing links to anything — which is what every surface saw
82 // until web/ grew its own pass and re-rendered the revision on each page view.
83 // Filling it once, in the accessor, is the difference between a link graph and
84 // a per-surface convention. It costs one render of each document; at the
85 // confirmed volume — tens of documents a day — that is the same walk's data
86 // being parsed once more, not a second read.
87 2 func archiveOf(sp core.SpaceRef, rev string, docs []gitx.Document) (*doc.Archive, map[string][]byte, error) {
88 2 arc := doc.FromDocuments(sp, rev, docs)
89 2 bodies := make(map[string][]byte, len(docs))
90 4 for _, d := range docs {
91 4 bodies[d.Path] = d.Data
92 4 }
93 2 if err := arc.LinkPass(archiveRenderer(), bodies); err != nil {
94 0 return nil, nil, fmt.Errorf("service: archive of %s at %s: %w", sp, rev, err)
95 0 }
96 2 return arc, bodies, nil
97 }