dxcomplete 0.2.0 → 0.2.2

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 (47) hide show
  1. package/.env.example +0 -7
  2. package/README.md +18 -54
  3. package/dist/cli.js +0 -22
  4. package/dist/validate.js +10 -26
  5. package/docs/model.md +3 -3
  6. package/docs/taxonomy.md +1 -1
  7. package/package.json +23 -23
  8. package/templates/process/README.md +1 -1
  9. package/dist/http/service.d.ts +0 -7
  10. package/dist/http/service.js +0 -725
  11. package/dist/mcp/docs.d.ts +0 -114
  12. package/dist/mcp/docs.js +0 -626
  13. package/dist/mcp/server.d.ts +0 -20
  14. package/dist/mcp/server.js +0 -3059
  15. package/dist/runtime/auth.d.ts +0 -162
  16. package/dist/runtime/auth.js +0 -394
  17. package/dist/runtime/check.d.ts +0 -7
  18. package/dist/runtime/check.js +0 -16
  19. package/dist/runtime/config.d.ts +0 -17
  20. package/dist/runtime/config.js +0 -93
  21. package/dist/runtime/mongo.d.ts +0 -9
  22. package/dist/runtime/mongo.js +0 -56
  23. package/dist/runtime/records.d.ts +0 -427
  24. package/dist/runtime/records.js +0 -2092
  25. package/scripts/check-env-surface.mjs +0 -136
  26. package/scripts/check-public-copy.mjs +0 -263
  27. package/scripts/check-service-boundary.mjs +0 -63
  28. package/scripts/dogfood-work-order.mjs +0 -506
  29. package/scripts/smoke-mcp-http.mjs +0 -4026
  30. package/src/cli.ts +0 -268
  31. package/src/http/server.ts +0 -314
  32. package/src/http/service.ts +0 -934
  33. package/src/init.ts +0 -262
  34. package/src/install-manifest.ts +0 -144
  35. package/src/mcp/docs.ts +0 -777
  36. package/src/mcp/server.ts +0 -4580
  37. package/src/package-root.ts +0 -31
  38. package/src/runtime/actor.ts +0 -61
  39. package/src/runtime/auth.ts +0 -673
  40. package/src/runtime/check.ts +0 -18
  41. package/src/runtime/config.ts +0 -128
  42. package/src/runtime/mongo.ts +0 -89
  43. package/src/runtime/records.ts +0 -3205
  44. package/src/runtime/workspace.ts +0 -155
  45. package/src/upgrade.ts +0 -356
  46. package/src/validate.ts +0 -141
  47. package/src/version.ts +0 -16
