~bigbes/sr-ht-spec

ref: 8219ede1804c144de2a2f2e42f3f476bff4b622a sr-ht-spec/service/archive.go -rw-r--r-- 4.4 KiB
8219ede1 — Eugene Blikh feat(web,service): the owner mints and revokes agent tokens in a browser 13 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package service

import (
	"context"
	"fmt"
	"sync"

	"github.com/go-git/go-git/v5/plumbing"

	"sourcecraft.dev/bigbes/sr-ht-spec/core"
	"sourcecraft.dev/bigbes/sr-ht-spec/doc"
	"sourcecraft.dev/bigbes/sr-ht-spec/gitx"
)

// archiveRenderer is shared across calls: doc.Renderer is documented as
// reusable and concurrency-safe, and building one per archive would rebuild the
// whole goldmark pipeline on every read. search/ shares one the same way.
var archiveRenderer = sync.OnceValue(doc.NewRenderer)

// Archive builds a space's addressable document set at a revision, together
// with every document's bytes, from one tree walk.
//
// This is the read every surface above this layer needs: the archive owns the
// design's addressing rule (a document's id when it is well-formed and unique
// in the space, otherwise its path), resolves the [[wikilinks]] between
// documents, and carries the link graph backlinks are read from. Before it
// existed, web/ scanned the git tree itself — reaching past this layer into
// gitx, which the layering rule forbids — and mcpsrv/ converted this package's
// documents back into git ones. Two workarounds around one missing accessor is
// how three agent-facing surfaces stop being behaviourally identical.
//
// The revision is resolved first and the documents are read at the resolved
// sha, so the archive and the bodies are the same revision by construction: a
// merge landing mid-read cannot make one answer describe two revisions. The
// resolved sha is the archive's Rev, and is what a caller hands back as the
// pinned ?rev=.
//
// bodies is keyed by tree path — the key doc.Page.Path carries — and holds the
// whole document, frontmatter included.
func (s *Service) Archive(ctx context.Context, sp *Space, rev string) (*doc.Archive, map[string][]byte, error) {
	commit, resolved, err := s.resolveRev(ctx, sp, rev)
	if err != nil {
		return nil, nil, err
	}
	docs, err := sp.Repo.ListDocuments(ctx, resolved)
	if err != nil {
		return nil, nil, readErr(err, "list documents at %s in %s", resolved, sp.Ref)
	}
	return archiveOf(sp.Ref, commit.String(), docs)
}

// ArchiveFrom builds the same archive out of documents this package already
// returned, without touching git.
//
// It is the seam for a caller that holds a [Document] set — a test double
// standing in for this service, a future surface that has already listed a
// revision — and it exists so that the one conversion from a hex object name
// back to a git hash lives here rather than in each of them. A malformed sha is
// an error, not a zero hash: plumbing.NewHash silently yields the zero value
// for anything it cannot parse, and a document whose render-cache key is zero
// is a cache collision waiting to happen.
//
// rev must be the resolved commit the documents were read at, for the same
// reason [Service.Archive] resolves before it reads.
func ArchiveFrom(sp core.SpaceRef, rev string, docs []Document) (*doc.Archive, map[string][]byte, error) {
	converted := make([]gitx.Document, 0, len(docs))
	for _, d := range docs {
		if !plumbing.IsHash(d.Blob) {
			return nil, nil, fmt.Errorf("service: document %q carries a malformed blob id %q", d.Path, d.Blob)
		}
		converted = append(converted, gitx.Document{Path: d.Path, Blob: plumbing.NewHash(d.Blob), Data: d.Data})
	}
	return archiveOf(sp, rev, converted)
}

// archiveOf assembles the archive, the bodies and the link graph out of one
// revision's documents.
//
// The link pass runs here rather than at each caller. doc.Page.Links is what
// Archive.Backlinks reads, and an archive handed out without it looks like a
// space where nothing links to anything — which is what every surface saw
// until web/ grew its own pass and re-rendered the revision on each page view.
// Filling it once, in the accessor, is the difference between a link graph and
// a per-surface convention. It costs one render of each document; at the
// confirmed volume — tens of documents a day — that is the same walk's data
// being parsed once more, not a second read.
func archiveOf(sp core.SpaceRef, rev string, docs []gitx.Document) (*doc.Archive, map[string][]byte, error) {
	arc := doc.FromDocuments(sp, rev, docs)
	bodies := make(map[string][]byte, len(docs))
	for _, d := range docs {
		bodies[d.Path] = d.Data
	}
	if err := arc.LinkPass(archiveRenderer(), bodies); err != nil {
		return nil, nil, fmt.Errorf("service: archive of %s at %s: %w", sp, rev, err)
	}
	return arc, bodies, nil
}