@bud.sh/app 0.5.0 → 0.6.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 (115) hide show
  1. package/README.md +41 -31
  2. package/dist/client/assets/bud/design.js +1 -1
  3. package/dist/client/assets/canvas-Bih9EdKJ.js +1 -0
  4. package/dist/client/assets/difference-worker-Bzg-rizy.js +8 -0
  5. package/dist/client/assets/dist-CdE5wBkg.js +5 -0
  6. package/dist/client/assets/files-1dLuoM2f.js +1 -0
  7. package/dist/client/assets/i18n-CkO6VFI0.js +1 -0
  8. package/dist/client/assets/index-Cn2mxroU.css +2 -0
  9. package/dist/client/assets/{index-ChANGtl-.js → index-DtISWZp9.js} +2 -2
  10. package/dist/client/assets/pages-BJ-tNJ4J.js +1 -0
  11. package/dist/client/assets/{project-branches-DO897zM6.js → project-branches-Ce3rXrkc.js} +1 -1
  12. package/dist/client/assets/project-comments-CPUONfs8.js +1 -0
  13. package/dist/client/assets/project-thread-links-C-TGkq59.js +1 -0
  14. package/dist/client/assets/{project-threads-Lay351p4.js → project-threads-Bv7IDBgi.js} +105 -105
  15. package/dist/client/assets/project-work-DAE9-olg.js +1 -0
  16. package/dist/client/assets/{task-design-Avb2RO4w.js → task-design-CwySIAOl.js} +3 -3
  17. package/dist/client/assets/tasks-KWsCLi_h.js +3 -0
  18. package/dist/client/assets/wasm-inline-DSUyFn1L.js +1 -0
  19. package/dist/client/assets/workspace-CVdcfaaB.js +2 -0
  20. package/dist/client/index.html +3 -3
  21. package/dist/hosted/assets/{canvas-V36732ER.js → canvas-AII16n9i.js} +97 -6
  22. package/dist/hosted/assets/{comment-data-CF2ZV_3k.js → comment-data-BXfMElfx.js} +2004 -402
  23. package/dist/hosted/assets/dist-BCSTeNf4.js +4785 -0
  24. package/dist/hosted/assets/{files-DjSCjfU7.js → files-D_1yeAVO.js} +10 -5
  25. package/dist/hosted/assets/{pages-BzjzeuPu.js → pages-CN_SbKWA.js} +3 -3
  26. package/dist/hosted/assets/{project-branches-BT97J9i7.js → project-branches-Bxp0DCwg.js} +1 -1
  27. package/dist/hosted/assets/{project-thread-links-BRLgByEe.js → project-thread-links-KqQoKBLE.js} +76 -4
  28. package/dist/{node-host/assets/screen-CZXWJEds.js → hosted/assets/screen-DpAcDUKR.js} +29 -1
  29. package/dist/hosted/assets/tasks-D66LlwxM.js +1423 -0
  30. package/dist/hosted/assets/wasm-inline-Dl9tqabx.js +4 -0
  31. package/dist/hosted/hosted.js +7885 -6219
  32. package/dist/node/assets/Stream-CAm1eJ2n.js +16604 -0
  33. package/dist/{server/assets/bridge-B_Blt55E.js → node/assets/bridge-DQSBYHmw.js} +124 -290
  34. package/dist/node/assets/config-9_jWwNBh.js +1067 -0
  35. package/dist/node/assets/load-config.server-CMVtRVmO.js +261 -0
  36. package/dist/node/assets/module-3GgtT4Xs.js +308 -0
  37. package/dist/node/assets/server-DTQFuoX2.js +846 -0
  38. package/dist/node/assets/src-D-kG1GxB.js +2 -0
  39. package/dist/node/assets/{src-CerrKjID.js → src-Dwe9APUx.js} +93 -3
  40. package/dist/node/assets/src-DysjfbM8.js +8592 -0
  41. package/dist/node/bridge.js +1 -845
  42. package/dist/node/cli.js +206 -0
  43. package/dist/node/design.js +811 -13
  44. package/dist/node/index.js +2 -739
  45. package/dist/node/plugins.js +5711 -0
  46. package/dist/node/server.js +1 -6746
  47. package/dist/node/vite.js +121 -0
  48. package/dist/{server/assets/canvas-Dc1mYCwQ.js → node-host/assets/canvas-BeDoufy1.js} +97 -7
  49. package/dist/node-host/assets/{comment-data-DyIZNpfG.js → comment-data-DE2T9yTj.js} +1980 -376
  50. package/dist/node-host/assets/dist-BCSTeNf4.js +4785 -0
  51. package/dist/node-host/assets/{files-Dh2TiQZq.js → files-Dl-PTS09.js} +11 -6
  52. package/dist/node-host/assets/{pages-BxpjbYhs.js → pages-DL2sk6_U.js} +3 -3
  53. package/dist/node-host/assets/{project-branches-B7HUGiVK.js → project-branches-DGp5ELct.js} +1 -1
  54. package/dist/{server/assets/project-thread-links-CBOpf6zk.js → node-host/assets/project-thread-links-CgBV3Jcm.js} +76 -5
  55. package/dist/{hosted/assets/screen-CKp2C7kx.js → node-host/assets/screen-B55yyVfb.js} +29 -1
  56. package/dist/node-host/assets/tasks-DZRNkoxe.js +1425 -0
  57. package/dist/node-host/assets/wasm-inline-Dl9tqabx.js +4 -0
  58. package/dist/node-host/node-host.js +6127 -4438
  59. package/dist/server/assets/{branch-previews-ClkoJ0lM.js → branch-previews-DedrpnVR.js} +1555 -673
  60. package/dist/{node-host/assets/canvas-TRUfxfL5.js → server/assets/canvas-CJWQ-jjv.js} +98 -6
  61. package/dist/server/assets/{comment-data-CsjukwAW.js → comment-data-Rg42ToTW.js} +1666 -47
  62. package/dist/server/assets/dist-BCSTeNf4.js +4785 -0
  63. package/dist/server/assets/{events-iHpLGr3k.js → events-BH_rAu7O.js} +155 -20
  64. package/dist/server/assets/{files-E9mSFeoc.js → files-CPJqKeMo.js} +12 -7
  65. package/dist/server/assets/{listener-DN5TyOjJ.js → listener-D61O6PzF.js} +708 -12
  66. package/dist/server/assets/{pages-BBBXyJWV.js → pages-KJY_Ba92.js} +4 -4
  67. package/dist/server/assets/{project-branches-B0YIK6Ur.js → project-branches-qG9ACZvo.js} +2 -2
  68. package/dist/{node-host/assets/project-thread-links-CauJF0Cz.js → server/assets/project-thread-links-CqK0lqCF.js} +77 -4
  69. package/dist/{node-host/assets/tasks-request-ZWmbc8fE.js → server/assets/project-work-DoIwxWMm.js} +87 -2
  70. package/dist/server/assets/run-BJ8r5jSh.js +572 -0
  71. package/dist/server/assets/{screen-BIQA2mr4.js → screen-CBrHPFug.js} +30 -2
  72. package/dist/server/assets/{server-DxK-UB_Z.js → server-Dkbp5stG.js} +1521 -793
  73. package/dist/server/assets/{services-BRrgXYlT.js → services-BTfig8A8.js} +234 -997
  74. package/dist/server/assets/tasks-Bgu5Lg9Y.js +1427 -0
  75. package/dist/server/assets/wasm-inline-Dl9tqabx.js +4 -0
  76. package/dist/server/capture.js +1 -2
  77. package/dist/server/migration.js +569 -12
  78. package/dist/server/runtime.js +7 -5
  79. package/dist/server/server.js +3 -3
  80. package/dist/types/assets/{config-DIC2cf_S.d.ts → config-CvcYmE2s.d.ts} +188 -50
  81. package/dist/types/assets/{contract-CAT-XFNq.d.ts → contract-B7yuuqSn.d.ts} +44 -2
  82. package/dist/types/assets/{items-CaVkByHk.d.ts → items-BQdaVksf.d.ts} +12 -1
  83. package/dist/types/assets/memory-state-BBW-FAVA.d.ts +236 -0
  84. package/dist/types/assets/picker-Be5Gx4WR.d.ts +9 -0
  85. package/dist/types/assets/{task-design-xJOhRNWh.d.ts → task-design-BDHKFHpp.d.ts} +3 -1
  86. package/dist/types/bridge.d.ts +1 -1
  87. package/dist/types/companion.d.ts +3 -3
  88. package/dist/types/design.d.ts +2 -2
  89. package/dist/types/hosted.d.ts +3 -3
  90. package/dist/types/index.d.ts +2 -2
  91. package/dist/types/node-host.d.ts +2 -2
  92. package/dist/types/plugins.d.ts +445 -0
  93. package/dist/types/screen.d.ts +2 -2
  94. package/dist/types/server.d.ts +2 -228
  95. package/dist/types/vite.d.ts +16 -0
  96. package/package.json +19 -3
  97. package/dist/client/assets/canvas-Cu6R6xxT.js +0 -1
  98. package/dist/client/assets/difference-worker-jfTSn-Tw.js +0 -8
  99. package/dist/client/assets/files-js6ElfkU.js +0 -1
  100. package/dist/client/assets/i18n-DxqFr2mi.js +0 -1
  101. package/dist/client/assets/index-CFFxP0gS.css +0 -2
  102. package/dist/client/assets/pages-6_lhiQmz.js +0 -1
  103. package/dist/client/assets/project-comments-DFFKVpia.js +0 -1
  104. package/dist/client/assets/project-thread-links-NLFWQRD2.js +0 -1
  105. package/dist/client/assets/project-work-du0Y-L5Y.js +0 -1
  106. package/dist/client/assets/tasks-CA-E8bbK.js +0 -1
  107. package/dist/client/assets/tasks-request-C55IeuiP.js +0 -1
  108. package/dist/client/assets/workspace-Cw5HvX5d.js +0 -2
  109. package/dist/hosted/assets/tasks-Dj7AyBJB.js +0 -649
  110. package/dist/hosted/assets/tasks-request-DZob623r.js +0 -113
  111. package/dist/node-host/assets/tasks-Bs1IOpeU.js +0 -651
  112. package/dist/server/assets/project-work-qSKFO5yj.js +0 -88
  113. package/dist/server/assets/tasks-CiXodsLN.js +0 -652
  114. package/dist/server/assets/tasks-request-DhwCzpRi.js +0 -115
  115. package/dist/types/assets/picker-D1Bw3ndx.d.ts +0 -14
