~bigbes/sr-ht-spec

ref: 3a56ee617a14a82d7d2d430173c84474444bc111 sr-ht-spec/graph/schema.graphqls -rw-r--r-- 14.3 KiB
3a56ee61 — Eugene Blikh deps: bump sr-ht-ecore for the slog panic reporter 9 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# spec.sr.ht — the read schema.
#
# There are no mutations here and that is the design's decision, not an
# omission: the write plane's concurrency story is `If-Match: <base-rev>`, an
# HTTP idiom with well-defined 409 semantics that agents get right by default,
# and modelling base-rev as a mutation argument is a contract worth designing
# once, after the proposal state machine has stopped moving. Federated types are
# consumed contracts, so churning the proposal types here would be expensive.
#
# Everything this schema serves goes through service/ — the same functions the
# REST, MCP and web surfaces call — so the three agent-facing surfaces cannot
# disagree about what a document is or which revision is approved.

"""
An RFC-3339 timestamp.
"""
scalar Time

"""
An opaque pagination cursor. Passed back into the same field to fetch the next
page; null when there are no further results.
"""
scalar Cursor

"""
The lifecycle of a proposal. Collapsed from the usual five states because there
is exactly one reviewer: "approve" is "merge now", and a proposal you dislike is
rejected rather than sent back for changes.
"""
enum ProposalState {
  OPEN
  MERGED
  REJECTED
}

"""
How a merge was authorized. Auto-merged is not human-approved, and readers must
be able to tell: a bot asking for the approved text of a spec should be able to
require human approval and get a different answer than for a firehose note.
"""
enum Approval {
  HUMAN
  POLICY
}

type Query {
  """
  Every space on the instance, ordered by owner then name. There is one human
  here and no visibility levels, so there is nothing to filter: the list is the
  whole corpus.
  """
  spaces: [Space!]!

  """
  One space by reference. `owner` carries no leading '~' — that is routing
  decoration, never part of the stored name. Null when there is no such space.
  """
  space(owner: String!, name: String!): Space

  """
  One document of a space.

  Exactly one of `id` and `path` must be given. `id` is the document's address
  under the design's rule: its frontmatter `id:` when that is well-formed and
  unique in the space, and otherwise its path without the ".md" extension. An id
  claimed by two documents resolves to neither — refusing to guess is the only
  safe answer, since silently picking one would make links point at a document
  the author did not mean.

  `rev` pins the read to an immutable revision and must be a full 40-character
  object name; omit it to read the space's approved head, which is the default
  because serving drafts by default would poison every downstream agent context
  with unreviewed text. Ref names are refused, so there is no `rev` that makes
  this field serve an unreviewed proposal branch.

  Null when the space, the revision or the document is absent.
  """
  document(space: String!, id: String, path: String, rev: String): Document

  """
  Every document of a space at a revision, in tree order. `rev` follows the same
  rule as on `document`.
  """
  documents(space: String!, rev: String): [Document!]!

  """
  Keyword search over the one global index.

  `spaces` is the project filter: a project is a saved filter over this index,
  not a container, so searching a project means passing its member spaces here.

  Omitting the argument searches every space — that is the meta-project, a
  filter that excludes nothing. Passing a list searches exactly those spaces,
  and passing an *empty* list therefore searches nothing at all, which is what
  an empty project's membership means. The two are different arguments here on
  purpose: null and [] are distinct in GraphQL, so a client that resolved a
  project to zero spaces cannot be handed the whole corpus by accident.

  An empty `query` returns no hits rather than every document: "search for
  nothing" is a caller that has not collected its input yet.
  """
  search(query: String!, spaces: [String!], limit: Int, offset: Int): SearchResults!

  """
  Every stored project, ordered by owner then name. The meta-project is not in
  this list because it is not a row; it is an address that resolves to a filter,
  and `project(owner: ..., name: "everything")` returns it.
  """
  projects: [Project!]!

  """
  One project by reference. `name` carries no leading '+'. Null when there is no
  such project.
  """
  project(owner: String!, name: String!): Project

  """
  A space's proposals in one state, newest first.

  `state` is required. The proposal store lists by state, and an optional
  argument would need an "every state" meaning for its absent case — the same
  empty-means-everything polarity this schema refuses on `search(spaces:)`. Ask
  for the states you want, with aliases if you want more than one.

  This is a read: opening, merging and rejecting proposals are write-plane
  operations and live on REST and MCP.
  """
  proposals(space: String!, state: ProposalState!): [Proposal!]!
}

