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 }