~bigbes/sr-ht-spec

ref: 3cb1c03d8078d5748cc13a2e9bd7ba7d078e1b37 sr-ht-spec/graph/resolver.go -rw-r--r-- 4.9 KiB
3cb1c03d — Eugene Blikh go.mod: take the shared libraries' current heads 2 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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package graph

import (
	"context"
	"time"

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

	"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)
}

// 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/ or search/ 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)
)

// 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
}