dxcomplete 0.1.0 → 0.2.1

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 (50) hide show
  1. package/README.md +22 -22
  2. package/dist/init.js +19 -0
  3. package/dist/mcp/docs.d.ts +18 -2
  4. package/dist/mcp/docs.js +201 -13
  5. package/dist/mcp/server.js +761 -47
  6. package/dist/runtime/check.d.ts +1 -1
  7. package/dist/runtime/records.d.ts +95 -4
  8. package/dist/runtime/records.js +640 -11
  9. package/dist/validate.js +3 -1
  10. package/docs/codex-integration.md +45 -18
  11. package/docs/glossary.md +39 -7
  12. package/docs/index.md +2 -1
  13. package/docs/model.md +3 -3
  14. package/docs/operating-guide.md +116 -0
  15. package/docs/taxonomy.md +12 -6
  16. package/docs/workflows.md +5 -3
  17. package/package.json +21 -3
  18. package/scripts/{dogfood-work-order.mjs → runtime-work-order.mjs} +4 -4
  19. package/scripts/smoke-mcp-http.mjs +460 -6
  20. package/src/init.ts +35 -0
  21. package/src/mcp/docs.ts +234 -14
  22. package/src/mcp/server.ts +1138 -83
  23. package/src/runtime/records.ts +914 -12
  24. package/src/validate.ts +3 -1
  25. package/templates/AGENTS.md +30 -0
  26. package/templates/process/controls.yml +10 -6
  27. package/templates/process/diagrams/01-intake-triage.mmd +5 -5
  28. package/templates/process/diagrams/02-product-definition.mmd +1 -1
  29. package/templates/process/diagrams/06-change-release-control.mmd +5 -7
  30. package/templates/process/diagrams/07-deployment-operations.mmd +2 -2
  31. package/templates/process/diagrams/08-support-incident-management.mmd +5 -4
  32. package/templates/process/diagrams/09-problem-improvement.mmd +4 -3
  33. package/templates/process/diagrams/10-risk-control-management.mmd +6 -4
  34. package/templates/process/diagrams/11-audit-evidence-capture.mmd +1 -1
  35. package/templates/process/taxonomy.yml +91 -17
  36. package/templates/process/workflows.yml +10 -9
  37. package/website/flow.html +1 -0
  38. package/website/glossary.html +37 -8
  39. package/website/index.html +2 -1
  40. package/website/objects.html +68 -11
  41. package/website/operating-guide.html +165 -0
  42. package/website/outcomes.html +1 -0
  43. package/website/phase-build.html +1 -0
  44. package/website/phase-elicit.html +1 -0
  45. package/website/phase-go-live.html +2 -1
  46. package/website/phase-measure.html +1 -0
  47. package/website/phase-operate.html +1 -0
  48. package/website/phase-orient.html +1 -0
  49. package/website/phase-weigh.html +1 -0
  50. package/website/roles.html +1 -0
@@ -13,6 +13,11 @@ export const COLLECTION_NAMES = [
13
13
  "commitments",
14
14
  "deferrals",
15
15
  "changes",
16
+ "incidents",
17
+ "problems",
18
+ "maintenance_schedules",
19
+ "support_requests",
20
+ "value_realizations",
16
21
  "decisions",
17
22
  "risks"
18
23
  ];
