~bigbes/sr-ht-spec

ref: 42d4ee137ee6f4c70dd2ce4097c21606151f4a29 sr-ht-spec/graph/resolver.go -rw-r--r-- 4.4 KiB
42d4ee13 — Eugene Blikh feat(mcpsrv): spec_propose write tool (Phase 3) 26 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
package graph

import (
	"context"
	"time"

	"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 only the seams above; every field of
// the schema is answered by calling through them.
type Resolver struct {
	reader    Reader
	searcher  Searcher
	proposals Proposals
}