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.
- package/README.md +22 -22
- package/dist/init.js +19 -0
- package/dist/mcp/docs.d.ts +18 -2
- package/dist/mcp/docs.js +201 -13
- package/dist/mcp/server.js +761 -47
- package/dist/runtime/check.d.ts +1 -1
- package/dist/runtime/records.d.ts +95 -4
- package/dist/runtime/records.js +640 -11
- package/dist/validate.js +3 -1
- package/docs/codex-integration.md +45 -18
- package/docs/glossary.md +39 -7
- package/docs/index.md +2 -1
- package/docs/model.md +3 -3
- package/docs/operating-guide.md +116 -0
- package/docs/taxonomy.md +12 -6
- package/docs/workflows.md +5 -3
- package/package.json +21 -3
- package/scripts/{dogfood-work-order.mjs → runtime-work-order.mjs} +4 -4
- package/scripts/smoke-mcp-http.mjs +460 -6
- package/src/init.ts +35 -0
- package/src/mcp/docs.ts +234 -14
- package/src/mcp/server.ts +1138 -83
- package/src/runtime/records.ts +914 -12
- package/src/validate.ts +3 -1
- package/templates/AGENTS.md +30 -0
- package/templates/process/controls.yml +10 -6
- package/templates/process/diagrams/01-intake-triage.mmd +5 -5
- package/templates/process/diagrams/02-product-definition.mmd +1 -1
- package/templates/process/diagrams/06-change-release-control.mmd +5 -7
- package/templates/process/diagrams/07-deployment-operations.mmd +2 -2
- package/templates/process/diagrams/08-support-incident-management.mmd +5 -4
- package/templates/process/diagrams/09-problem-improvement.mmd +4 -3
- package/templates/process/diagrams/10-risk-control-management.mmd +6 -4
- package/templates/process/diagrams/11-audit-evidence-capture.mmd +1 -1
- package/templates/process/taxonomy.yml +91 -17
- package/templates/process/workflows.yml +10 -9
- package/website/flow.html +1 -0
- package/website/glossary.html +37 -8
- package/website/index.html +2 -1
- package/website/objects.html +68 -11
- package/website/operating-guide.html +165 -0
- package/website/outcomes.html +1 -0
- package/website/phase-build.html +1 -0
- package/website/phase-elicit.html +1 -0
- package/website/phase-go-live.html +2 -1
- package/website/phase-measure.html +1 -0
- package/website/phase-operate.html +1 -0
- package/website/phase-orient.html +1 -0
- package/website/phase-weigh.html +1 -0
- package/website/roles.html +1 -0
package/dist/runtime/records.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|