~bigbes/sr-ht-spec

ref: 64cae3af81d4b0039edc8ec3946bed36166a447b sr-ht-spec/graph/resolver.go -rw-r--r-- 5.8 KiB
64cae3af — Eugene Blikh graph: accept a meta.sr.ht token, so /query can be federated a day 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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package graph

import (
	"context"
	"time"

	"github.com/99designs/gqlgen/graphql"

	"sourcecraft.dev/bigbes/sr-ht-spec/authn"
	"sourcecraft.dev/bigbes/sr-ht-spec/core"
	"sourcecraft.dev/bigbes/sr-ht-spec/doc"
	"sourcecraft.dev/bigbes/sr-ht-spec/search"
	"sourcecraft.dev/bigbes/sr-ht-spec/service"
)

// Reader is the part of the orchestration layer this schema reads through.
// *service.Service satisfies it.
//
// It is declared the way mcpsrv/ and web/ declare theirs, and every method is
// one service/ already has: the design's "MCP and GraphQL are not competitors —
// the MCP tools call the same resolver layer, not a parallel implementation" is
// only true if all three surfaces call the same functions. A method here that
// service/ does not have would be the beginning of a second implementation, and
// a rule this schema resolved for itself would be a rule the other two could
// disagree with.
type Reader interface {
	ListSpaces(ctx context.Context) ([]*service.Space, error)
	OpenSpace(ctx context.Context, ref core.SpaceRef) (*service.Space, error)
	ResolveRev(ctx context.Context, sp *service.Space, rev string) (string, error)

	// Archive is the whole of the document read path here: the addressing rule
	// — a document is named by its frontmatter id when that is well-formed and
	// unique in the space, and by its path otherwise — lives in doc/ behind a
	// constructor that takes git objects, so a surface that resolved ids for
	// itself would either reach past service/ into gitx or keep a second copy
	// of the rule. Neither is allowed to happen a third time.
	Archive(ctx context.Context, sp *service.Space, rev string) (*doc.Archive, map[string][]byte, error)

	GetProject(ctx context.Context, ref core.ProjectRef) (*service.Project, error)
	ListProjects(ctx context.Context) ([]*service.Project, error)

	// ProjectSpaces is the listing counterpart of service.ResolveProject, and
	// the difference is not cosmetic: a filter is never enumerated, whereas a
	// list is enumerated by definition. The meta-project therefore lists every
	// space here — a snapshot of the corpus as of the call — while a query
	// restricted to it must go through the filter, which excludes nothing and
	// so cannot go stale.
	ProjectSpaces(ctx context.Context, ref core.ProjectRef) ([]*service.Space, error)
}

// Searcher is the query side of the one global index. *search.Index satisfies
// it.
type Searcher interface {
	Search(ctx context.Context, q search.Query) (search.Results, error)
}

// Proposal is one proposal as the read schema needs it: service/-shaped, in
// core types, so the port below stays free of anything GraphQL.
type Proposal struct {
	ID           int
	Space        core.SpaceRef
	Title        string
	Rationale    string
	BaseRev      string
	Branch       string
	State        core.ProposalState
	Approval     core.Approval
	MergedRev    string
	Agent        string
	AgentSession string
	Created      time.Time
	Resolved     *time.Time
}

// Proposals is the read side of the proposal store.
//
// It is a port with no production implementation yet, and that is a real gap
// rather than a design choice of this package: the design puts "proposal
// listing" in Phase 2's read schema, but service/ exposes no proposal read at
// all, and db/'s only listing is by state across every space. Nothing above
// service/ may touch db/ directly — that rule is what keeps the three
// agent-facing surfaces behaviourally identical — so this schema declares what
// it needs and waits for service/ to supply it.
//
// Until then Options.Proposals is nil and the `proposals` field fails loudly
// with that explanation. It does not return an empty list: "this space has no
// open proposals" and "this build cannot answer the question" are different
// answers, and quietly giving the first would tell a reviewer their queue is
// clear when it is unread.
type Proposals interface {
	ListProposals(ctx context.Context, space core.SpaceRef, state core.ProposalState) ([]Proposal, error)
}

// MetaAuthenticator turns a presented meta.sr.ht personal access token into a
// principal. The production implementation is *authn.MetaAuth.
//
// It takes the credential and not the request, which is the whole difference
// from the *authn.Resolver this endpoint holds beside it, and it is deliberate
// twice over. The credential has already been read by the time this is asked —
// resolveCaller routed on it to get here — so handing the request back would
// invite a second parse that could disagree with the first. And a plane that
// cannot see the request cannot read a cookie off it, which makes this
// endpoint's bearer-only rule structural rather than a thing to remember.
type MetaAuthenticator interface {
	VerifyToken(ctx context.Context, presented string) (authn.Principal, error)
}

// Compile-time assertions that the production types satisfy the interfaces this
// package is written against. They are here rather than in a test so that a
// signature change in service/, search/ or authn/ breaks the build of the
// package that depends on them, not of a test somebody may not run.
var (
	_ Reader            = (*service.Service)(nil)
	_ Searcher          = (*search.Index)(nil)
	_ MetaAuthenticator = (*authn.MetaAuth)(nil)
)

// Resolver is the root resolver. It holds the seams above — every read field of
// the schema is answered by calling through them — and the schema it is itself
// part of.
type Resolver struct {
	reader    Reader
	searcher  Searcher
	proposals Proposals

	// schema is this service's executable schema, set by newSchema once the
	// resolver it is built from exists. The webhook management resolvers need
	// it: createUserWebhook validates a subscriber's stored query against it.
	// It used to be read off core-go's server context, which is installed on
	// the authenticated router and which /query no longer runs behind.
	schema graphql.ExecutableSchema
}