@vercel/board 0.0.0-stage → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +179 -2
  2. package/dist/a2-5EG8FkZ8.d.mts +3359 -0
  3. package/dist/a2-DYGYankn.mjs +3045 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +29 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-rdN4dDTU.mjs +223 -0
  9. package/dist/attempts-BQUp9MKO.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-wHFcAte3.d.mts +203 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
  35. package/dist/backend-http-BsDSgmeo.mjs +613 -0
  36. package/dist/backend-http-Cv6xbEy9.mjs +469 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-3EvQ8idk.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-BpgyEdh6.d.mts +4 -0
  56. package/dist/exports-CyY3_FjZ.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-BZ3lItjH.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-Ccbb2LSb.mjs +170 -0
  76. package/dist/server-Cxey38B-.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-ojFEXEyN.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +262 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +499 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +165 -3
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. package/sql/postgres/schema.sql +451 -0
@@ -0,0 +1,512 @@
1
+ import { t as BoardError } from "./client-DGDPaN7U.mjs";
2
+ //#region src/conformance/index.ts
3
+ var Mismatch = class extends Error {};
4
+ const show = (value) => JSON.stringify(value) ?? String(value);
5
+ function check(condition, message) {
6
+ if (!condition) throw new Mismatch(message);
7
+ }
8
+ function same(actual, expected, what) {
9
+ if (actual !== expected) throw new Mismatch(`${what}: expected ${show(expected)}, got ${show(actual)}`);
10
+ }
11
+ async function rejects(attempt, status, code, what) {
12
+ try {
13
+ await attempt;
14
+ } catch (error) {
15
+ if (error instanceof BoardError && error.status === status && error.code === code) return;
16
+ throw new Mismatch(`${what}: expected ${status} ${code}, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
17
+ }
18
+ throw new Mismatch(`${what}: expected ${status} ${code}, and it succeeded`);
19
+ }
20
+ /** An ID or cursor as the protocol shapes them: a string of 1 to 128 characters. */
21
+ function isRef(value, what) {
22
+ check(typeof value === "string" && value.length >= 1 && value.length <= 128, `${what} should be a string of 1 to 128 characters, got ${show(value)}`);
23
+ }
24
+ /**
25
+ * The groups a board may keep none of, saying so with a 501: a board on `a2()` without a scheduler
26
+ * answers `no_scheduler`, and without a blob store `no_files`.
27
+ */
28
+ const OPTIONAL = /* @__PURE__ */ new Set(["schedules", "attachments"]);
29
+ const needRoot = (ctx) => {
30
+ check(ctx.root && ctx.reply, "posting failed, so there's nothing to read");
31
+ return {
32
+ root: ctx.root,
33
+ reply: ctx.reply
34
+ };
35
+ };
36
+ async function post(ctx, text, extra = {}) {
37
+ const message = await ctx.board.post(ctx.channel, {
38
+ text,
39
+ labels: ctx.labels,
40
+ noSync: true,
41
+ ...extra
42
+ });
43
+ ctx.posted.push(message.id);
44
+ return message;
45
+ }
46
+ const CHECKS = [
47
+ {
48
+ group: "core",
49
+ name: "posts a message, and reads it back by its ID",
50
+ run: async (ctx) => {
51
+ const root = await post(ctx, "conformance: a root");
52
+ isRef(root.id, "a message's id");
53
+ check(typeof root.version === "number", `a message's version should be a number, got ${show(root.version)}`);
54
+ const thread = await ctx.board.thread(root.id);
55
+ same(thread.message.id, root.id, "the thread read by that id");
56
+ same(thread.message.text, "conformance: a root", "its text");
57
+ ctx.root = root;
58
+ const reply = await ctx.board.reply(root.id, {
59
+ text: "conformance: a reply",
60
+ noSync: true
61
+ });
62
+ ctx.posted.push(reply.id);
63
+ ctx.reply = reply;
64
+ }
65
+ },
66
+ {
67
+ group: "core",
68
+ name: "puts a reply in its root's thread",
69
+ run: async (ctx) => {
70
+ const { root, reply } = needRoot(ctx);
71
+ same(reply.re, root.id, "the reply's re");
72
+ check((await ctx.board.thread(root.id)).replies.some((m) => m.id === reply.id), `the thread's replies should include ${reply.id}`);
73
+ }
74
+ },
75
+ {
76
+ group: "core",
77
+ name: "posts once for a client ID, however many times it is sent",
78
+ run: async (ctx) => {
79
+ const id = `conformance-${ctx.labels.conformance}`;
80
+ const first = await post(ctx, "conformance: once", { id });
81
+ same((await post(ctx, "conformance: once", { id })).id, first.id, "the second post with the same client id");
82
+ same((await ctx.board.posted(id)).id, first.id, "the post found by its client id");
83
+ await rejects(ctx.board.posted(`${id}-never`), 404, "post_not_found", "a client id nothing was posted with");
84
+ }
85
+ },
86
+ {
87
+ group: "core",
88
+ name: "raises the version with each edit, reaction, and label",
89
+ run: async (ctx) => {
90
+ const { root } = needRoot(ctx);
91
+ const version = async () => (await ctx.board.thread(root.id)).message;
92
+ let before = await version();
93
+ for (const [what, change] of [
94
+ ["an edit", () => ctx.board.edit(root.id, "conformance: an edited root")],
95
+ ["a reaction", () => ctx.board.react(root.id, "eyes")],
96
+ ["a label", () => ctx.board.label(root.id, { set: { state: "open" } })]
97
+ ]) {
98
+ await change();
99
+ const after = await version();
100
+ check(after.version > before.version, `${what} should raise the version past ${before.version}, got ${after.version}`);
101
+ before = after;
102
+ }
103
+ same(before.text, "conformance: an edited root", "the edited text");
104
+ check(before.editedAt !== null, "an edited message should have editedAt");
105
+ check(before.reactions.eyes?.includes(ctx.me.actor), `reactions.eyes should include ${ctx.me.actor}, got ${show(before.reactions)}`);
106
+ same(before.labels.state, "open", "the label set");
107
+ await ctx.board.label(root.id, { unset: ["state"] });
108
+ same((await version()).labels.state, void 0, "the label unset");
109
+ }
110
+ },
111
+ {
112
+ group: "core",
113
+ name: "matches a label on the message or its thread's root",
114
+ run: async (ctx) => {
115
+ const { reply } = needRoot(ctx);
116
+ const { messages } = await ctx.board.messages({
117
+ channel: ctx.channel,
118
+ label: ctx.labels
119
+ });
120
+ check(messages.some((m) => m.id === reply.id), "a reply should match its root's label");
121
+ }
122
+ },
123
+ {
124
+ group: "core",
125
+ name: "reads a channel after a cursor or a message ID, oldest first",
126
+ run: async (ctx) => {
127
+ const { root, reply } = needRoot(ctx);
128
+ const page = await ctx.board.messages({
129
+ channel: ctx.channel,
130
+ label: ctx.labels,
131
+ after: ctx.start
132
+ });
133
+ isRef(page.cursor, "a read's cursor");
134
+ const ids = page.messages.map((m) => m.id);
135
+ check(ids.indexOf(root.id) !== -1 && ids.indexOf(root.id) < ids.indexOf(reply.id), `the root should come before its reply, got ${show(ids)}`);
136
+ const later = await ctx.board.messages({
137
+ channel: ctx.channel,
138
+ label: ctx.labels,
139
+ after: root.id
140
+ });
141
+ check(!later.messages.some((m) => m.id === root.id), "reading after a message should leave it out");
142
+ check(later.messages.some((m) => m.id === reply.id), "reading after a message should include what came after it");
143
+ }
144
+ },
145
+ {
146
+ group: "core",
147
+ name: "lists threads, each with its reply count and latest reply",
148
+ run: async (ctx) => {
149
+ const { root, reply } = needRoot(ctx);
150
+ const { threads } = await ctx.board.threads({
151
+ channel: ctx.channel,
152
+ label: ctx.labels
153
+ });
154
+ const found = threads.find((t) => t.message.id === root.id);
155
+ check(found, `the threads should include ${root.id}`);
156
+ same(found.replies, 1, "its reply count");
157
+ same(found.last?.id, reply.id, "its latest reply");
158
+ }
159
+ },
160
+ {
161
+ group: "core",
162
+ name: "reads events after a cursor, each with a cursor of its own",
163
+ run: async (ctx) => {
164
+ const { root } = needRoot(ctx);
165
+ const page = await ctx.board.poll({
166
+ channel: ctx.channel,
167
+ after: ctx.start
168
+ });
169
+ isRef(page.cursor, "the events' cursor");
170
+ const posted = page.events.find((event) => event.type === "message.posted" && event.message?.id === root.id);
171
+ check(posted, `the events should include ${root.id} being posted`);
172
+ isRef(posted.cursor, "an event's cursor");
173
+ check(!(await ctx.board.poll({
174
+ channel: ctx.channel,
175
+ after: page.cursor
176
+ })).events.some((event) => event.message?.id === root.id), "reading on from the cursor should not repeat what it read");
177
+ }
178
+ },
179
+ {
180
+ group: "core",
181
+ name: "holds a long-poll open until the next event",
182
+ run: async (ctx) => {
183
+ const { cursor } = await ctx.board.poll({
184
+ channel: ctx.channel,
185
+ label: ctx.labels
186
+ });
187
+ const waiting = ctx.board.poll({
188
+ channel: ctx.channel,
189
+ label: ctx.labels,
190
+ after: cursor,
191
+ wait: 10
192
+ });
193
+ await new Promise((resolve) => setTimeout(resolve, 200));
194
+ const later = await post(ctx, "conformance: while it waits");
195
+ check((await waiting).events.some((event) => event.message?.id === later.id), "the long-poll should return the post made while it waited");
196
+ }
197
+ },
198
+ {
199
+ group: "core",
200
+ name: "deletes a message, leaving a tombstone",
201
+ run: async (ctx) => {
202
+ const message = await post(ctx, "conformance: to delete");
203
+ await ctx.board.delete(message.id);
204
+ const { message: after } = await ctx.board.thread(message.id);
205
+ check(after.deleted && after.text === "", `a deleted message should be deleted with no text, got ${show({
206
+ deleted: after.deleted,
207
+ text: after.text
208
+ })}`);
209
+ }
210
+ },
211
+ {
212
+ group: "core",
213
+ name: "lists the channel and you in it",
214
+ run: async (ctx) => {
215
+ const found = (await ctx.board.channels()).find((c) => c.path === ctx.channel);
216
+ check(found, `the channels should include ${ctx.channel}`);
217
+ check(typeof found.lastIndex === "number", `a channel's lastIndex should be a number, got ${show(found.lastIndex)}`);
218
+ check((await ctx.board.actors()).some((a) => a.id === ctx.me.actor), `the actors should include ${ctx.me.actor}`);
219
+ }
220
+ },
221
+ {
222
+ group: "core",
223
+ name: "answers 404 for an ID that names nothing",
224
+ run: async (ctx) => {
225
+ await rejects(ctx.board.thread(`no-such-message-${ctx.labels.conformance}`), 404, "message_not_found", "reading a message that was never posted");
226
+ }
227
+ },
228
+ {
229
+ group: "core",
230
+ name: "answers 400 for a request that does not validate",
231
+ run: async (ctx) => {
232
+ await rejects(ctx.board.post("Not A Channel", {
233
+ text: "conformance",
234
+ noSync: true
235
+ }), 400, "invalid_request", "posting to a malformed channel");
236
+ }
237
+ },
238
+ {
239
+ group: "schedules",
240
+ name: "schedules a reply, lists it, and cancels it",
241
+ run: async (ctx) => {
242
+ const { root } = needRoot(ctx);
243
+ const schedule = await ctx.board.schedule({ re: root.id }, {
244
+ text: "conformance: later",
245
+ noSync: true,
246
+ in: "1d"
247
+ });
248
+ isRef(schedule.id, "a schedule's id");
249
+ same(schedule.re, root.id, "the schedule's re");
250
+ same(schedule.state, "pending", "the schedule's state");
251
+ check((await ctx.board.schedules({ re: root.id })).some((s) => s.id === schedule.id), `the schedules should include ${schedule.id}`);
252
+ const { schedule: canceled } = await ctx.board.cancelSchedule(schedule.id);
253
+ same(canceled.state, "canceled", "the canceled schedule");
254
+ }
255
+ },
256
+ {
257
+ group: "attachments",
258
+ name: "takes a file, posts it, and gives it back",
259
+ run: async (ctx) => {
260
+ const bytes = new TextEncoder().encode(`conformance ${ctx.labels.conformance}\n`);
261
+ const attachment = await ctx.board.upload({
262
+ data: bytes,
263
+ name: "conformance.txt",
264
+ type: "text/plain"
265
+ });
266
+ check((await post(ctx, "conformance: a file", { attachments: [attachment.id] })).attachments.some((a) => a.id === attachment.id), "the post should carry the file");
267
+ const response = await ctx.board.download(attachment.id);
268
+ same(new TextDecoder().decode(await response.arrayBuffer()), new TextDecoder().decode(bytes), "the file's bytes");
269
+ }
270
+ },
271
+ {
272
+ group: "watchers",
273
+ name: "matches a registered tail, hands the match to a pull, and takes its ack",
274
+ run: async (ctx) => {
275
+ const registered = (await ctx.board.watchers()).find((r) => r.owner === ctx.me.principal);
276
+ if (registered && (Object.keys(registered.watch).length > 0 || (registered.sync?.length ?? 0) > 0)) return "this token has watchers or a sync registered here, and checking would replace them";
277
+ const url = registered?.url ?? null;
278
+ await ctx.board.register({
279
+ url: null,
280
+ watch: { conformance: { tail: `${ctx.channel} --label conformance=${ctx.labels.conformance}` } }
281
+ });
282
+ try {
283
+ const message = await post(ctx, "conformance: for a watcher");
284
+ const deadline = Date.now() + 1e4;
285
+ let match;
286
+ while (!match && Date.now() < deadline) match = (await ctx.board.matches({ wait: 5 })).find((m) => m.event.message?.id === message.id);
287
+ check(match, "a pull should hand out the match for the post");
288
+ same(match.watcher, "conformance", "the match's watcher");
289
+ await ctx.board.ack(match.id, { lease: match.lease });
290
+ await rejects(ctx.board.ack(match.id, { lease: match.lease }), 404, "no_match", "acking a match twice");
291
+ } finally {
292
+ await ctx.board.register({
293
+ url,
294
+ watch: {}
295
+ });
296
+ }
297
+ }
298
+ },
299
+ {
300
+ group: "sync",
301
+ name: "keeps sync's records: an import as a platform author, its lookups by platform ID, and a receipt",
302
+ run: async (ctx) => {
303
+ const key = "conformance";
304
+ const thread = `${key}:${ctx.labels.conformance}:1`;
305
+ const author = {
306
+ id: `${key}:user:${ctx.labels.conformance}`,
307
+ handle: "conformance",
308
+ name: "Conformance",
309
+ avatar: null,
310
+ isBot: false
311
+ };
312
+ const origin = {
313
+ thread,
314
+ message: "1"
315
+ };
316
+ const registered = (await ctx.board.watchers()).find((registration) => registration.owner === ctx.me.principal);
317
+ if (registered && (Object.keys(registered.watch).length > 0 || (registered.sync?.length ?? 0) > 0)) return "this token has watchers or a sync registered here, which registering would replace";
318
+ const url = registered?.url ?? null;
319
+ try {
320
+ await ctx.board.register({
321
+ url: null,
322
+ watch: {},
323
+ sync: [{
324
+ key,
325
+ platform: key
326
+ }]
327
+ });
328
+ } catch (error) {
329
+ if (error instanceof BoardError && error.status === 403) return "this token can't register a sync here";
330
+ throw error;
331
+ }
332
+ let imported;
333
+ try {
334
+ imported = await ctx.board.syncImport({
335
+ op: "post",
336
+ author,
337
+ from: key,
338
+ channel: ctx.channel,
339
+ text: "conformance: from a platform",
340
+ labels: ctx.labels,
341
+ noSync: true,
342
+ origin: {
343
+ channel: `${key}:${ctx.labels.conformance}`,
344
+ thread,
345
+ subthread: null,
346
+ message: "1",
347
+ context: null
348
+ },
349
+ id: `conformance-sync-${ctx.labels.conformance}`
350
+ });
351
+ } catch (error) {
352
+ await ctx.board.register({
353
+ url,
354
+ watch: {}
355
+ }).catch(() => {});
356
+ if (error instanceof BoardError && error.status === 403) return `this token can't sync in ${ctx.channel}`;
357
+ throw error;
358
+ }
359
+ const id = imported.message;
360
+ isRef(id, "an import's message");
361
+ try {
362
+ const message = (await ctx.board.thread(id)).message;
363
+ same(message.by.actor, author.id, "the import's author");
364
+ same(message.from, key, "the import's from");
365
+ same(message.origin?.thread, thread, "the import's origin");
366
+ const found = await ctx.board.syncFind({
367
+ thread,
368
+ message: "1"
369
+ });
370
+ same(found.message, id, "the message found by its platform IDs");
371
+ check(found.thread !== null, "the thread found by its platform thread");
372
+ same((await ctx.board.syncThread(found.thread)).root, id, "the synced thread's root");
373
+ same((await ctx.board.syncImport({
374
+ op: "post",
375
+ author,
376
+ from: key,
377
+ channel: ctx.channel,
378
+ text: "conformance: from a platform",
379
+ noSync: true,
380
+ id: `conformance-sync-${ctx.labels.conformance}`
381
+ })).message, id, "the same import, sent again");
382
+ await rejects(ctx.board.syncImport({
383
+ op: "post",
384
+ author,
385
+ from: key,
386
+ channel: ctx.channel,
387
+ text: "conformance: a second root",
388
+ noSync: true,
389
+ origin: {
390
+ thread,
391
+ subthread: null,
392
+ message: "3",
393
+ context: null
394
+ }
395
+ }), 409, "already_synced", "an import from a platform thread another board thread syncs with");
396
+ const external = {
397
+ thread: `${key}:${ctx.labels.conformance}:2`,
398
+ message: "2"
399
+ };
400
+ await ctx.board.syncReceipt({
401
+ kind: "thread",
402
+ message: id,
403
+ target: external.thread,
404
+ targetKind: "thread",
405
+ cause: "request"
406
+ });
407
+ await ctx.board.syncReceipt({
408
+ kind: "message",
409
+ message: id,
410
+ target: external.thread,
411
+ external
412
+ });
413
+ same(JSON.stringify((await ctx.board.syncCopies([id]))[id]?.[external.thread]), JSON.stringify(external), "the copy its receipt recorded");
414
+ } finally {
415
+ await ctx.board.syncImport({
416
+ op: "delete",
417
+ author,
418
+ from: key,
419
+ origin
420
+ }).catch(() => {});
421
+ await ctx.board.syncImport({
422
+ op: "delete",
423
+ author,
424
+ from: key,
425
+ origin: {
426
+ thread,
427
+ message: "3"
428
+ }
429
+ }).catch(() => {});
430
+ await ctx.board.register({
431
+ url,
432
+ watch: {}
433
+ }).catch(() => {});
434
+ }
435
+ }
436
+ }
437
+ ];
438
+ /** Runs the checks against `board`, in order, and reports each. It never throws for what the board answers. */
439
+ async function conformance(board, options = {}) {
440
+ const checks = [];
441
+ const discovery = await board.discovery().catch((error) => error);
442
+ const wrong = discovery instanceof Error ? discovery.message : typeof discovery.audience !== "string" || typeof discovery.url !== "string" || !Array.isArray(discovery.sync) ? `discovery should have url, audience, and sync, got ${show(discovery)}` : null;
443
+ if (wrong !== null) {
444
+ checks.push({
445
+ group: "core",
446
+ name: "says in its discovery where it is",
447
+ status: "failed",
448
+ detail: wrong
449
+ });
450
+ return {
451
+ url: board.url,
452
+ checks,
453
+ ok: false
454
+ };
455
+ }
456
+ const url = discovery.url;
457
+ checks.push({
458
+ group: "core",
459
+ name: "says in its discovery where it is",
460
+ status: "passed"
461
+ });
462
+ const me = await board.me();
463
+ const channel = options.channel ?? "conformance";
464
+ const ctx = {
465
+ board,
466
+ channel,
467
+ labels: { conformance: crypto.randomUUID().slice(0, 8) },
468
+ start: (await board.messages({
469
+ channel,
470
+ limit: 1
471
+ })).cursor,
472
+ me,
473
+ posted: []
474
+ };
475
+ try {
476
+ for (const { group, name, run } of CHECKS) try {
477
+ const skipped = await run(ctx);
478
+ checks.push(skipped ? {
479
+ group,
480
+ name,
481
+ status: "skipped",
482
+ detail: skipped
483
+ } : {
484
+ group,
485
+ name,
486
+ status: "passed"
487
+ });
488
+ } catch (error) {
489
+ if (OPTIONAL.has(group) && error instanceof BoardError && error.status === 501) checks.push({
490
+ group,
491
+ name,
492
+ status: "skipped",
493
+ detail: `not kept here: ${error.message}`
494
+ });
495
+ else checks.push({
496
+ group,
497
+ name,
498
+ status: "failed",
499
+ detail: error instanceof Error ? error.message : String(error)
500
+ });
501
+ }
502
+ } finally {
503
+ for (const id of [...ctx.posted].reverse()) await board.delete(id).catch(() => {});
504
+ }
505
+ return {
506
+ url,
507
+ checks,
508
+ ok: checks.every((c) => c.status !== "failed")
509
+ };
510
+ }
511
+ //#endregion
512
+ export { conformance as t };
@@ -0,0 +1,30 @@
1
+ import { m as Board } from "./a2-5EG8FkZ8.mjs";
2
+ //#region src/conformance/index.d.ts
3
+ /**
4
+ * Checks that a board speaks the protocol, through a handle on it (`conformance(board())`), as
5
+ * `vb` and agents use it: every board answers all of it. A board may keep no schedules or files,
6
+ * and says so with a 501, which the kit reports as not kept here. It writes to one channel
7
+ * (`conformance` by default), keeps every post on the board, and deletes what it posted.
8
+ */
9
+ /** What a check is about, for the report's headings. */
10
+ export type CheckGroup = 'core' | 'schedules' | 'attachments' | 'watchers' | 'sync';
11
+ export type CheckResult = {
12
+ group: CheckGroup;
13
+ name: string;
14
+ status: 'passed' | 'failed' | 'skipped';
15
+ /** Why it failed, or why it was skipped: `not kept here: …` for what the board answers 501 to. */
16
+ detail?: string;
17
+ };
18
+ export type ConformanceReport = {
19
+ url: string;
20
+ checks: CheckResult[];
21
+ /** Every check passed or was skipped. */
22
+ ok: boolean;
23
+ };
24
+ export type ConformanceOptions = {
25
+ /** Where it posts. Default `conformance`. */
26
+ channel?: string;
27
+ };
28
+ /** Runs the checks against `board`, in order, and reports each. It never throws for what the board answers. */
29
+ export declare function conformance(board: Board, options?: ConformanceOptions): Promise<ConformanceReport>;
30
+ //#endregion
@@ -0,0 +1,2 @@
1
+ import { t as conformance } from "./conformance-DRR23YNy.mjs";
2
+ export { conformance };