@@ -0,0 +1,846 @@
1
+ import { A as parseWorkValue, B as statusCategory, C as parseWorkMutationResult, E as parseWorkSnapshot, F as parseMessage, G as record$1, H as identifier, I as parseMessageAuthor, K as strict, L as withoutMessages, M as workRunSummary, N as messageDeletionRefusal, P as messageDeletionRefusals, R as fieldByRole, S as parseWorkItemRef, T as parseWorkQuery, U as nonempty, V as WorkValidationError, W as parseWork, a as parseThreadDeleteInput, b as parseWorkDetail, c as parseThreadLinkInput, f as parseThreadStatusInput, g as parseWorkActionInput, h as parseSectionData, i as parseThreadCapabilities, j as valueByRole, k as parseWorkUpdate, l as parseThreadReplyInput, o as parseThreadFilter, r as parseThread, s as parseThreadInput, t as MAX_THREADS, u as parseThreadSnapshot, x as parseWorkItem, y as parseWorkCreate, z as parseWorkSchema } from "./src-Dwe9APUx.js";
2
+ import { F as offerUnsafe, Ht as sync, I as acquireRelease, Kt as try_, Ur as TaggedError, Yt as yieldNow, Zt as currentTimeMillis, _r as make, ft as gen, mr as add, n as callback, pn as effectContext, pr as Service } from "./Stream-CAm1eJ2n.js";
3
+ import { any } from "remix/data-schema";
4
+ import { isAbsolute, resolve } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ //#region ../work/src/server/errors.ts
7
+ var WorkErrorBase = TaggedError("WorkError");
8
+ /** Safe messages cross the HTTP boundary; diagnostics and credentials do not. */
9
+ var WorkError = class extends WorkErrorBase {};
10
+ //#endregion
11
+ //#region ../work/src/server/services.ts
12
+ var WorkAdapterBase = Service()("@bud.sh/work/WorkAdapter");
13
+ /** A backend's work items. Run programs at a request boundary with its AbortSignal. */
14
+ var WorkAdapter = class extends WorkAdapterBase {};
15
+ var ThreadAdapterBase = Service()("@bud.sh/work/ThreadAdapter");
16
+ /** Discussions receive trusted server attribution separately from browser input. */
17
+ var ThreadAdapter = class extends ThreadAdapterBase {};
18
+ var ThreadImportSchema = strict({
19
+ source: identifier,
20
+ sourceId: nonempty(512),
21
+ thread: any()
22
+ });
23
+ function history(value) {
24
+ let input = parseWork(record$1, value, "import.thread");
25
+ if (!Array.isArray(input.messages) || input.messages.length === 0 || input.messages.length > 67108864 || input.messagesLoaded !== true || input.messagesTruncated !== false || input.messageCount !== input.messages.length) throw new WorkValidationError("import.thread.messages");
26
+ let encoder = new TextEncoder();
27
+ let bytes = 0;
28
+ let messages = Array.from(input.messages, (value) => {
29
+ let message = parseMessage(value);
30
+ bytes += encoder.encode(JSON.stringify(message)).byteLength + 1;
31
+ if (bytes > 67108864) throw new WorkValidationError("import.thread.bytes");
32
+ return message;
33
+ });
34
+ let parents = new Map(messages.map((message) => [message.id, message.parentId]));
35
+ if (parents.size !== messages.length) throw new WorkValidationError("import.thread.messages");
36
+ let complete = /* @__PURE__ */ new Set();
37
+ for (let message of messages) {
38
+ let path = /* @__PURE__ */ new Set();
39
+ let id = message.id;
40
+ while (id !== null && !complete.has(id)) {
41
+ if (path.has(id) || !parents.has(id)) throw new WorkValidationError("import.thread.messages.parentId");
42
+ path.add(id);
43
+ id = parents.get(id);
44
+ }
45
+ for (let id of path) complete.add(id);
46
+ }
47
+ let thread = parseThread({
48
+ ...input,
49
+ messages: messages.slice(0, 200),
50
+ messagesTruncated: messages.length > 200
51
+ });
52
+ bytes += encoder.encode(JSON.stringify({
53
+ ...thread,
54
+ messages: []
55
+ })).byteLength;
56
+ if (bytes > 67108864) throw new WorkValidationError("import.thread.bytes");
57
+ return {
58
+ ...thread,
59
+ messages,
60
+ messagesTruncated: false
61
+ };
62
+ }
63
+ /**
64
+ * Validates complete source history before a server-side migration writes anything.
65
+ * @param value - The source identity and fully loaded discussion.
66
+ * @returns Complete, byte-bounded history, independent of the browser's message-count limit.
67
+ */
68
+ function parseThreadImport(value) {
69
+ let input = parseWork(ThreadImportSchema, value, "import");
70
+ return {
71
+ ...input,
72
+ thread: history(input.thread)
73
+ };
74
+ }
75
+ //#endregion
76
+ //#region ../work/src/server/memory-state.ts
77
+ var expectedRevision = nonempty(256);
78
+ var projectRoot = nonempty(4096);
79
+ /**
80
+ * Runs a parser over caller input, turning its failure into an `input` work error.
81
+ * @param parse - Parses the input.
82
+ * @returns The parsed input.
83
+ */
84
+ function input(parse) {
85
+ try {
86
+ return parse();
87
+ } catch (cause) {
88
+ if (cause instanceof WorkError) throw cause;
89
+ throw new WorkError({
90
+ reason: "input",
91
+ message: "Check the supplied values.",
92
+ cause
93
+ });
94
+ }
95
+ }
96
+ /**
97
+ * Runs a parser over stored data, turning its failure into a `storage` work error.
98
+ * @param parse - Parses the stored data.
99
+ * @returns The parsed data.
100
+ */
101
+ function output(parse) {
102
+ try {
103
+ return parse();
104
+ } catch (cause) {
105
+ if (cause instanceof WorkError) throw cause;
106
+ throw new WorkError({
107
+ reason: "storage",
108
+ message: "The adapter returned invalid data.",
109
+ cause
110
+ });
111
+ }
112
+ }
113
+ /**
114
+ * Throws a `refused` work error.
115
+ * @param message - The reason, safe to show.
116
+ */
117
+ function refused(message) {
118
+ throw new WorkError({
119
+ reason: "refused",
120
+ message
121
+ });
122
+ }
123
+ /**
124
+ * Throws a `conflict` work error unless a record is still at the revision the caller saw.
125
+ * @param current - The stored record.
126
+ * @param revision - The caller's expected revision.
127
+ */
128
+ function expected(current, revision) {
129
+ input(() => parseWork(expectedRevision, revision, "expectedRevision"));
130
+ if (current.revision !== revision) throw new WorkError({
131
+ reason: "conflict",
132
+ message: "This record changed. Refresh and review your changes before saving.",
133
+ currentRevision: current.revision
134
+ });
135
+ }
136
+ /**
137
+ * A loaded section, cut to its limit.
138
+ * @param items - The section's entries.
139
+ * @param max - The most entries to return.
140
+ * @param count - How many entries exist in all.
141
+ * @returns The available section, truncated when more exist than it holds.
142
+ */
143
+ function available(items, max, count = items.length) {
144
+ return {
145
+ state: "available",
146
+ items: items.slice(0, max),
147
+ truncated: count > max
148
+ };
149
+ }
150
+ /**
151
+ * A section the store does not provide.
152
+ * @returns The unsupported section.
153
+ */
154
+ function unsupported() {
155
+ return {
156
+ state: "unsupported",
157
+ items: [],
158
+ truncated: false
159
+ };
160
+ }
161
+ /** The items, discussions and watchers behind the in-memory work and thread adapters. */
162
+ var MemoryStore = class {
163
+ identity = randomUUID();
164
+ schema;
165
+ author;
166
+ defaults;
167
+ capabilities;
168
+ options;
169
+ workspaces = /* @__PURE__ */ new Map();
170
+ #watchers = /* @__PURE__ */ new Map();
171
+ seed;
172
+ constructor(options) {
173
+ this.schema = parseWorkSchema(options.schema);
174
+ this.author = parseMessageAuthor(options.author ?? {
175
+ id: "memory",
176
+ name: "Memory adapter",
177
+ email: ""
178
+ });
179
+ this.defaults = Object.fromEntries(Object.entries(options.defaults ?? {}).map(([key, value]) => {
180
+ let field = this.schema.fields.find((field) => field.id === key);
181
+ if (!field) refused("A default refers to an undeclared field.");
182
+ return [key, parseWorkValue(value, field, this.schema)];
183
+ }));
184
+ this.options = {
185
+ ...options,
186
+ actions: { ...options.actions },
187
+ sections: { ...options.sections }
188
+ };
189
+ this.capabilities = parseThreadCapabilities({
190
+ create: this.schema.capabilities.threads,
191
+ reply: this.schema.capabilities.threads,
192
+ status: this.schema.capabilities.threads,
193
+ link: this.schema.capabilities.threads,
194
+ delete: this.schema.capabilities.threads,
195
+ ...options.threadCapabilities,
196
+ sync: false,
197
+ edit: false
198
+ });
199
+ if (this.schema.capabilities.sync) refused("Memory storage has no remote synchronization.");
200
+ for (let action of this.schema.actions) if (action.kind === "custom" && !this.options.actions?.[action.id]) refused("Declare an implementation for each custom memory action.");
201
+ for (let id of Object.keys(this.options.actions ?? {})) if (!this.schema.actions.some((action) => action.id === id)) refused("An action implementation has no declaration.");
202
+ for (let id of Object.keys(this.options.sections ?? {})) if (!this.schema.sections.some((section) => section.id === id)) refused("A section implementation has no declaration.");
203
+ this.seed = {
204
+ sequence: 0,
205
+ items: /* @__PURE__ */ new Map(),
206
+ threads: /* @__PURE__ */ new Map()
207
+ };
208
+ for (let value of options.items ?? []) {
209
+ let item = parseWorkItem(value, this.schema);
210
+ if (!item.contentLoaded || !item.revision || this.seed.items.has(item.id)) refused("Seed work items need complete content and unique identities with revisions.");
211
+ this.seed.items.set(item.id, {
212
+ item,
213
+ activity: [],
214
+ activityCount: 0,
215
+ history: [],
216
+ historyCount: 0
217
+ });
218
+ }
219
+ for (let value of options.threads ?? []) {
220
+ let thread = parseThread(value);
221
+ if (!thread.messagesLoaded || thread.messagesTruncated || !thread.revision || this.seed.threads.has(thread.id)) refused("Seed threads need complete messages and unique identities with revisions.");
222
+ for (let id of thread.itemIds) this.item(this.seed, id);
223
+ this.seed.threads.set(thread.id, thread);
224
+ }
225
+ for (let row of this.seed.items.values()) for (let relation of row.item.relations) this.item(this.seed, relation.item.id);
226
+ }
227
+ /**
228
+ * Signals every write to one workspace while the stream runs.
229
+ * @param root - The project directory.
230
+ * @returns One element per write.
231
+ */
232
+ watch(root) {
233
+ return callback((queue) => acquireRelease(try_({
234
+ try: () => {
235
+ let key = input(() => resolve(root));
236
+ let notify = () => void offerUnsafe(queue, void 0);
237
+ let watchers = this.#watchers.get(key) ?? /* @__PURE__ */ new Set();
238
+ this.#watchers.set(key, watchers.add(notify));
239
+ return () => watchers.delete(notify);
240
+ },
241
+ catch: (cause) => cause instanceof WorkError ? cause : new WorkError({
242
+ reason: "input",
243
+ message: "Invalid project directory.",
244
+ cause
245
+ })
246
+ }), (stop) => sync(stop)));
247
+ }
248
+ run(root, write, operation) {
249
+ return gen({ self: this }, function* () {
250
+ yield* yieldNow;
251
+ let now = yield* currentTimeMillis;
252
+ return yield* try_({
253
+ try: () => {
254
+ let key = input(() => {
255
+ parseWork(projectRoot, root, "root");
256
+ if (!isAbsolute(root)) throw new TypeError("The project directory must be absolute.");
257
+ return resolve(root);
258
+ });
259
+ let current = this.workspaces.get(key) ?? structuredClone(this.seed);
260
+ let data = write ? structuredClone(current) : current;
261
+ let result = structuredClone(operation(data, new Date(now).toISOString()));
262
+ if (write) {
263
+ this.workspaces.set(key, data);
264
+ for (let notify of [...this.#watchers.get(key) ?? []]) notify();
265
+ }
266
+ return result;
267
+ },
268
+ catch: (cause) => cause instanceof WorkError ? cause : new WorkError({
269
+ reason: "storage",
270
+ message: "Memory storage failed.",
271
+ cause
272
+ })
273
+ });
274
+ });
275
+ }
276
+ next(data, prefix) {
277
+ return `${prefix}-${this.identity}-${++data.sequence}`;
278
+ }
279
+ item(data, id) {
280
+ input(() => parseWork(identifier, id, "id"));
281
+ let item = data.items.get(id);
282
+ if (!item) throw new WorkError({
283
+ reason: "not-found",
284
+ message: "This work item does not exist."
285
+ });
286
+ return item;
287
+ }
288
+ thread(data, id) {
289
+ input(() => parseWork(identifier, id, "id"));
290
+ let thread = data.threads.get(id);
291
+ if (!thread) throw new WorkError({
292
+ reason: "not-found",
293
+ message: "This discussion does not exist."
294
+ });
295
+ return thread;
296
+ }
297
+ done(item) {
298
+ let status = valueByRole(item, this.schema, "status");
299
+ return typeof status === "string" && ["done", "cancelled"].includes(statusCategory(this.schema, status) ?? "");
300
+ }
301
+ view(data, id) {
302
+ let item = this.item(data, id).item;
303
+ let children = [...data.items.values()].filter((row) => row.item.relations.some((relation) => relation.direction === "out" && relation.item.id === id && this.schema.relations.find((kind) => kind.type === relation.type)?.kind === "parent"));
304
+ let discussion = [...data.threads.values()].filter((thread) => thread.itemIds.includes(id)).reduce((count, thread) => count + thread.messageCount, 0);
305
+ let blocked = item.relations.some((relation) => relation.direction === "out" && this.schema.relations.find((kind) => kind.type === relation.type)?.kind === "blocks" && !this.done(this.item(data, relation.item.id).item));
306
+ return output(() => parseWorkItem({
307
+ ...item,
308
+ counts: {
309
+ ...this.schema.capabilities.discussion ? { discussion } : {},
310
+ children: children.length,
311
+ childrenDone: children.filter((row) => this.done(row.item)).length
312
+ },
313
+ ...this.schema.capabilities.ready ? { ready: !this.done(item) && !blocked } : {}
314
+ }, this.schema));
315
+ }
316
+ save(data, value, at, summary) {
317
+ let prior = data.items.get(value.id);
318
+ let revision = this.next(data, "revision");
319
+ let values = { ...value.values };
320
+ for (let [role, entry] of [
321
+ ["identifier", value.id],
322
+ ["revision", revision],
323
+ ["updated", at],
324
+ ...!prior ? [["created", at]] : []
325
+ ]) {
326
+ let field = this.schema.fields.find((field) => field.role === role);
327
+ if (field) values[field.id] = entry;
328
+ }
329
+ let item = output(() => parseWorkItem({
330
+ ...value,
331
+ revision,
332
+ values
333
+ }, this.schema));
334
+ let row = prior ?? {
335
+ item,
336
+ history: [],
337
+ historyCount: 0,
338
+ activity: [],
339
+ activityCount: 0
340
+ };
341
+ row.item = item;
342
+ data.items.set(item.id, row);
343
+ item = this.view(data, item.id);
344
+ row.item = item;
345
+ row.history.unshift({
346
+ revision,
347
+ author: this.author.name,
348
+ at,
349
+ item: structuredClone(item)
350
+ });
351
+ row.history = row.history.slice(0, 50);
352
+ row.historyCount++;
353
+ row.activity.unshift({
354
+ id: this.next(data, "activity"),
355
+ actor: this.author.name,
356
+ at,
357
+ verb: prior ? "changed" : "created",
358
+ summary
359
+ });
360
+ row.activity = row.activity.slice(0, 200);
361
+ row.activityCount++;
362
+ return item;
363
+ }
364
+ touchThreads(data, ids, at) {
365
+ for (let id of new Set(ids)) this.save(data, this.item(data, id).item, at, "Discussion changed");
366
+ }
367
+ initialValues(id, at, supplied) {
368
+ let values = {
369
+ ...this.defaults,
370
+ ...supplied
371
+ };
372
+ let status = fieldByRole(this.schema, "status");
373
+ if (status && values[status.id] === void 0 && this.schema.statuses.length) values[status.id] = this.schema.statuses[0].value;
374
+ for (let role of [
375
+ "identifier",
376
+ "created",
377
+ "updated"
378
+ ]) {
379
+ let field = fieldByRole(this.schema, role);
380
+ if (field) values[field.id] = role === "identifier" ? id : at;
381
+ }
382
+ return values;
383
+ }
384
+ };
385
+ //#endregion
386
+ //#region ../work/src/server/memory-threads.ts
387
+ /**
388
+ * The thread adapter over an in-memory store.
389
+ * @param store - The store shared with the work adapter.
390
+ * @returns The `ThreadAdapter` implementation.
391
+ */
392
+ function memoryThreads(store) {
393
+ function view(thread, brief = false) {
394
+ let limit = brief ? 1 : 200;
395
+ return output(() => parseThread({
396
+ ...thread,
397
+ messages: thread.messages.slice(0, limit),
398
+ messagesLoaded: !brief,
399
+ messagesTruncated: !brief && thread.messages.length > limit
400
+ }));
401
+ }
402
+ function save(data, thread, at, oldIds = []) {
403
+ let changed = {
404
+ ...thread,
405
+ revision: store.next(data, "thread-revision"),
406
+ updatedAt: new Date(Math.max(Date.parse(at), Date.parse(thread.updatedAt))).toISOString(),
407
+ messageCount: thread.messages.length
408
+ };
409
+ let result = view(changed);
410
+ data.threads.set(changed.id, changed);
411
+ store.touchThreads(data, [...oldIds, ...changed.itemIds], at);
412
+ return result;
413
+ }
414
+ function snapshot(data, filter) {
415
+ let threads = [...data.threads.values()].filter((thread) => {
416
+ if (filter.itemId && !thread.itemIds.includes(filter.itemId)) return false;
417
+ let kind = thread.anchor ? "type" in thread.anchor ? "code" : "canvas" : null;
418
+ if (filter.anchorKind && filter.anchorKind !== kind) return false;
419
+ return !filter.pagePath || thread.anchor && !("type" in thread.anchor) && thread.anchor.pagePath === filter.pagePath;
420
+ });
421
+ return output(() => parseThreadSnapshot({
422
+ connection: {
423
+ state: "ready",
424
+ adapter: store.schema.adapter
425
+ },
426
+ capabilities: store.capabilities,
427
+ threads: threads.slice(0, MAX_THREADS).map((thread) => view(thread, true)),
428
+ truncated: threads.length > MAX_THREADS
429
+ }));
430
+ }
431
+ function link(root, id, ids, expectedRevision, remove) {
432
+ return store.run(root, true, (data, at) => {
433
+ if (!store.capabilities.link) refused("This adapter does not allow linking discussions.");
434
+ let request = input(() => parseThreadLinkInput({
435
+ itemIds: ids,
436
+ expectedRevision
437
+ }));
438
+ let thread = store.thread(data, id);
439
+ expected(thread, request.expectedRevision);
440
+ for (let id of request.itemIds) store.item(data, id);
441
+ let itemIds = remove ? thread.itemIds.filter((id) => !request.itemIds.includes(id)) : [.../* @__PURE__ */ new Set([...thread.itemIds, ...request.itemIds])];
442
+ if (itemIds.length > 100) refused("This discussion has too many linked work items.");
443
+ if (!thread.anchor && !itemIds.length) refused("An unanchored discussion needs a linked work item.");
444
+ return save(data, {
445
+ ...thread,
446
+ itemIds
447
+ }, at, thread.itemIds);
448
+ });
449
+ }
450
+ return {
451
+ watch: (root) => store.watch(root),
452
+ list: (root, value) => store.run(root, false, (data) => snapshot(data, input(() => parseThreadFilter(value)))),
453
+ get: (root, id) => store.run(root, false, (data) => view(store.thread(data, id))),
454
+ create: (root, value, author) => store.run(root, true, (data, at) => {
455
+ if (!store.capabilities.create) refused("This adapter does not allow creating discussions.");
456
+ let request = input(() => parseThreadInput(value));
457
+ let trusted = input(() => parseMessageAuthor(author));
458
+ for (let id of request.itemIds) store.item(data, id);
459
+ return save(data, {
460
+ id: store.next(data, "thread"),
461
+ revision: null,
462
+ status: "open",
463
+ anchor: request.anchor,
464
+ itemIds: request.itemIds,
465
+ messages: [{
466
+ id: store.next(data, "message"),
467
+ body: request.body,
468
+ author: trusted,
469
+ createdAt: at,
470
+ parentId: null
471
+ }],
472
+ messageCount: 1,
473
+ messagesLoaded: true,
474
+ messagesTruncated: false,
475
+ createdAt: at,
476
+ updatedAt: at
477
+ }, at);
478
+ }),
479
+ reply: (root, id, value, author) => store.run(root, true, (data, at) => {
480
+ if (!store.capabilities.reply) refused("This adapter does not allow replying to discussions.");
481
+ let request = input(() => parseThreadReplyInput(value));
482
+ let trusted = input(() => parseMessageAuthor(author));
483
+ let thread = store.thread(data, id);
484
+ expected(thread, request.expectedRevision);
485
+ if (request.parentId && !thread.messages.some((message) => message.id === request.parentId)) refused("The reply parent must belong to this discussion.");
486
+ return save(data, {
487
+ ...thread,
488
+ messages: [...thread.messages, {
489
+ id: store.next(data, "message"),
490
+ body: request.body,
491
+ author: trusted,
492
+ createdAt: at,
493
+ parentId: request.parentId
494
+ }]
495
+ }, at);
496
+ }),
497
+ setStatus: (root, id, status, expectedRevision) => store.run(root, true, (data, at) => {
498
+ if (!store.capabilities.status) refused("This adapter does not allow changing discussion status.");
499
+ let request = input(() => parseThreadStatusInput({
500
+ status,
501
+ expectedRevision
502
+ }));
503
+ let thread = store.thread(data, id);
504
+ expected(thread, request.expectedRevision);
505
+ return save(data, {
506
+ ...thread,
507
+ status: request.status
508
+ }, at);
509
+ }),
510
+ deleteMessage: (root, id, value, author) => store.run(root, true, (data, at) => {
511
+ if (!store.capabilities.delete) refused("This adapter does not allow deleting messages.");
512
+ let request = input(() => parseThreadDeleteInput(value));
513
+ let trusted = input(() => parseMessageAuthor(author));
514
+ let thread = store.thread(data, id);
515
+ expected(thread, request.expectedRevision);
516
+ let refusal = messageDeletionRefusal(thread.messages, request.messageId, trusted.id);
517
+ if (refusal === "missing") throw new WorkError({
518
+ reason: "not-found",
519
+ message: messageDeletionRefusals.missing
520
+ });
521
+ if (refusal) refused(messageDeletionRefusals[refusal]);
522
+ if (thread.messages[0].id === request.messageId) {
523
+ data.threads.delete(id);
524
+ store.touchThreads(data, thread.itemIds, at);
525
+ return null;
526
+ }
527
+ return save(data, {
528
+ ...thread,
529
+ messages: withoutMessages(thread.messages, [request.messageId])
530
+ }, at);
531
+ }),
532
+ link: (root, id, ids, revision) => link(root, id, ids, revision, false),
533
+ unlink: (root, id, ids, revision) => link(root, id, ids, revision, true)
534
+ };
535
+ }
536
+ //#endregion
537
+ //#region ../work/src/server/memory-actions.ts
538
+ /**
539
+ * Computes what a declared action changes on an item, using its custom implementation or the
540
+ * built-in status, claim and relation behaviour.
541
+ * @param store - The memory store, with its schema, author and action implementations.
542
+ * @param item - The item the action runs on.
543
+ * @param action - The action's declaration.
544
+ * @param values - The action's validated input.
545
+ * @param at - When the action runs, for close timestamps.
546
+ * @returns The new values or outgoing relations.
547
+ */
548
+ function actionChange(store, item, action, values, at) {
549
+ let custom = store.options.actions?.[action.id];
550
+ if (custom) return custom(structuredClone({
551
+ item,
552
+ input: values,
553
+ schema: store.schema,
554
+ author: store.author
555
+ }));
556
+ let patch = {};
557
+ let status = fieldByRole(store.schema, "status");
558
+ if (action.kind === "close" || action.kind === "reopen" || action.kind === "transition") {
559
+ if (!status) refused("This schema has no status field.");
560
+ let destination = action.kind === "transition" ? values[action.input?.find((field) => field.role === "status" || field.kind === "status")?.id ?? ""] : store.schema.statuses.find((status) => action.kind === "close" ? status.category === "done" : ["unstarted", "backlog"].includes(status.category))?.value;
561
+ if (typeof destination !== "string" || !store.schema.statuses.some((status) => status.value === destination)) refused("This action has no valid destination status.");
562
+ patch[status.id] = destination;
563
+ let closed = fieldByRole(store.schema, "closed");
564
+ if (closed && action.kind === "close") patch[closed.id] = at;
565
+ if (closed && !closed.required && action.kind === "reopen") patch[closed.id] = null;
566
+ let reason = fieldByRole(store.schema, "closeReason");
567
+ let reasonInput = action.input?.find((field) => field.role === "closeReason");
568
+ if (reason && reasonInput && values[reasonInput.id] !== void 0) patch[reason.id] = values[reasonInput.id];
569
+ return { values: patch };
570
+ }
571
+ if (action.kind === "claim" || action.kind === "unclaim") {
572
+ let assignee = fieldByRole(store.schema, "assignee");
573
+ if (!assignee) refused("This schema has no assignee field.");
574
+ let current = item.values[assignee.id];
575
+ if (action.kind === "claim" && current !== null && current !== void 0 && current !== "" && !(Array.isArray(current) && !current.length) && !(typeof current === "string" && current === store.author.name) && !(typeof current === "object" && !Array.isArray(current) && current.id === store.author.id)) refused("This work item is already assigned.");
576
+ let person = {
577
+ id: store.author.id,
578
+ name: store.author.name,
579
+ email: store.author.email
580
+ };
581
+ let claimed = action.kind === "claim";
582
+ if (assignee.kind === "person") patch[assignee.id] = claimed ? person : null;
583
+ else if (assignee.kind === "people") patch[assignee.id] = claimed ? [person] : [];
584
+ else if (assignee.kind === "text") patch[assignee.id] = claimed ? person.name : "";
585
+ else refused("This assignee field cannot represent a claim.");
586
+ return { values: patch };
587
+ }
588
+ if (action.kind === "relate" || action.kind === "unrelate") {
589
+ let targetField = action.input?.find((field) => field.kind === "item");
590
+ if (!targetField) refused("A relation action requires an item input.");
591
+ let target = input(() => parseWorkItemRef(values[targetField.id]));
592
+ let editable = store.schema.relations.filter((relation) => relation.editable);
593
+ let relationField = action.input?.find((field) => field.kind === "enum");
594
+ let type = relationField ? values[relationField.id] : editable.length === 1 ? editable[0].type : void 0;
595
+ if (typeof type !== "string" || !editable.some((relation) => relation.type === type)) refused("Select an editable relation type.");
596
+ let remaining = item.relations.filter((relation) => !(relation.direction === "out" && relation.type === type && relation.item.id === target.id));
597
+ return { relations: action.kind === "unrelate" ? remaining : [...remaining, {
598
+ type,
599
+ direction: "out",
600
+ item: target
601
+ }] };
602
+ }
603
+ refused("This action needs an implementation.");
604
+ }
605
+ function reference(store, item) {
606
+ let title = valueByRole(item, store.schema, "title");
607
+ let status = valueByRole(item, store.schema, "status");
608
+ return {
609
+ id: item.id,
610
+ ...typeof title === "string" ? { title: title.slice(0, 1e3) } : {},
611
+ ...typeof status === "string" ? { status } : {}
612
+ };
613
+ }
614
+ /**
615
+ * Rebuilds every item's incoming relations and link summaries from the outgoing ones, refusing
616
+ * self-links and parent or blocking cycles, and saves the items that changed.
617
+ * @param store - The memory store.
618
+ * @param data - The project's items.
619
+ * @param at - When the change happens.
620
+ */
621
+ function reconcileRelations(store, data, at) {
622
+ let incoming = /* @__PURE__ */ new Map();
623
+ for (let row of data.items.values()) for (let relation of row.item.relations.filter((relation) => relation.direction === "out")) {
624
+ store.item(data, relation.item.id);
625
+ if (relation.item.id === row.item.id) refused("A work item cannot relate to itself.");
626
+ let relations = incoming.get(relation.item.id) ?? [];
627
+ relations.push({
628
+ type: relation.type,
629
+ direction: "in",
630
+ item: reference(store, row.item)
631
+ });
632
+ incoming.set(relation.item.id, relations);
633
+ let kind = store.schema.relations.find((kind) => kind.type === relation.type)?.kind;
634
+ if (kind !== "parent" && kind !== "blocks") continue;
635
+ let pending = [relation.item.id];
636
+ let seen = /* @__PURE__ */ new Set();
637
+ while (pending.length) {
638
+ let id = pending.pop();
639
+ if (id === row.item.id) refused("This relationship would create a cycle.");
640
+ if (seen.has(id)) continue;
641
+ seen.add(id);
642
+ for (let edge of store.item(data, id).item.relations) if (edge.direction === "out" && edge.type === relation.type) pending.push(edge.item.id);
643
+ }
644
+ }
645
+ for (let row of data.items.values()) {
646
+ let relations = [...row.item.relations.filter((relation) => relation.direction === "out").map((relation) => ({
647
+ ...relation,
648
+ item: reference(store, store.item(data, relation.item.id).item)
649
+ })), ...incoming.get(row.item.id) ?? []];
650
+ if (JSON.stringify(row.item.relations) !== JSON.stringify(relations)) store.save(data, {
651
+ ...row.item,
652
+ relations
653
+ }, at, "Relationships changed");
654
+ }
655
+ }
656
+ //#endregion
657
+ //#region ../work/src/server/memory-work.ts
658
+ var actionResult = strict({
659
+ values: any(),
660
+ relations: any()
661
+ });
662
+ /**
663
+ * The work adapter over an in-memory store.
664
+ * @param store - The store shared with the thread adapter.
665
+ * @returns The `WorkAdapter` implementation.
666
+ */
667
+ function memoryWork(store) {
668
+ let schema = store.schema;
669
+ let connection = {
670
+ state: "ready",
671
+ adapter: schema.adapter
672
+ };
673
+ function detail(data, id) {
674
+ let row = store.item(data, id);
675
+ let messages = [...data.threads.values()].filter((thread) => thread.itemIds.includes(id)).flatMap((thread) => thread.messages);
676
+ return output(() => parseWorkDetail({
677
+ connection,
678
+ item: store.view(data, id),
679
+ discussion: schema.capabilities.discussion ? available(messages, 200) : unsupported(),
680
+ history: schema.capabilities.history ? available(row.history, 50, row.historyCount) : unsupported(),
681
+ activity: schema.capabilities.activity ? available(row.activity, 200, row.activityCount) : unsupported()
682
+ }, schema));
683
+ }
684
+ function result(data, id) {
685
+ return output(() => parseWorkMutationResult({ item: store.view(data, id) }, schema));
686
+ }
687
+ return {
688
+ watch: (root) => store.watch(root),
689
+ describe: (root) => store.run(root, false, () => connection),
690
+ schema: (root) => store.run(root, false, () => schema),
691
+ list: (root, value) => store.run(root, false, (data) => {
692
+ let query = input(() => parseWorkQuery(value));
693
+ let search = query.search?.trim().toLowerCase();
694
+ let rows = [...data.items.values()].map((row) => row.item).filter((item) => {
695
+ if (query.status && valueByRole(item, schema, "status") !== query.status) return false;
696
+ return !search || [item.id, ...Object.values(item.values).filter((value) => typeof value === "string")].some((value) => value.toLowerCase().includes(search));
697
+ });
698
+ let items = rows.slice(0, query.limit).map((row) => {
699
+ let item = store.view(data, row.id);
700
+ return {
701
+ ...item,
702
+ values: Object.fromEntries(Object.entries(item.values).filter(([key]) => schema.fields.find((field) => field.id === key)?.kind !== "markdown")),
703
+ contentLoaded: false
704
+ };
705
+ });
706
+ return output(() => parseWorkSnapshot({
707
+ connection,
708
+ items,
709
+ truncated: rows.length > query.limit,
710
+ schemaVersion: schema.adapter.version
711
+ }, schema));
712
+ }),
713
+ get: (root, id) => store.run(root, false, (data) => detail(data, id)),
714
+ section: (root, id, sectionId) => store.run(root, false, (data) => {
715
+ input(() => parseWork(identifier, sectionId, "sectionId"));
716
+ let item = store.view(data, id);
717
+ let declared = schema.sections.find((section) => section.id === sectionId);
718
+ if (!declared) return {
719
+ kind: "custom",
720
+ data: unsupported()
721
+ };
722
+ let custom = store.options.sections?.[sectionId];
723
+ let result;
724
+ if (custom) result = custom(structuredClone(item));
725
+ else if (declared.kind === "activity") result = {
726
+ kind: "activity",
727
+ data: schema.capabilities.activity ? available(store.item(data, id).activity, 200, store.item(data, id).activityCount) : unsupported()
728
+ };
729
+ else if (declared.kind === "history") result = {
730
+ kind: "history",
731
+ data: schema.capabilities.history ? available(store.item(data, id).history, 50, store.item(data, id).historyCount) : unsupported()
732
+ };
733
+ else if (declared.kind === "metadata") result = {
734
+ kind: "metadata",
735
+ data: available([item.extra], 1)
736
+ };
737
+ else result = {
738
+ kind: declared.kind,
739
+ data: unsupported()
740
+ };
741
+ return output(() => {
742
+ if (result.kind !== declared.kind) throw new TypeError("The section renderer changed.");
743
+ return parseSectionData(result, schema);
744
+ });
745
+ }),
746
+ create: (root, value) => store.run(root, true, (data, at) => {
747
+ if (!schema.capabilities.create) refused("This adapter does not allow creating work items.");
748
+ let request = input(() => parseWorkCreate(value, schema));
749
+ let id = store.next(data, "work");
750
+ store.save(data, {
751
+ id,
752
+ revision: null,
753
+ values: store.initialValues(id, at, request.values),
754
+ relations: [],
755
+ counts: {},
756
+ contentLoaded: true,
757
+ extra: null
758
+ }, at, "Work item created");
759
+ return result(data, id);
760
+ }),
761
+ readItemData: (root, id, key) => store.run(root, false, (data) => {
762
+ let row = store.item(data, id);
763
+ return {
764
+ value: row.data?.[key] ?? null,
765
+ revision: row.item.revision
766
+ };
767
+ }),
768
+ writeItemData: (root, id, key, value, expectedRevision) => store.run(root, true, (data, at) => {
769
+ let row = store.item(data, id);
770
+ expected(row.item, expectedRevision);
771
+ row.data = {
772
+ ...row.data,
773
+ [key]: value
774
+ };
775
+ let { run: _run, ...rest } = row.item;
776
+ let run = key === "budRun" ? workRunSummary(value) : row.item.run;
777
+ return { revision: store.save(data, run ? {
778
+ ...rest,
779
+ run
780
+ } : rest, at, "Bud record updated").revision };
781
+ }),
782
+ update: (root, id, patch, expectedRevision) => store.run(root, true, (data, at) => {
783
+ if (!schema.capabilities.update) refused("This adapter does not allow editing work items.");
784
+ let request = input(() => parseWorkUpdate({
785
+ values: patch,
786
+ expectedRevision
787
+ }, schema));
788
+ let item = store.item(data, id).item;
789
+ expected(item, request.expectedRevision);
790
+ store.save(data, {
791
+ ...item,
792
+ values: {
793
+ ...item.values,
794
+ ...request.values
795
+ }
796
+ }, at, "Work item updated");
797
+ reconcileRelations(store, data, at);
798
+ return result(data, id);
799
+ }),
800
+ act: (root, id, actionId, values, expectedRevision) => store.run(root, true, (data, at) => {
801
+ let request = input(() => parseWorkActionInput({
802
+ action: actionId,
803
+ input: values,
804
+ expectedRevision
805
+ }, schema));
806
+ let item = store.item(data, id).item;
807
+ expected(item, request.expectedRevision);
808
+ let action = schema.actions.find((action) => action.id === request.action);
809
+ let changed = actionChange(store, item, action, request.input, at);
810
+ output(() => parseWork(actionResult, changed, "actionResult"));
811
+ let next = {
812
+ ...item,
813
+ values: {
814
+ ...item.values,
815
+ ...changed.values
816
+ },
817
+ relations: changed.relations ?? item.relations
818
+ };
819
+ store.save(data, next, at, action.label);
820
+ reconcileRelations(store, data, at);
821
+ return result(data, id);
822
+ })
823
+ };
824
+ }
825
+ //#endregion
826
+ //#region ../work/src/server/memory.ts
827
+ /**
828
+ * Creates isolated work and discussion services for each layer and project root.
829
+ * @param options - The schema, complete seed records and trusted behavior for this adapter.
830
+ * @returns A layer with atomic in-memory writes and no filesystem or process requirements.
831
+ */
832
+ function memoryAdapterLayer(options) {
833
+ return effectContext(try_({
834
+ try: () => {
835
+ let store = new MemoryStore(options);
836
+ return make(WorkAdapter, memoryWork(store)).pipe(add(ThreadAdapter, memoryThreads(store)));
837
+ },
838
+ catch: (cause) => new WorkError({
839
+ reason: "input",
840
+ message: "The memory adapter configuration is invalid.",
841
+ cause
842
+ })
843
+ }));
844
+ }
845
+ //#endregion
846
+ export { WorkError as a, WorkAdapter as i, parseThreadImport as n, ThreadAdapter as r, memoryAdapterLayer as t };