@@ -1,506 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { pathToFileURL } from "node:url";
4
- import { connectRuntime } from "../dist/runtime/mongo.js";
5
- import {
6
- appendDxcompleteTicketReply,
7
- archiveRecord,
8
- COLLECTION_NAMES,
9
- DXCOMPLETE_TICKET_COLLECTION_NAME
10
- } from "../dist/runtime/records.js";
11
- import { ensureWorkspaceBootstrap } from "../dist/runtime/auth.js";
12
- import { loadWorkspaceConfig } from "../dist/runtime/workspace.js";
13
-
14
- const defaultEnvFile = ".env.local";
15
- const runtimeActorId = "dxcomplete-runtime";
16
-
17
- const args = parseArgs(process.argv.slice(2));
18
-
19
- if (args.help || (!args.planPath && !args.modulePath)) {
20
- printHelp();
21
- process.exit(args.help ? 0 : 1);
22
- }
23
-
24
- const startedAt = Date.now();
25
- const runtime = await connectRuntime({ envFile: args.envFile ?? defaultEnvFile });
26
-
27
- try {
28
- const workspaceConfig = await maybeLoadWorkspaceConfig(args.workspaceConfigPath);
29
- const context = createContext(runtime, workspaceConfig);
30
- const results = [];
31
-
32
- if (args.planPath) {
33
- results.push(await runPlan(await readPlan(args.planPath), context));
34
- }
35
-
36
- if (args.modulePath) {
37
- results.push(await runModule(args.modulePath, context));
38
- }
39
-
40
- console.log(
41
- JSON.stringify(
42
- {
43
- ok: true,
44
- databaseName: runtime.config.databaseName,
45
- workspaceId: workspaceConfig?.workspaceId,
46
- elapsedSeconds: elapsedSeconds(startedAt),
47
- results
48
- },
49
- null,
50
- 2
51
- )
52
- );
53
- } finally {
54
- await runtime.close();
55
- }
56
-
57
- function parseArgs(argv) {
58
- const parsed = {
59
- help: false
60
- };
61
-
62
- for (let index = 0; index < argv.length; index += 1) {
63
- const arg = argv[index];
64
-
65
- if (arg === "--help" || arg === "-h") {
66
- parsed.help = true;
67
- continue;
68
- }
69
-
70
- if (arg === "--plan") {
71
- parsed.planPath = readRequiredArg(argv, index, "--plan");
72
- index += 1;
73
- continue;
74
- }
75
-
76
- if (arg.startsWith("--plan=")) {
77
- parsed.planPath = arg.slice("--plan=".length);
78
- continue;
79
- }
80
-
81
- if (arg === "--module") {
82
- parsed.modulePath = readRequiredArg(argv, index, "--module");
83
- index += 1;
84
- continue;
85
- }
86
-
87
- if (arg.startsWith("--module=")) {
88
- parsed.modulePath = arg.slice("--module=".length);
89
- continue;
90
- }
91
-
92
- if (arg === "--env") {
93
- parsed.envFile = readRequiredArg(argv, index, "--env");
94
- index += 1;
95
- continue;
96
- }
97
-
98
- if (arg.startsWith("--env=")) {
99
- parsed.envFile = arg.slice("--env=".length);
100
- continue;
101
- }
102
-
103
- if (arg === "--workspace-config") {
104
- parsed.workspaceConfigPath = readRequiredArg(argv, index, "--workspace-config");
105
- index += 1;
106
- continue;
107
- }
108
-
109
- if (arg.startsWith("--workspace-config=")) {
110
- parsed.workspaceConfigPath = arg.slice("--workspace-config=".length);
111
- continue;
112
- }
113
-
114
- throw new Error(`Unknown argument: ${arg}`);
115
- }
116
-
117
- return parsed;
118
- }
119
-
120
- function readRequiredArg(argv, index, name) {
121
- const value = argv[index + 1];
122
- if (!value) {
123
- throw new Error(`${name} requires a value.`);
124
- }
125
- return value;
126
- }
127
-
128
- async function maybeLoadWorkspaceConfig(configPath) {
129
- try {
130
- return await loadWorkspaceConfig(configPath ? { configPath } : {});
131
- } catch (error) {
132
- if (configPath) {
133
- throw error;
134
- }
135
- return undefined;
136
- }
137
- }
138
-
139
- function createContext(runtime, workspaceConfig) {
140
- return {
141
- runtime,
142
- db: runtime.db,
143
- workspaceConfig,
144
- actorId: runtimeActorId,
145
- helpers: {
146
- appendDxcompleteTicketReply,
147
- archiveRecord,
148
- collectionNames: COLLECTION_NAMES,
149
- dxcompleteTicketCollectionName: DXCOMPLETE_TICKET_COLLECTION_NAME,
150
- ensureWorkspaceBootstrap,
151
- getTicketOwnerActorId,
152
- needsReplySummary,
153
- verifyNoLinksToType
154
- }
155
- };
156
- }
157
-
158
- async function readPlan(planPath) {
159
- const absolutePath = path.resolve(planPath);
160
- return JSON.parse(await readFile(absolutePath, "utf8"));
161
- }
162
-
163
- async function runPlan(plan, context) {
164
- if (!plan || typeof plan !== "object" || Array.isArray(plan)) {
165
- throw new Error("Plan must be a JSON object.");
166
- }
167
-
168
- const started = Date.now();
169
- const result = {
170
- kind: "plan",
171
- operations: []
172
- };
173
-
174
- if (plan.ensureWorkspaceBootstrap) {
175
- if (!context.workspaceConfig) {
176
- throw new Error("ensureWorkspaceBootstrap requires dxcomplete/workspace.json or --workspace-config.");
177
- }
178
- await ensureWorkspaceBootstrap(context.db, context.workspaceConfig, context.actorId);
179
- result.operations.push({
180
- operation: "ensureWorkspaceBootstrap",
181
- workspaceId: context.workspaceConfig.workspaceId
182
- });
183
- }
184
-
185
- if (plan.ticketSummary) {
186
- result.operations.push({
187
- operation: "ticketSummary",
188
- ...(await needsReplySummary(context.db, plan.ticketSummary))
189
- });
190
- }
191
-
192
- if (plan.getTickets) {
193
- result.operations.push({
194
- operation: "getTickets",
195
- tickets: await getTickets(context.db, readStringArray(plan.getTickets, "getTickets"))
196
- });
197
- }
198
-
199
- if (plan.appendTicketReplies) {
200
- result.operations.push({
201
- operation: "appendTicketReplies",
202
- replies: await appendTicketReplies(context, readArray(plan.appendTicketReplies, "appendTicketReplies"))
203
- });
204
- }
205
-
206
- if (plan.archiveRecords) {
207
- result.operations.push({
208
- operation: "archiveRecords",
209
- archived: await archiveRecords(context, readArray(plan.archiveRecords, "archiveRecords"))
210
- });
211
- }
212
-
213
- if (plan.verifyNoLinksToType) {
214
- result.operations.push({
215
- operation: "verifyNoLinksToType",
216
- checks: await runNoLinkChecks(context, readArray(plan.verifyNoLinksToType, "verifyNoLinksToType"))
217
- });
218
- }
219
-
220
- result.elapsedSeconds = elapsedSeconds(started);
221
- return result;
222
- }
223
-
224
- async function runModule(modulePath, context) {
225
- const started = Date.now();
226
- const moduleUrl = pathToFileURL(path.resolve(modulePath)).href;
227
- const module = await import(moduleUrl);
228
-
229
- if (typeof module.run !== "function") {
230
- throw new Error(`${modulePath} must export async function run(context).`);
231
- }
232
-
233
- return {
234
- kind: "module",
235
- modulePath,
236
- result: await module.run(context),
237
- elapsedSeconds: elapsedSeconds(started)
238
- };
239
- }
240
-
241
- async function needsReplySummary(db, options = {}) {
242
- const limit = readOptionalLimit(options.limit, 100);
243
- const filter = {};
244
-
245
- if (!options.includeArchived) {
246
- filter.archivedAt = { $exists: false };
247
- }
248
-
249
- const tickets = await db
250
- .collection(DXCOMPLETE_TICKET_COLLECTION_NAME)
251
- .find(filter, { projection: { _id: 1, title: 1, updatedAt: 1, "fields.ownerActorId": 1, "fields.entries": 1 } })
252
- .sort({ updatedAt: -1 })
253
- .limit(limit)
254
- .toArray();
255
-
256
- const rows = tickets.map(ticketNeedsReplyRow);
257
-
258
- return {
259
- activeCount: tickets.length,
260
- needsReplyCount: rows.filter((row) => row.needsReply).length,
261
- needsReply: rows.filter((row) => row.needsReply)
262
- };
263
- }
264
-
265
- async function getTickets(db, ids) {
266
- const tickets = await db
267
- .collection(DXCOMPLETE_TICKET_COLLECTION_NAME)
268
- .find({ _id: { $in: ids } })
269
- .toArray();
270
- const byId = new Map(tickets.map((ticket) => [ticket._id, ticket]));
271
-
272
- return ids.map((id) => {
273
- const ticket = byId.get(id);
274
- if (!ticket) {
275
- return { id, found: false };
276
- }
277
-
278
- const entries = ticket.fields?.entries ?? [];
279
- return {
280
- id,
281
- found: true,
282
- title: ticket.title,
283
- ownerActorId: ticket.fields?.ownerActorId,
284
- updatedAt: ticket.updatedAt,
285
- entries: entries.length,
286
- lastEntry: entries.at(-1)
287
- };
288
- });
289
- }
290
-
291
- async function appendTicketReplies(context, replies) {
292
- const results = [];
293
-
294
- for (const reply of replies) {
295
- if (!reply || typeof reply !== "object" || Array.isArray(reply)) {
296
- throw new Error("Each appendTicketReplies entry must be an object.");
297
- }
298
-
299
- const id = readRequiredString(reply.id, "appendTicketReplies[].id");
300
- const body = await readBody(reply, "appendTicketReplies[]");
301
- const addressedToActorId =
302
- typeof reply.addressedToActorId === "string" && reply.addressedToActorId.trim()
303
- ? reply.addressedToActorId.trim()
304
- : await getTicketOwnerActorId(context.db, id);
305
-
306
- const updated = await appendDxcompleteTicketReply(
307
- context.db,
308
- {
309
- id,
310
- body,
311
- addressedToActorId
312
- },
313
- context.actorId
314
- );
315
-
316
- results.push({
317
- id,
318
- addressedToActorId,
319
- updatedAt: updated.updatedAt,
320
- entries: Array.isArray(updated.fields?.entries) ? updated.fields.entries.length : undefined
321
- });
322
- }
323
-
324
- return results;
325
- }
326
-
327
- async function archiveRecords(context, records) {
328
- const results = [];
329
-
330
- for (const record of records) {
331
- if (!record || typeof record !== "object" || Array.isArray(record)) {
332
- throw new Error("Each archiveRecords entry must be an object.");
333
- }
334
-
335
- const updated = await archiveRecord(
336
- context.db,
337
- {
338
- recordType: readRequiredString(record.recordType, "archiveRecords[].recordType"),
339
- id: readRequiredString(record.id, "archiveRecords[].id"),
340
- workspaceId: typeof record.workspaceId === "string" ? record.workspaceId : context.workspaceConfig?.workspaceId,
341
- reason: typeof record.reason === "string" ? record.reason : undefined,
342
- supersededByType: typeof record.supersededByType === "string" ? record.supersededByType : undefined,
343
- supersededById: typeof record.supersededById === "string" ? record.supersededById : undefined
344
- },
345
- context.actorId
346
- );
347
-
348
- results.push({
349
- recordType: updated.recordType,
350
- id: updated._id,
351
- archivedAt: updated.archivedAt
352
- });
353
- }
354
-
355
- return results;
356
- }
357
-
358
- async function runNoLinkChecks(context, checks) {
359
- const results = [];
360
-
361
- for (const check of checks) {
362
- if (!check || typeof check !== "object" || Array.isArray(check)) {
363
- throw new Error("Each verifyNoLinksToType entry must be an object.");
364
- }
365
-
366
- const toType = readRequiredString(check.toType, "verifyNoLinksToType[].toType");
367
- const collections = check.collections
368
- ? readStringArray(check.collections, "verifyNoLinksToType[].collections")
369
- : [...COLLECTION_NAMES];
370
- results.push(await verifyNoLinksToType(context.db, toType, collections));
371
- }
372
-
373
- return results;
374
- }
375
-
376
- async function verifyNoLinksToType(db, toType, collections = COLLECTION_NAMES) {
377
- const residue = [];
378
-
379
- for (const collectionName of collections) {
380
- const count = await db.collection(collectionName).countDocuments({ "links.toType": toType });
381
- if (count > 0) {
382
- residue.push({ collectionName, count });
383
- }
384
- }
385
-
386
- return {
387
- toType,
388
- ok: residue.length === 0,
389
- residue
390
- };
391
- }
392
-
393
- async function getTicketOwnerActorId(db, id) {
394
- const ticket = await db
395
- .collection(DXCOMPLETE_TICKET_COLLECTION_NAME)
396
- .findOne({ _id: id }, { projection: { "fields.ownerActorId": 1 } });
397
-
398
- if (!ticket) {
399
- throw new Error(`DX Complete Ticket not found: ${id}`);
400
- }
401
-
402
- const ownerActorId = ticket.fields?.ownerActorId;
403
- if (typeof ownerActorId !== "string" || !ownerActorId) {
404
- throw new Error(`DX Complete Ticket owner actor is missing: ${id}`);
405
- }
406
-
407
- return ownerActorId;
408
- }
409
-
410
- function ticketNeedsReplyRow(ticket) {
411
- const entries = ticket.fields?.entries ?? [];
412
- const lastReply = [...entries].reverse().find((entry) => entry.direction === "dxcomplete_reply");
413
- const lastSubmitter = [...entries].reverse().find((entry) => entry.direction === "submitter_entry");
414
- const needsReply = Boolean(lastSubmitter && (!lastReply || lastSubmitter.createdAt > lastReply.createdAt));
415
-
416
- return {
417
- id: ticket._id,
418
- title: ticket.title,
419
- ownerActorId: ticket.fields?.ownerActorId,
420
- updatedAt: ticket.updatedAt,
421
- entries: entries.length,
422
- needsReply,
423
- lastSubmitterAt: lastSubmitter?.createdAt,
424
- lastReplyAt: lastReply?.createdAt
425
- };
426
- }
427
-
428
- async function readBody(input, label) {
429
- if (typeof input.body === "string") {
430
- return input.body;
431
- }
432
-
433
- if (typeof input.bodyFile === "string" && input.bodyFile.trim()) {
434
- return readFile(path.resolve(input.bodyFile), "utf8");
435
- }
436
-
437
- throw new Error(`${label} requires body or bodyFile.`);
438
- }
439
-
440
- function readStringArray(value, label) {
441
- const array = readArray(value, label);
442
- return array.map((entry, index) => readRequiredString(entry, `${label}[${index}]`));
443
- }
444
-
445
- function readArray(value, label) {
446
- if (!Array.isArray(value)) {
447
- throw new Error(`${label} must be an array.`);
448
- }
449
- return value;
450
- }
451
-
452
- function readRequiredString(value, label) {
453
- if (typeof value !== "string" || !value.trim()) {
454
- throw new Error(`${label} must be a non-empty string.`);
455
- }
456
- return value.trim();
457
- }
458
-
459
- function readOptionalLimit(value, fallback) {
460
- if (value === undefined) {
461
- return fallback;
462
- }
463
-
464
- if (!Number.isInteger(value) || value < 1 || value > 500) {
465
- throw new Error("limit must be an integer from 1 to 500.");
466
- }
467
-
468
- return value;
469
- }
470
-
471
- function elapsedSeconds(started) {
472
- return Number(((Date.now() - started) / 1000).toFixed(2));
473
- }
474
-
475
- function printHelp() {
476
- console.log(`dogfood-work-order
477
-
478
- Usage:
479
- node scripts/dogfood-work-order.mjs --plan ./work-order.json
480
- node scripts/dogfood-work-order.mjs --module ./one-off.mjs
481
-
482
- Purpose:
483
- Open Mongo once and batch dogfood reads, ticket replies, simple verification,
484
- and optional one-off work-order modules.
485
-
486
- Plan shape:
487
- {
488
- "ensureWorkspaceBootstrap": true,
489
- "ticketSummary": { "limit": 100 },
490
- "getTickets": ["ticket-id"],
491
- "appendTicketReplies": [
492
- { "id": "ticket-id", "body": "Reply body" }
493
- ],
494
- "archiveRecords": [
495
- { "recordType": "requirements", "id": "uuid", "reason": "Archived" }
496
- ],
497
- "verifyNoLinksToType": [
498
- { "toType": "initiatives" }
499
- ]
500
- }
501
-
502
- Notes:
503
- Ticket replies default to the ticket owner actor when addressedToActorId is omitted.
504
- A module must export async function run(context).
505
- `);
506
- }