feat(graph): GraphQL-native webhook surface (Phase 5a) The webhook types, mutations, and resolvers, adapted from the pages.sr.ht core-go template for spec's single-owner model. - SDL: WebhookEvent (PROPOSAL_OPENED/MERGED/REJECTED), WebhookSubscription interface + UserWebhookSubscription, WebhookDelivery, WebhookPayload interface + ProposalEvent (carries a Proposal), cursor wrappers, `webhook` payload root field, and a `type Mutation` with createUserWebhook / deleteUserWebhook. No OAuth `client` field and no @access/@private directives — spec has no OAuth clients or scopes, so the owner gate is the entire ACL. - Models: hand-written database.Model impls (UserWebhookSubscription, WebhookDelivery) so gqlgen autobinds rather than generates them; events via pq.Array; cursor keyset pagination. - Resolvers: all owner-gated via authn (spec's ACL), using core-go's webhook engine — Validate, NewAuthConfig (INTERNAL, via the coreauth bridge), FilterWebhooks, WebhookContext.Exec for the sample, and the `webhook`→Payload(ctx) root. Proposal writes deliberately stay off this surface (only webhook mutations; the schema test now asserts exactly that). - gqlgen.yml binds Cursor to core-go's model.Cursor; generated code regenerated with the pinned gqlgen v0.17.36 (reproducible). Compiles and vets clean; existing graph read tests still pass. Runtime context wiring and event firing are the next slices.
feat(graph): wire the proposals read to service.ListProposals (Phase 3) The Proposals port declared a proposal listing and left Options.Proposals nil, so the `proposals` query failed loudly with "service/ exposes no proposal listing yet". Phase 3 supplies it: an adapter maps service.Proposal onto graph.Proposal at the edge — the two structs are identical, but service/ must not import graph/, so the rename lives here beside web.NewReader's equivalent — and cmd wires graph.NewProposals(svc) into the schema. The `proposals` field now answers from service/.
feat(graph): the read-only GraphQL schema at /query Eight query fields over the service layer, no Mutation and no Subscription — the design defers mutations until the proposal state machine settles, and TestSchemaHasNoMutations stands guard on that. Access is fail-closed and gated before parse, matching web's ACL exactly, so introspection is treated as content too. A federating api.sr.ht must therefore present a token or skip us, which costs one log line. A malformed rev is reported as a GraphQL error rather than folded into null. service/ deliberately hides malformed-versus-absent from probing, but the caller here is already authenticated as the owner or its own agent, and a bare null for rev=proposals/42 is indistinguishable from an absent document — it reads as a silently dropped argument. Proposal listing declares its port but is unwired: service/ exposes no proposal read yet, and returning an empty list would tell a reviewer their queue is clear when it is merely unread.