"""
A space: one bare git repository, addressed `~owner/name`. It is the unit of
ownership, ACL and review policy.
"""
type Space {
  """The owning user, with no leading '~'."""
  owner: String!
  name: String!
  """The canonical reference, "~owner/name"."""
  ref: String!
  created: Time!
  """
  The current tip of the approved branch, as a full object name. This is the
  value to pin a later read to, and the value the write plane takes as
  `If-Match`. Resolving it opens the space's repository, so it is only paid for
  when it is asked for.
  """
  approvedRev: String!
}

"""
A markdown document with YAML frontmatter, at one revision. A document reachable
from the approved ref is approved: approval is a property of the branch, never
of the frontmatter, which is why there is no "approved" field here and no
"approved" value in `status`.
"""
type Document {
  """The space this document belongs to, "~owner/name"."""
  space: String!
  """
  How this document is addressed: its frontmatter id when that is well-formed
  and unique in the space, and its path without the extension otherwise.
  """
  id: String!
  """
  The frontmatter `id:`, when the document has a well-formed one. Null when it
  is absent or malformed.

  It is not always the same as `id`: an id two documents claim resolves to
  neither of them, so `id` falls back to the path there as well. `id != docId`
  is precisely the set of documents that cannot be addressed by their id, which
  is what an agent about to propose an edit needs to know.
  """
  docId: String
  """The document's path in the tree, extension included."""
  path: String!
  """The commit this content was read at. Immutable: pass it back as `rev`."""
  rev: String!
  """The sha of this document's content. Changes only when the content does."""
  blob: String!
  """
  Whether the caller named the revision. False means this is the approved head
  as of this call — the reviewed, canonical text.
  """
  pinned: Boolean!
  """
  The document's title: its frontmatter `title:`, then its first `# heading`,
  then its file name. Never empty — the chain is what keeps an untitled document
  nameable rather than reported as blank.
  """
  title: String!
  """
  The authored lifecycle marker (draft, review, superseded). It is not approval
  state. Empty when the frontmatter does not carry the key.
  """
  status: String!
  """The top-level directory the document lives under ("specs", "notes")."""
  section: String!
  """Authored metadata. Empty when the frontmatter does not carry the key."""
  summary: String!
  type: String!
  supersedes: String!
  tags: [String!]!
  owners: [String!]!
  """The whole document, frontmatter included, exactly as stored."""
  markdown: String!
}

"""
A project: a named set of spaces sharing one search scope, one MCP view and one
wikilink namespace.

A project is pure metadata — a saved filter, not a container. It owns no index
and no storage, which is why it has no revision and no documents of its own.
"""
type Project {
  """The owning user, with no leading '~'."""
  owner: String!
  """The project name, with no leading '+'."""
  name: String!
  """The canonical reference, "~owner/+name"."""
  ref: String!
  """
  Whether this is the meta-project: the degenerate filter that excludes nothing.
  Its `spaces` is therefore every space as of this call rather than a stored
  membership — a snapshot, which is why a query restricted to a project should
  be built from the filter and not from a frozen list of names.
  """
  meta: Boolean!
  """
  The member spaces, ordered by owner then name. Empty for a project that
  selects nothing, which is what a project with no members means.
  """
  spaces: [Space!]!
}

"""One page of ranked hits."""
type SearchResults {
  """How many documents matched, not how many were returned."""
  total: Int!
  """How long the query took, as a Go duration string."""
  took: String!
  hits: [SearchHit!]!
}

"""
One ranked document. `space`, `path`, `rev` and `anchor` together are a pinned,
immutable address for the result.
"""
type SearchHit {
  space: String!
  """The document's address within its space, as `Document.id` reports it."""
  id: String!
  rev: String!
  path: String!
  """The heading anchor within `path`, set for one entry of an activity log."""
  anchor: String!
  title: String!
  section: String!
  """The language the document was indexed as ("en", "ru")."""
  lang: String!
  score: Float!
  """
  A fragment of the matching text with the matched terms wrapped in `<mark>`.
  Everything around them is HTML-escaped by the index, so this is HTML: render
  it as HTML or strip the tags, but do not escape it again.
  """
  snippet: String!
}