@@ -42,6 +47,11 @@ export const READABLE_ID_TYPE_CODES = {
42
47
  deferrals: "DFR",
43
48
  decisions: "DEC",
44
49
  changes: "CHG",
50
+ incidents: "INC",
51
+ problems: "PRB",
52
+ maintenance_schedules: "MNT",
53
+ support_requests: "SUP",
54
+ value_realizations: "VAL",
45
55
  risks: "RSK",
46
56
  estimates: "EST",
47
57
  benefits: "BFT"
@@ -56,7 +66,9 @@ const VERSIONED_RECORD_TYPES = [
56
66
  "expectations",
57
67
  "requirements",
58
68
  "estimates",
59
- "benefits"
69
+ "benefits",
70
+ "maintenance_schedules",
71
+ "value_realizations"
60
72
  ];
61
73
  const VERSION_HISTORY_FIELD = "versionHistory";
62
74
  const CHANGE_MANAGED_FIELDS = [
@@ -64,6 +76,11 @@ const CHANGE_MANAGED_FIELDS = [
64
76
  "executionSteps",
65
77
  "rollbackPlan",
66
78
  "riskImpact",
79
+ "changeType",
80
+ "impactGrade",
81
+ "emergencyImportance",
82
+ "emergencyImmediacy",
83
+ "emergencyRationaleGaps",
67
84
  "plannedFor",
68
85
  "events"
69
86
  ];
@@ -81,6 +98,23 @@ const COMPONENT_MANAGED_FIELDS = [
81
98
  "secretPointers",
82
99
  "notes"
83
100
  ];
101
+ const MAINTENANCE_SCHEDULE_MANAGED_FIELDS = [
102
+ "name",
103
+ "kind",
104
+ "cadence",
105
+ "startDate",
106
+ "rationale",
107
+ "notes"
108
+ ];
109
+ const SUPPORT_REQUEST_LEDGER_MANAGED_FIELDS = [
110
+ "reporter",
111
+ "kind",
112
+ "reportedExperience",
113
+ "entries",
114
+ "currentStatus",
115
+ "status"
116
+ ];
117
+ const VALUE_REALIZATION_MANAGED_FIELDS = ["metrics"];
84
118
  const DECISION_LEDGER_MANAGED_FIELDS = [
85
119
  "matter",
86
120
  "entries",
@@ -102,6 +136,32 @@ const TASK_LEDGER_MANAGED_FIELDS = [
102
136
  "details",
103
137
  "status"
104
138
  ];
139
+ const RISK_LEDGER_MANAGED_FIELDS = [
140
+ "topic",
141
+ "entries",
142
+ "currentStatus",
143
+ "currentAssessment",
144
+ "currentTreatment",
145
+ "likelihood",
146
+ "impact",
147
+ "mitigation"
148
+ ];
149
+ const INCIDENT_LEDGER_MANAGED_FIELDS = [
150
+ "description",
151
+ "entries",
152
+ "currentStatus",
153
+ "currentSeverity",
154
+ "status",
155
+ "severity"
156
+ ];
157
+ const PROBLEM_LEDGER_MANAGED_FIELDS = [
158
+ "description",
159
+ "entries",
160
+ "currentStatus",
161
+ "currentRootCause",
162
+ "status",
163
+ "rootCause"
164
+ ];
105
165
  const DECISION_INPUT_MANAGED_FIELDS = ["informedBy", "informedByIds", "inputRecords"];
106
166
  const JOURNAL_ENTRY_MANAGED_FIELDS = ["kind", "body", "tag", "covers", "coveredBySummaryId"];
107
167
  const JOURNAL_COMPACTION_THRESHOLD = 200;
@@ -112,7 +172,9 @@ const VERSIONED_TYPED_FIELDS = {
112
172
  estimates: ["lineItems", "rollup"],
113
173
  benefits: ["benefitItems", "rollup"],
114
174
  environments: ["name", "description"],
115
- components: ["name", "environmentId", "kind", "locator", "identifiers", "secretPointers", "notes"]
175
+ components: ["name", "environmentId", "kind", "locator", "identifiers", "secretPointers", "notes"],
176
+ maintenance_schedules: ["name", "kind", "cadence", "startDate", "rationale", "notes"],
177
+ value_realizations: ["metrics"]
116
178
  };
117
179
  const RESERVED_RELATIONSHIP_FIELDS = {
118
180
  changes: ["requirementId"],
@@ -120,6 +182,7 @@ const RESERVED_RELATIONSHIP_FIELDS = {
120
182
  deferrals: ["requirementIds", "expectationIds"],
121
183
  estimates: ["requirementIds", "expectationIds"],
122
184
  benefits: ["requirementIds", "expectationIds"],
185
+ value_realizations: ["requirementIds", "expectationIds", "commitmentIds"],
123
186
  expectations: ["statementId"],
124
187
  requirements: ["expectationId"],
125
188
  tasks: ["requirementId"]
@@ -173,10 +236,10 @@ export async function createRecord(db, recordType, input, actorId) {
173
236
  finally {
174
237
  await session.endSession();
175
238
  }
176
- return record;
239
+ return withDerivedRecordFields(db, record);
177
240
  }
178
241
  await db.collection(recordType).insertOne(record);
179
- return record;
242
+ return withDerivedRecordFields(db, record);
180
243
  }
181
244
  export async function listRecords(db, recordType, limit, options = {}) {
182
245
  const filter = {};
@@ -186,12 +249,13 @@ export async function listRecords(db, recordType, limit, options = {}) {
186
249
  if (!options.includeArchived) {
187
250
  filter.archivedAt = { $exists: false };
188
251
  }
189
- return db
252
+ const records = await db
190
253
  .collection(recordType)
191
254
  .find(filter)
192
255
  .sort({ createdAt: -1 })
193
256
  .limit(limit)
194
257
  .toArray();
258
+ return Promise.all(records.map((record) => withDerivedRecordFields(db, record)));
195
259
  }
196
260
  export async function getRecord(db, recordType, id, options = {}) {
197
261
  const filter = recordIdentityFilter(recordType, id);
@@ -200,7 +264,8 @@ export async function getRecord(db, recordType, id, options = {}) {
200
264
  isCurrentCollection(recordType)) {
201
265
  filter.workspaceId = readRequiredWorkspaceId(options.workspaceId, recordType);
202
266
  }
203
- return db.collection(recordType).findOne(filter);
267
+ const record = await db.collection(recordType).findOne(filter);
268
+ return record ? withDerivedRecordFields(db, record) : null;
204
269
  }
205
270
  export async function listLinkedRecords(db, input) {
206
271
  const source = await getRecord(db, input.recordType, input.id, { workspaceId: input.workspaceId });
@@ -270,6 +335,204 @@ export async function listLinkedRecords(db, input) {
270
335
  }
271
336
  return result;
272
337
  }
338
+ async function withDerivedRecordFields(db, record) {
339
+ if (record.recordType === "maintenance_schedules") {
340
+ return {
341
+ ...record,
342
+ derived: await deriveMaintenanceScheduleState(db, record)
343
+ };
344
+ }
345
+ if (record.recordType === "value_realizations") {
346
+ return {
347
+ ...record,
348
+ derived: deriveValueRealizationState(record)
349
+ };
350
+ }
351
+ return record;
352
+ }
353
+ async function deriveMaintenanceScheduleState(db, record) {
354
+ const cadence = parseMaintenanceCadence(record.fields.cadence);
355
+ const startDate = parseDateString(record.fields.startDate);
356
+ const occurrences = await findMaintenanceOccurrences(db, record);
357
+ const latestOccurrence = occurrences[0];
358
+ const latestOccurrenceAt = latestOccurrence?.occurredAt;
359
+ const basis = latestOccurrenceAt ?? startDate?.toISOString();
360
+ const nextDueAt = cadence && basis ? addMaintenanceCadence(new Date(basis), cadence).toISOString() : undefined;
361
+ return compactRuntimeObject({
362
+ occurrenceCount: occurrences.length,
363
+ latestOccurrence,
364
+ cadence: cadence ?? undefined,
365
+ startDate: startDate?.toISOString(),
366
+ nextDueAt,
367
+ overdue: nextDueAt ? new Date() > new Date(nextDueAt) : undefined
368
+ });
369
+ }
370
+ async function findMaintenanceOccurrences(db, schedule) {
371
+ const workspaceId = schedule.workspaceId;
372
+ if (!workspaceId) {
373
+ return [];
374
+ }
375
+ const linkFilter = {
376
+ workspaceId,
377
+ archivedAt: { $exists: false },
378
+ links: {
379
+ $elemMatch: {
380
+ toType: "maintenance_schedules",
381
+ toId: schedule._id,
382
+ relationship: "assigned_to"
383
+ }
384
+ }
385
+ };
386
+ const [changes, tasks] = await Promise.all([
387
+ db.collection("changes").find(linkFilter).toArray(),
388
+ db.collection("tasks").find(linkFilter).toArray()
389
+ ]);
390
+ const occurrences = [
391
+ ...changes.flatMap((record) => occurrenceFromChange(record)),
392
+ ...tasks.flatMap((record) => occurrenceFromTask(record))
393
+ ];
394
+ return occurrences.sort((left, right) => right.occurredAt.localeCompare(left.occurredAt));
395
+ }
396
+ function occurrenceFromChange(record) {
397
+ const events = Array.isArray(record.fields.events) ? record.fields.events : [];
398
+ const event = [...events]
399
+ .reverse()
400
+ .find((entry) => Boolean(entry &&
401
+ typeof entry === "object" &&
402
+ (entry.eventType === "result_reported" || entry.eventType === "recovery_recorded") &&
403
+ typeof entry.createdAt === "string"));
404
+ const plannedFor = typeof record.fields.plannedFor === "string" ? record.fields.plannedFor : undefined;
405
+ const occurredAt = typeof event?.createdAt === "string" ? event.createdAt : plannedFor ?? record.createdAt;
406
+ return [{ recordType: "changes", id: record._id, readableId: record.readableId, occurredAt }];
407
+ }
408
+ function occurrenceFromTask(record) {
409
+ const entries = Array.isArray(record.fields.entries) ? record.fields.entries : [];
410
+ const doneEntry = [...entries]
411
+ .reverse()
412
+ .find((entry) => Boolean(entry &&
413
+ typeof entry === "object" &&
414
+ entry.entryType === "status_change" &&
415
+ entry.status === "done" &&
416
+ typeof entry.createdAt === "string"));
417
+ const occurredAt = typeof doneEntry?.createdAt === "string" ? doneEntry.createdAt : record.createdAt;
418
+ return [{ recordType: "tasks", id: record._id, readableId: record.readableId, occurredAt }];
419
+ }
420
+ function parseMaintenanceCadence(value) {
421
+ if (!value || typeof value !== "object") {
422
+ return undefined;
423
+ }
424
+ const cadence = value;
425
+ if (Number.isInteger(cadence.count) &&
426
+ (cadence.count ?? 0) > 0 &&
427
+ (cadence.unit === "day" ||
428
+ cadence.unit === "week" ||
429
+ cadence.unit === "month" ||
430
+ cadence.unit === "quarter" ||
431
+ cadence.unit === "year")) {
432
+ return { count: cadence.count, unit: cadence.unit };
433
+ }
434
+ return undefined;
435
+ }
436
+ function parseDateString(value) {
437
+ if (typeof value !== "string") {
438
+ return undefined;
439
+ }
440
+ const date = new Date(value);
441
+ return Number.isNaN(date.getTime()) ? undefined : date;
442
+ }
443
+ function addMaintenanceCadence(date, cadence) {
444
+ const next = new Date(date);
445
+ switch (cadence.unit) {
446
+ case "day":
447
+ next.setUTCDate(next.getUTCDate() + cadence.count);
448
+ return next;
449
+ case "week":
450
+ next.setUTCDate(next.getUTCDate() + cadence.count * 7);
451
+ return next;
452
+ case "month":
453
+ next.setUTCMonth(next.getUTCMonth() + cadence.count);
454
+ return next;
455
+ case "quarter":
456
+ next.setUTCMonth(next.getUTCMonth() + cadence.count * 3);
457
+ return next;
458
+ case "year":
459
+ next.setUTCFullYear(next.getUTCFullYear() + cadence.count);
460
+ return next;
461
+ }
462
+ }
463
+ function deriveValueRealizationState(record) {
464
+ const metrics = normalizeValueRealizationMetrics(record.fields.metrics);
465
+ const comparisons = metrics.map((metric) => {
466
+ if (!metric.actual) {
467
+ return {
468
+ id: metric.id,
469
+ name: metric.name,
470
+ unit: metric.unit,
471
+ direction: metric.direction,
472
+ status: "open"
473
+ };
474
+ }
475
+ const absoluteChange = metric.actual.value - metric.baseline.value;
476
+ const ratio = metric.baseline.value === 0 ? null : metric.actual.value / metric.baseline.value;
477
+ const percentChange = ratio === null ? null : (ratio - 1) * 100;
478
+ const outcome = absoluteChange === 0
479
+ ? "unchanged"
480
+ : metric.direction === "higher_is_better"
481
+ ? absoluteChange > 0
482
+ ? "improved"
483
+ : "regressed"
484
+ : absoluteChange < 0
485
+ ? "improved"
486
+ : "regressed";
487
+ return {
488
+ id: metric.id,
489
+ name: metric.name,
490
+ unit: metric.unit,
491
+ direction: metric.direction,
492
+ status: "measured",
493
+ absoluteChange,
494
+ percentChange,
495
+ ratio,
496
+ outcome,
497
+ baselineMeasuredAt: metric.baseline.measuredAt,
498
+ actualMeasuredAt: metric.actual.measuredAt
499
+ };
500
+ });
501
+ return {
502
+ metricCount: metrics.length,
503
+ openMetricCount: comparisons.filter((comparison) => comparison.status === "open").length,
504
+ measuredMetricCount: comparisons.filter((comparison) => comparison.status === "measured").length,
505
+ comparisons
506
+ };
507
+ }
508
+ function normalizeValueRealizationMetrics(value) {
509
+ if (!Array.isArray(value)) {
510
+ return [];
511
+ }
512
+ return value.filter(isValueRealizationMetric);
513
+ }
514
+ function isValueRealizationMetric(value) {
515
+ if (!value || typeof value !== "object") {
516
+ return false;
517
+ }
518
+ const metric = value;
519
+ return (typeof metric.id === "string" &&
520
+ typeof metric.name === "string" &&
521
+ typeof metric.unit === "string" &&
522
+ (metric.direction === "lower_is_better" || metric.direction === "higher_is_better") &&
523
+ isMeasuredValue(metric.baseline) &&
524
+ (metric.actual === undefined || isMeasuredValue(metric.actual)));
525
+ }
526
+ function isMeasuredValue(value) {
527
+ return (Boolean(value) &&
528
+ typeof value === "object" &&
529
+ typeof value.value === "number" &&
530
+ Number.isFinite(value.value) &&
531
+ typeof value.measuredAt === "string");
532
+ }
533
+ function compactRuntimeObject(input) {
534
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
535
+ }
273
536
  export async function migrateLegacyIntakeItemsToDxcompleteTickets(db) {
274
537
  const legacyCollection = db.collection(LEGACY_INTAKE_COLLECTION_NAME);
275
538
  const ticketCollection = db.collection(DXCOMPLETE_TICKET_COLLECTION_NAME);
@@ -681,6 +944,178 @@ export async function appendTaskEntry(db, input, actorId) {
681
944
  }
682
945
  return updated;
683
946
  }
947
+ export async function appendRiskEntry(db, input, actorId) {
948
+ const collection = db.collection("risks");
949
+ const filter = scopedRecordFilter("risks", input.riskId, input.workspaceId);
950
+ const existing = await collection.findOne(filter);
951
+ if (!existing) {
952
+ throw new Error(`Record not found: risks/${input.riskId}`);
953
+ }
954
+ assertRiskEntryInput(input);
955
+ const now = new Date().toISOString();
956
+ const entry = {
957
+ id: randomUUID(),
958
+ entryType: input.entryType,
959
+ body: input.body,
960
+ createdAt: now,
961
+ createdBy: actorId,
962
+ ...(input.likelihood ? { likelihood: input.likelihood } : {}),
963
+ ...(input.impact ? { impact: input.impact } : {}),
964
+ ...(input.treatment ? { treatment: input.treatment } : {}),
965
+ ...(input.treatmentRationale ? { treatmentRationale: input.treatmentRationale } : {})
966
+ };
967
+ const update = {
968
+ $push: {
969
+ "fields.entries": entry
970
+ },
971
+ $set: {
972
+ updatedAt: now,
973
+ updatedBy: actorId
974
+ }
975
+ };
976
+ if (entry.entryType === "identified" || entry.entryType === "reopened") {
977
+ update.$set["fields.currentStatus"] = riskEntryToCurrentStatus(entry, "open");
978
+ }
979
+ if (entry.entryType === "closed") {
980
+ update.$set["fields.currentStatus"] = riskEntryToCurrentStatus(entry, "closed");
981
+ }
982
+ if (entry.entryType === "assessment") {
983
+ update.$set["fields.currentAssessment"] = riskEntryToCurrentAssessment(entry);
984
+ }
985
+ if (entry.entryType === "treatment") {
986
+ update.$set["fields.currentTreatment"] = riskEntryToCurrentTreatment(entry);
987
+ }
988
+ await collection.updateOne(filter, update);
989
+ const updated = await getRecord(db, "risks", input.riskId, { workspaceId: input.workspaceId });
990
+ if (!updated) {
991
+ throw new Error(`Record not found after risk entry append: risks/${input.riskId}`);
992
+ }
993
+ return updated;
994
+ }
995
+ export async function appendIncidentEntry(db, input, actorId) {
996
+ const collection = db.collection("incidents");
997
+ const filter = scopedRecordFilter("incidents", input.incidentId, input.workspaceId);
998
+ const existing = await collection.findOne(filter);
999
+ if (!existing) {
1000
+ throw new Error(`Record not found: incidents/${input.incidentId}`);
1001
+ }
1002
+ assertIncidentEntryInput(input);
1003
+ const now = new Date().toISOString();
1004
+ const entry = {
1005
+ id: randomUUID(),
1006
+ entryType: input.entryType,
1007
+ body: input.body,
1008
+ createdAt: now,
1009
+ createdBy: actorId,
1010
+ ...(input.severity ? { severity: input.severity } : {})
1011
+ };
1012
+ const update = {
1013
+ $push: {
1014
+ "fields.entries": entry
1015
+ },
1016
+ $set: {
1017
+ updatedAt: now,
1018
+ updatedBy: actorId
1019
+ }
1020
+ };
1021
+ if (entry.entryType === "detected" || entry.entryType === "reopened") {
1022
+ update.$set["fields.currentStatus"] = incidentEntryToCurrentStatus(entry, "open");
1023
+ }
1024
+ if (entry.entryType === "resolved") {
1025
+ update.$set["fields.currentStatus"] = incidentEntryToCurrentStatus(entry, "resolved");
1026
+ }
1027
+ if (entry.entryType === "severity") {
1028
+ update.$set["fields.currentSeverity"] = incidentEntryToCurrentSeverity(entry);
1029
+ }
1030
+ await collection.updateOne(filter, update);
1031
+ const updated = await getRecord(db, "incidents", input.incidentId, { workspaceId: input.workspaceId });
1032
+ if (!updated) {
1033
+ throw new Error(`Record not found after incident entry append: incidents/${input.incidentId}`);
1034
+ }
1035
+ return updated;
1036
+ }
1037
+ export async function appendProblemEntry(db, input, actorId) {
1038
+ const collection = db.collection("problems");
1039
+ const filter = scopedRecordFilter("problems", input.problemId, input.workspaceId);
1040
+ const existing = await collection.findOne(filter);
1041
+ if (!existing) {
1042
+ throw new Error(`Record not found: problems/${input.problemId}`);
1043
+ }
1044
+ assertProblemEntryInput(input);
1045
+ const now = new Date().toISOString();
1046
+ const entry = {
1047
+ id: randomUUID(),
1048
+ entryType: input.entryType,
1049
+ body: input.body,
1050
+ createdAt: now,
1051
+ createdBy: actorId,
1052
+ ...(input.rootCause ? { rootCause: input.rootCause } : {})
1053
+ };
1054
+ const update = {
1055
+ $push: {
1056
+ "fields.entries": entry
1057
+ },
1058
+ $set: {
1059
+ updatedAt: now,
1060
+ updatedBy: actorId
1061
+ }
1062
+ };
1063
+ if (entry.entryType === "identified" || entry.entryType === "reopened") {
1064
+ update.$set["fields.currentStatus"] = problemEntryToCurrentStatus(entry, "open");
1065
+ }
1066
+ if (entry.entryType === "known_error") {
1067
+ update.$set["fields.currentStatus"] = problemEntryToCurrentStatus(entry, "known_error");
1068
+ }
1069
+ if (entry.entryType === "resolved") {
1070
+ update.$set["fields.currentStatus"] = problemEntryToCurrentStatus(entry, "resolved");
1071
+ }
1072
+ if (entry.entryType === "root_cause") {
1073
+ update.$set["fields.currentRootCause"] = problemEntryToCurrentRootCause(entry);
1074
+ }
1075
+ await collection.updateOne(filter, update);
1076
+ const updated = await getRecord(db, "problems", input.problemId, { workspaceId: input.workspaceId });
1077
+ if (!updated) {
1078
+ throw new Error(`Record not found after problem entry append: problems/${input.problemId}`);
1079
+ }
1080
+ return updated;
1081
+ }
1082
+ export async function appendSupportRequestEntry(db, input, actorId) {
1083
+ const collection = db.collection("support_requests");
1084
+ const filter = scopedRecordFilter("support_requests", input.supportRequestId, input.workspaceId);
1085
+ const existing = await collection.findOne(filter);
1086
+ if (!existing) {
1087
+ throw new Error(`Record not found: support_requests/${input.supportRequestId}`);
1088
+ }
1089
+ assertSupportRequestEntryInput(input);
1090
+ const now = new Date().toISOString();
1091
+ const entry = {
1092
+ id: randomUUID(),
1093
+ entryType: input.entryType,
1094
+ body: input.body,
1095
+ createdAt: now,
1096
+ createdBy: actorId,
1097
+ ...(input.incidentId ? { incidentId: input.incidentId } : {})
1098
+ };
1099
+ const update = {
1100
+ $push: {
1101
+ "fields.entries": entry
1102
+ },
1103
+ $set: {
1104
+ updatedAt: now,
1105
+ updatedBy: actorId
1106
+ }
1107
+ };
1108
+ const currentStatus = supportRequestEntryToCurrentStatus(entry, readSupportRequestStatus(existing));
1109
+ if (currentStatus) {
1110
+ update.$set["fields.currentStatus"] = currentStatus;
1111
+ }
1112
+ await collection.updateOne(filter, update);
1113
+ const updated = await getRecord(db, "support_requests", input.supportRequestId, { workspaceId: input.workspaceId });
1114
+ if (!updated) {
1115
+ throw new Error(`Record not found after support request entry append: support_requests/${input.supportRequestId}`);
1116
+ }
1117
+ return updated;
1118
+ }
684
1119
  export function decisionEntryToCurrentDecision(entry) {
685
1120
  return {
686
1121
  entryId: entry.id,
@@ -703,6 +1138,117 @@ export function taskEntryToCurrentStatus(entry) {
703
1138
  createdBy: entry.createdBy
704
1139
  };
705
1140
  }
1141
+ export function riskEntryToCurrentStatus(entry, status) {
1142
+ return {
1143
+ entryId: entry.id,
1144
+ status,
1145
+ body: entry.body,
1146
+ createdAt: entry.createdAt,
1147
+ createdBy: entry.createdBy
1148
+ };
1149
+ }
1150
+ export function riskEntryToCurrentAssessment(entry) {
1151
+ if (entry.entryType !== "assessment" || !entry.likelihood || !entry.impact) {
1152
+ throw new Error("Risk current assessment can only derive from an assessment entry with likelihood and impact.");
1153
+ }
1154
+ return {
1155
+ entryId: entry.id,
1156
+ likelihood: entry.likelihood,
1157
+ impact: entry.impact,
1158
+ body: entry.body,
1159
+ createdAt: entry.createdAt,
1160
+ createdBy: entry.createdBy
1161
+ };
1162
+ }
1163
+ export function riskEntryToCurrentTreatment(entry) {
1164
+ if (entry.entryType !== "treatment" || !entry.treatment) {
1165
+ throw new Error("Risk current treatment can only derive from a treatment entry.");
1166
+ }
1167
+ return {
1168
+ entryId: entry.id,
1169
+ treatment: entry.treatment,
1170
+ body: entry.body,
1171
+ createdAt: entry.createdAt,
1172
+ createdBy: entry.createdBy,
1173
+ ...(entry.treatmentRationale ? { treatmentRationale: entry.treatmentRationale } : {})
1174
+ };
1175
+ }
1176
+ export function incidentEntryToCurrentStatus(entry, status) {
1177
+ return {
1178
+ entryId: entry.id,
1179
+ status,
1180
+ body: entry.body,
1181
+ createdAt: entry.createdAt,
1182
+ createdBy: entry.createdBy
1183
+ };
1184
+ }
1185
+ export function incidentEntryToCurrentSeverity(entry) {
1186
+ if (entry.entryType !== "severity" || !entry.severity) {
1187
+ throw new Error("Incident current severity can only derive from a severity entry.");
1188
+ }
1189
+ return {
1190
+ entryId: entry.id,
1191
+ severity: entry.severity,
1192
+ body: entry.body,
1193
+ createdAt: entry.createdAt,
1194
+ createdBy: entry.createdBy
1195
+ };
1196
+ }
1197
+ export function problemEntryToCurrentStatus(entry, status) {
1198
+ return {
1199
+ entryId: entry.id,
1200
+ status,
1201
+ body: entry.body,
1202
+ createdAt: entry.createdAt,
1203
+ createdBy: entry.createdBy
1204
+ };
1205
+ }
1206
+ export function problemEntryToCurrentRootCause(entry) {
1207
+ if (entry.entryType !== "root_cause") {
1208
+ throw new Error("Problem current root cause can only derive from a root_cause entry.");
1209
+ }
1210
+ return {
1211
+ entryId: entry.id,
1212
+ rootCause: entry.rootCause ?? entry.body,
1213
+ body: entry.body,
1214
+ createdAt: entry.createdAt,
1215
+ createdBy: entry.createdBy
1216
+ };
1217
+ }
1218
+ export function supportRequestEntryToCurrentStatus(entry, previousStatus) {
1219
+ const status = supportRequestStatusForEntryType(entry.entryType);
1220
+ if (!status) {
1221
+ return previousStatus;
1222
+ }
1223
+ return {
1224
+ entryId: entry.id,
1225
+ status,
1226
+ body: entry.body,
1227
+ createdAt: entry.createdAt,
1228
+ createdBy: entry.createdBy
1229
+ };
1230
+ }
1231
+ function supportRequestStatusForEntryType(entryType) {
1232
+ switch (entryType) {
1233
+ case "raised":
1234
+ case "reopened":
1235
+ return "open";
1236
+ case "triage":
1237
+ return "triaged";
1238
+ case "escalated":
1239
+ return "escalated";
1240
+ case "resolved":
1241
+ return "resolved";
1242
+ case "update":
1243
+ case "note":
1244
+ return undefined;
1245
+ }
1246
+ }
1247
+ function readSupportRequestStatus(record) {
1248
+ return record.fields.currentStatus && typeof record.fields.currentStatus === "object"
1249
+ ? record.fields.currentStatus
1250
+ : undefined;
1251
+ }
706
1252
  function assertDecisionEntryInput(input) {
707
1253
  if (input.entryType !== "decision" && (input.decidedBy || input.rationale)) {
708
1254
  throw new Error("decidedBy and rationale are only valid on decision entries.");
@@ -716,6 +1262,41 @@ function assertTaskEntryInput(input) {
716
1262
  throw new Error("status is only valid on status_change entries.");
717
1263
  }
718
1264
  }
1265
+ function assertRiskEntryInput(input) {
1266
+ if (input.entryType === "assessment" && (!input.likelihood || !input.impact)) {
1267
+ throw new Error("assessment entries require likelihood and impact.");
1268
+ }
1269
+ if (input.entryType !== "assessment" && (input.likelihood || input.impact)) {
1270
+ throw new Error("likelihood and impact are only valid on assessment entries.");
1271
+ }
1272
+ if (input.entryType === "treatment" && !input.treatment) {
1273
+ throw new Error("treatment entries require treatment.");
1274
+ }
1275
+ if (input.entryType !== "treatment" && (input.treatment || input.treatmentRationale)) {
1276
+ throw new Error("treatment and treatmentRationale are only valid on treatment entries.");
1277
+ }
1278
+ }
1279
+ function assertIncidentEntryInput(input) {
1280
+ if (input.entryType === "severity" && !input.severity) {
1281
+ throw new Error("severity entries require severity.");
1282
+ }
1283
+ if (input.entryType !== "severity" && input.severity) {
1284
+ throw new Error("severity is only valid on severity entries.");
1285
+ }
1286
+ }
1287
+ function assertProblemEntryInput(input) {
1288
+ if (input.entryType !== "root_cause" && input.rootCause) {
1289
+ throw new Error("rootCause is only valid on root_cause entries.");
1290
+ }
1291
+ }
1292
+ function assertSupportRequestEntryInput(input) {
1293
+ if (input.entryType === "escalated" && !input.incidentId) {
1294
+ throw new Error("escalated support request entries require incidentId.");
1295
+ }
1296
+ if (input.entryType !== "escalated" && input.incidentId) {
1297
+ throw new Error("incidentId is only valid on escalated support request entries.");
1298
+ }
1299
+ }
719
1300
  function applyDeferralEventState(existing, event, actorId, now) {
720
1301
  const conditions = normalizeDeferralConditions(existing.fields.conditions);
721
1302
  let status = typeof existing.fields.status === "string" ? existing.fields.status : "open";
@@ -936,7 +1517,7 @@ export async function updateRecord(db, input, actorId) {
936
1517
  const changedFields = listSnapshotChanges(previousSnapshot, nextSnapshot);
937
1518
  const existingVersionHistory = normalizeVersionHistory(existing.fields.versionHistory);
938
1519
  if (changedFields.length === 0) {
939
- return existing;
1520
+ return withDerivedRecordFields(db, existing);
940
1521
  }
941
1522
  set["fields.versionHistory"] = [
942
1523
  ...existingVersionHistory,
@@ -957,7 +1538,7 @@ export async function updateRecord(db, input, actorId) {
957
1538
  if (!updated) {
958
1539
  throw new Error(`Updated record not found: ${input.recordType}/${input.id}`);
959
1540
  }
960
- return updated;
1541
+ return withDerivedRecordFields(db, updated);
961
1542
  }
962
1543
  function applyRecordUpdate(record, input) {
963
1544
  const fields = { ...record.fields };
@@ -1096,7 +1677,7 @@ export async function archiveRecord(db, input, actorId) {
1096
1677
  if (!updated) {
1097
1678
  throw new Error(`Archived record not found: ${input.recordType}/${input.id}`);
1098
1679
  }
1099
- return updated;
1680
+ return withDerivedRecordFields(db, updated);
1100
1681
  }
1101
1682
  export async function linkRecords(db, input, actorId) {
1102
1683
  const sourceCollection = db.collection(input.fromType);
@@ -1132,7 +1713,7 @@ export async function linkRecords(db, input, actorId) {
1132
1713
  if (!updated) {
1133
1714
  throw new Error(`Updated source record not found: ${input.fromType}/${input.fromId}`);
1134
1715
  }
1135
- return updated;
1716
+ return withDerivedRecordFields(db, updated);
1136
1717
  }
1137
1718
  export async function unlinkRecords(db, input, actorId) {
1138
1719
  const sourceCollection = db.collection(input.fromType);
@@ -1168,7 +1749,7 @@ export async function unlinkRecords(db, input, actorId) {
1168
1749
  if (!updated) {
1169
1750
  throw new Error(`Updated source record not found: ${input.fromType}/${input.fromId}`);
1170
1751
  }
1171
- return updated;
1752
+ return withDerivedRecordFields(db, updated);
1172
1753
  }
1173
1754
  function assertFieldName(key) {
1174
1755
  if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(key)) {
@@ -1211,8 +1792,14 @@ function assertNotManagedField(recordType, key) {
1211
1792
  assertNotBenefitsManagedField(recordType, key);
1212
1793
  assertNotEnvironmentManagedField(recordType, key);
1213
1794
  assertNotComponentManagedField(recordType, key);
1795
+ assertNotMaintenanceScheduleManagedField(recordType, key);
1796
+ assertNotSupportRequestLedgerManagedField(recordType, key);
1797
+ assertNotValueRealizationManagedField(recordType, key);
1214
1798
  assertNotDecisionLedgerManagedField(recordType, key);
1215
1799
  assertNotTaskLedgerManagedField(recordType, key);
1800
+ assertNotRiskLedgerManagedField(recordType, key);
1801
+ assertNotIncidentLedgerManagedField(recordType, key);
1802
+ assertNotProblemLedgerManagedField(recordType, key);
1216
1803
  assertNotDecisionInputManagedField(recordType, key);
1217
1804
  assertNotJournalEntryManagedField(recordType, key);
1218
1805
  }
@@ -1276,6 +1863,24 @@ function assertNotComponentManagedField(recordType, key) {
1276
1863
  }
1277
1864
  throw new Error(`fields.${key} is managed on components. Use create_component or update_component instead of setting or unsetting it directly.`);
1278
1865
  }
1866
+ function assertNotMaintenanceScheduleManagedField(recordType, key) {
1867
+ if (recordType !== "maintenance_schedules" || !MAINTENANCE_SCHEDULE_MANAGED_FIELDS.includes(key)) {
1868
+ return;
1869
+ }
1870
+ throw new Error(`fields.${key} is managed on maintenance schedules. Use create_maintenance_schedule or update_maintenance_schedule instead of setting or unsetting it directly.`);
1871
+ }
1872
+ function assertNotSupportRequestLedgerManagedField(recordType, key) {
1873
+ if (recordType !== "support_requests" || !SUPPORT_REQUEST_LEDGER_MANAGED_FIELDS.includes(key)) {
1874
+ return;
1875
+ }
1876
+ throw new Error(`fields.${key} is managed on support requests. Use create_support_request or append_support_request_entry instead of setting or unsetting it directly.`);
1877
+ }
1878
+ function assertNotValueRealizationManagedField(recordType, key) {
1879
+ if (recordType !== "value_realizations" || !VALUE_REALIZATION_MANAGED_FIELDS.includes(key)) {
1880
+ return;
1881
+ }
1882
+ throw new Error(`fields.${key} is managed on value realizations. Use create_value_realization or update_value_realization instead of setting or unsetting it directly.`);
1883
+ }
1279
1884
  function assertNotDecisionLedgerManagedField(recordType, key) {
1280
1885
  if (recordType !== "decisions" || !DECISION_LEDGER_MANAGED_FIELDS.includes(key)) {
1281
1886
  return;
@@ -1288,6 +1893,24 @@ function assertNotTaskLedgerManagedField(recordType, key) {
1288
1893
  }
1289
1894
  throw new Error(`fields.${key} is managed on tasks. Use create_task or append_task_entry instead of setting or unsetting it directly.`);
1290
1895
  }
1896
+ function assertNotRiskLedgerManagedField(recordType, key) {
1897
+ if (recordType !== "risks" || !RISK_LEDGER_MANAGED_FIELDS.includes(key)) {
1898
+ return;
1899
+ }
1900
+ throw new Error(`fields.${key} is managed on risks. Use create_risk or append_risk_entry instead of setting or unsetting it directly.`);
1901
+ }
1902
+ function assertNotIncidentLedgerManagedField(recordType, key) {
1903
+ if (recordType !== "incidents" || !INCIDENT_LEDGER_MANAGED_FIELDS.includes(key)) {
1904
+ return;
1905
+ }
1906
+ throw new Error(`fields.${key} is managed on incidents. Use create_incident or append_incident_entry instead of setting or unsetting it directly.`);
1907
+ }
1908
+ function assertNotProblemLedgerManagedField(recordType, key) {
1909
+ if (recordType !== "problems" || !PROBLEM_LEDGER_MANAGED_FIELDS.includes(key)) {
1910
+ return;
1911
+ }
1912
+ throw new Error(`fields.${key} is managed on problems. Use create_problem or append_problem_entry instead of setting or unsetting it directly.`);
1913
+ }
1291
1914
  function assertNotDecisionInputManagedField(recordType, key) {
1292
1915
  if (recordType !== "decisions" || !DECISION_INPUT_MANAGED_FIELDS.includes(key)) {
1293
1916
  return;
@@ -1316,6 +1939,12 @@ function versionedUpdateToolName(recordType) {
1316
1939
  if (recordType === "components") {
1317
1940
  return "update_component";
1318
1941
  }
1942
+ if (recordType === "maintenance_schedules") {
1943
+ return "update_maintenance_schedule";
1944
+ }
1945
+ if (recordType === "value_realizations") {
1946
+ return "update_value_realization";
1947
+ }
1319
1948
  if (recordType === "estimates") {
1320
1949
  return "update_estimate";
1321
1950
  }