"""
A bundle of document edits awaiting review: a branch `proposals/<id>` plus a
row. Proposals are read-only here; they are opened over REST and MCP.
"""
type Proposal {
  id: Int!
  """The space the proposal is against, "~owner/name"."""
  space: String!
  title: String!
  rationale: String!
  """
  The approved-head sha the agent had read when it opened the proposal. It does
  not move as the proposal accumulates edits.
  """
  baseRev: String!
  """The branch carrying the edits, "proposals/<id>"."""
  branch: String!
  state: ProposalState!
  """How the merge was authorized. Null until the proposal has merged."""
  approval: Approval
  """The merge commit. Null until the proposal has merged."""
  mergedRev: String
  """The agent identity that opened it, e.g. "claude-code/spec-writer"."""
  agent: String!
  """The agent's session id."""
  agentSession: String!
  created: Time!
  """When the proposal left the open state. Null while it is still open."""
  resolved: Time
}

# --- Webhooks ---------------------------------------------------------------
#
# The GraphQL-native webhook surface, adapted from the core-go reference. spec is
# single-owner: there are no OAuth clients and no @access scopes, so the
# reference's `client` field and every directive are dropped. Webhook management
# is owner-gated in the resolvers, which is the whole ACL.

"""
A proposal lifecycle event a webhook may subscribe to.
"""
enum WebhookEvent {
  PROPOSAL_OPENED
  PROPOSAL_MERGED
  PROPOSAL_REJECTED
}

"""
A webhook subscription: a URL and a GraphQL query evaluated in the webhook
context when one of `events` fires.
"""
interface WebhookSubscription {
  id: Int!
  events: [WebhookEvent!]!
  query: String!
  url: String!

  "All deliveries which have been sent to this webhook."
  deliveries(cursor: Cursor): WebhookDeliveryCursor!

  "Returns a sample payload for this subscription, for testing purposes."
  sample(event: WebhookEvent!): String!
}

type UserWebhookSubscription implements WebhookSubscription {
  id: Int!
  events: [WebhookEvent!]!
  query: String!
  url: String!
  deliveries(cursor: Cursor): WebhookDeliveryCursor!
  sample(event: WebhookEvent!): String!
}

type WebhookDelivery {
  uuid: String!
  date: Time!
  event: WebhookEvent!
  subscription: WebhookSubscription!
  requestBody: String!

  """
  These details are provided only after a response is received from the
  remote server. If a response is sent whose Content-Type is not text/*, or
  cannot be decoded as UTF-8, the response body will be null. It will be
  truncated after 64 KiB.
  """
  responseBody: String
  responseHeaders: String
  responseStatus: Int
}

"""
The payload delivered to a webhook: the event being dispatched, over which the
subscription's `query` is evaluated.
"""
interface WebhookPayload {
  uuid: String!
  event: WebhookEvent!
  date: Time!
}

type ProposalEvent implements WebhookPayload {
  uuid: String!
  event: WebhookEvent!
  date: Time!

  proposal: Proposal!
}

"""
A cursor for enumerating a list of webhook deliveries.

If there are additional results available, the cursor object may be passed
back into the same endpoint to retrieve another page. If the cursor is null,
there are no remaining results to return.
"""
type WebhookDeliveryCursor {
  results: [WebhookDelivery!]!
  cursor: Cursor
}

"""
A cursor for enumerating a list of webhook subscriptions.

If there are additional results available, the cursor object may be passed
back into the same endpoint to retrieve another page. If the cursor is null,
there are no remaining results to return.
"""
type WebhookSubscriptionCursor {
  results: [WebhookSubscription!]!
  cursor: Cursor
}

extend type Query {
  "Returns a list of user webhook subscriptions."
  userWebhooks(cursor: Cursor): WebhookSubscriptionCursor!

  "Returns details of a user webhook subscription by its ID."
  userWebhook(id: Int!): WebhookSubscription

  """
  Returns information about the webhook currently being processed. This is not
  valid during normal queries over HTTP, and will return an error if used
  outside of a webhook context.
  """
  webhook: WebhookPayload!
}

input UserWebhookInput {
  url: String!
  events: [WebhookEvent!]!
  query: String!
}

type Mutation {
  """
  Creates a new user webhook subscription. When an event from the provided list
  of events occurs, the `query` parameter (a GraphQL query) will be evaluated
  and the results will be sent to the provided URL as the body of an HTTP POST
  request. The list of events must include at least one event.

  This query is evaluated in the webhook context, such that query { webhook }
  may be used to access details of the event which triggered the webhook. The
  query may not make any mutations.
  """
  createUserWebhook(config: UserWebhookInput!): WebhookSubscription!

  """
  Deletes a user webhook. Any events already queued may still be delivered
  after this request completes.
  """
  deleteUserWebhook(id: Int!): WebhookSubscription!
}