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/src/runtime/records.ts
CHANGED
|
@@ -16,6 +16,11 @@ export const COLLECTION_NAMES = [
|
|
|
16
16
|
"commitments",
|
|
17
17
|
"deferrals",
|
|
18
18
|
"changes",
|
|
19
|
+
"incidents",
|
|
20
|
+
"problems",
|
|
21
|
+
"maintenance_schedules",
|
|
22
|
+
"support_requests",
|
|
23
|
+
"value_realizations",
|
|
19
24
|
"decisions",
|
|
20
25
|
"risks"
|
|
21
26
|
] as const;
|
|
@@ -46,6 +51,11 @@ export const READABLE_ID_TYPE_CODES = {
|
|
|
46
51
|
deferrals: "DFR",
|
|
47
52
|
decisions: "DEC",
|
|
48
53
|
changes: "CHG",
|
|
54
|
+
incidents: "INC",
|
|
55
|
+
problems: "PRB",
|
|
56
|
+
maintenance_schedules: "MNT",
|
|
57
|
+
support_requests: "SUP",
|
|
58
|
+
value_realizations: "VAL",
|
|
49
59
|
risks: "RSK",
|
|
50
60
|
estimates: "EST",
|
|
51
61
|
benefits: "BFT"
|
|
@@ -90,6 +100,7 @@ export type DxcRecord = {
|
|
|
90
100
|
createdBy: string;
|
|
91
101
|
updatedAt: string;
|
|
92
102
|
updatedBy: string;
|
|
103
|
+
derived?: Record<string, unknown>;
|
|
93
104
|
};
|
|
94
105
|
|
|
95
106
|
export type ReviewableRecordType = "expectations" | "requirements";
|
|
@@ -123,7 +134,6 @@ export type RecordVersionHistoryEntry = {
|
|
|
123
134
|
export type ChangeEventType =
|
|
124
135
|
| "notice_given"
|
|
125
136
|
| "veto_recorded"
|
|
126
|
-
| "emergency_declared"
|
|
127
137
|
| "decision_recorded"
|
|
128
138
|
| "result_reported"
|
|
129
139
|
| "recovery_recorded"
|
|
@@ -185,6 +195,70 @@ export type TaskEntry = {
|
|
|
185
195
|
status?: TaskStatus;
|
|
186
196
|
};
|
|
187
197
|
|
|
198
|
+
export type RiskEntryType = "identified" | "assessment" | "treatment" | "monitor_note" | "closed" | "reopened";
|
|
199
|
+
export type RiskLevel = "low" | "medium" | "high";
|
|
200
|
+
export type RiskTreatment = "accept" | "mitigate" | "transfer" | "avoid";
|
|
201
|
+
|
|
202
|
+
export type RiskEntry = {
|
|
203
|
+
id: string;
|
|
204
|
+
entryType: RiskEntryType;
|
|
205
|
+
body: string;
|
|
206
|
+
createdAt: string;
|
|
207
|
+
createdBy: string;
|
|
208
|
+
likelihood?: RiskLevel;
|
|
209
|
+
impact?: RiskLevel;
|
|
210
|
+
treatment?: RiskTreatment;
|
|
211
|
+
treatmentRationale?: string;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export type IncidentEntryType = "detected" | "update" | "severity" | "resolved" | "reopened" | "note";
|
|
215
|
+
export type IncidentSeverity = "low" | "medium" | "high" | "critical";
|
|
216
|
+
|
|
217
|
+
export type IncidentEntry = {
|
|
218
|
+
id: string;
|
|
219
|
+
entryType: IncidentEntryType;
|
|
220
|
+
body: string;
|
|
221
|
+
createdAt: string;
|
|
222
|
+
createdBy: string;
|
|
223
|
+
severity?: IncidentSeverity;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export type ProblemEntryType =
|
|
227
|
+
| "identified"
|
|
228
|
+
| "investigation"
|
|
229
|
+
| "root_cause"
|
|
230
|
+
| "known_error"
|
|
231
|
+
| "resolved"
|
|
232
|
+
| "reopened"
|
|
233
|
+
| "note";
|
|
234
|
+
|
|
235
|
+
export type ProblemEntry = {
|
|
236
|
+
id: string;
|
|
237
|
+
entryType: ProblemEntryType;
|
|
238
|
+
body: string;
|
|
239
|
+
createdAt: string;
|
|
240
|
+
createdBy: string;
|
|
241
|
+
rootCause?: string;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export type SupportRequestEntryType =
|
|
245
|
+
| "raised"
|
|
246
|
+
| "triage"
|
|
247
|
+
| "update"
|
|
248
|
+
| "escalated"
|
|
249
|
+
| "resolved"
|
|
250
|
+
| "reopened"
|
|
251
|
+
| "note";
|
|
252
|
+
|
|
253
|
+
export type SupportRequestEntry = {
|
|
254
|
+
id: string;
|
|
255
|
+
entryType: SupportRequestEntryType;
|
|
256
|
+
body: string;
|
|
257
|
+
createdAt: string;
|
|
258
|
+
createdBy: string;
|
|
259
|
+
incidentId?: string;
|
|
260
|
+
};
|
|
261
|
+
|
|
188
262
|
export type JournalEntryKind = "note" | "summary";
|
|
189
263
|
|
|
190
264
|
export type AppendJournalNoteInput = {
|
|
@@ -282,6 +356,41 @@ export type AppendTaskEntryInput = {
|
|
|
282
356
|
status?: TaskStatus;
|
|
283
357
|
};
|
|
284
358
|
|
|
359
|
+
export type AppendRiskEntryInput = {
|
|
360
|
+
workspaceId: string;
|
|
361
|
+
riskId: string;
|
|
362
|
+
entryType: RiskEntryType;
|
|
363
|
+
body: string;
|
|
364
|
+
likelihood?: RiskLevel;
|
|
365
|
+
impact?: RiskLevel;
|
|
366
|
+
treatment?: RiskTreatment;
|
|
367
|
+
treatmentRationale?: string;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
export type AppendIncidentEntryInput = {
|
|
371
|
+
workspaceId: string;
|
|
372
|
+
incidentId: string;
|
|
373
|
+
entryType: IncidentEntryType;
|
|
374
|
+
body: string;
|
|
375
|
+
severity?: IncidentSeverity;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export type AppendProblemEntryInput = {
|
|
379
|
+
workspaceId: string;
|
|
380
|
+
problemId: string;
|
|
381
|
+
entryType: ProblemEntryType;
|
|
382
|
+
body: string;
|
|
383
|
+
rootCause?: string;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export type AppendSupportRequestEntryInput = {
|
|
387
|
+
workspaceId: string;
|
|
388
|
+
supportRequestId: string;
|
|
389
|
+
entryType: SupportRequestEntryType;
|
|
390
|
+
body: string;
|
|
391
|
+
incidentId?: string;
|
|
392
|
+
};
|
|
393
|
+
|
|
285
394
|
export type UnlinkRecordsInput = {
|
|
286
395
|
fromType: CollectionName;
|
|
287
396
|
fromId: string;
|
|
@@ -374,7 +483,9 @@ const VERSIONED_RECORD_TYPES = [
|
|
|
374
483
|
"expectations",
|
|
375
484
|
"requirements",
|
|
376
485
|
"estimates",
|
|
377
|
-
"benefits"
|
|
486
|
+
"benefits",
|
|
487
|
+
"maintenance_schedules",
|
|
488
|
+
"value_realizations"
|
|
378
489
|
] as const;
|
|
379
490
|
const VERSION_HISTORY_FIELD = "versionHistory";
|
|
380
491
|
const CHANGE_MANAGED_FIELDS = [
|
|
@@ -382,6 +493,11 @@ const CHANGE_MANAGED_FIELDS = [
|
|
|
382
493
|
"executionSteps",
|
|
383
494
|
"rollbackPlan",
|
|
384
495
|
"riskImpact",
|
|
496
|
+
"changeType",
|
|
497
|
+
"impactGrade",
|
|
498
|
+
"emergencyImportance",
|
|
499
|
+
"emergencyImmediacy",
|
|
500
|
+
"emergencyRationaleGaps",
|
|
385
501
|
"plannedFor",
|
|
386
502
|
"events"
|
|
387
503
|
] as const;
|
|
@@ -399,6 +515,23 @@ const COMPONENT_MANAGED_FIELDS = [
|
|
|
399
515
|
"secretPointers",
|
|
400
516
|
"notes"
|
|
401
517
|
] as const;
|
|
518
|
+
const MAINTENANCE_SCHEDULE_MANAGED_FIELDS = [
|
|
519
|
+
"name",
|
|
520
|
+
"kind",
|
|
521
|
+
"cadence",
|
|
522
|
+
"startDate",
|
|
523
|
+
"rationale",
|
|
524
|
+
"notes"
|
|
525
|
+
] as const;
|
|
526
|
+
const SUPPORT_REQUEST_LEDGER_MANAGED_FIELDS = [
|
|
527
|
+
"reporter",
|
|
528
|
+
"kind",
|
|
529
|
+
"reportedExperience",
|
|
530
|
+
"entries",
|
|
531
|
+
"currentStatus",
|
|
532
|
+
"status"
|
|
533
|
+
] as const;
|
|
534
|
+
const VALUE_REALIZATION_MANAGED_FIELDS = ["metrics"] as const;
|
|
402
535
|
const DECISION_LEDGER_MANAGED_FIELDS = [
|
|
403
536
|
"matter",
|
|
404
537
|
"entries",
|
|
@@ -420,6 +553,32 @@ const TASK_LEDGER_MANAGED_FIELDS = [
|
|
|
420
553
|
"details",
|
|
421
554
|
"status"
|
|
422
555
|
] as const;
|
|
556
|
+
const RISK_LEDGER_MANAGED_FIELDS = [
|
|
557
|
+
"topic",
|
|
558
|
+
"entries",
|
|
559
|
+
"currentStatus",
|
|
560
|
+
"currentAssessment",
|
|
561
|
+
"currentTreatment",
|
|
562
|
+
"likelihood",
|
|
563
|
+
"impact",
|
|
564
|
+
"mitigation"
|
|
565
|
+
] as const;
|
|
566
|
+
const INCIDENT_LEDGER_MANAGED_FIELDS = [
|
|
567
|
+
"description",
|
|
568
|
+
"entries",
|
|
569
|
+
"currentStatus",
|
|
570
|
+
"currentSeverity",
|
|
571
|
+
"status",
|
|
572
|
+
"severity"
|
|
573
|
+
] as const;
|
|
574
|
+
const PROBLEM_LEDGER_MANAGED_FIELDS = [
|
|
575
|
+
"description",
|
|
576
|
+
"entries",
|
|
577
|
+
"currentStatus",
|
|
578
|
+
"currentRootCause",
|
|
579
|
+
"status",
|
|
580
|
+
"rootCause"
|
|
581
|
+
] as const;
|
|
423
582
|
const DECISION_INPUT_MANAGED_FIELDS = ["informedBy", "informedByIds", "inputRecords"] as const;
|
|
424
583
|
const JOURNAL_ENTRY_MANAGED_FIELDS = ["kind", "body", "tag", "covers", "coveredBySummaryId"] as const;
|
|
425
584
|
const JOURNAL_COMPACTION_THRESHOLD = 200;
|
|
@@ -430,7 +589,9 @@ const VERSIONED_TYPED_FIELDS: Partial<Record<CollectionName, string[]>> = {
|
|
|
430
589
|
estimates: ["lineItems", "rollup"],
|
|
431
590
|
benefits: ["benefitItems", "rollup"],
|
|
432
591
|
environments: ["name", "description"],
|
|
433
|
-
components: ["name", "environmentId", "kind", "locator", "identifiers", "secretPointers", "notes"]
|
|
592
|
+
components: ["name", "environmentId", "kind", "locator", "identifiers", "secretPointers", "notes"],
|
|
593
|
+
maintenance_schedules: ["name", "kind", "cadence", "startDate", "rationale", "notes"],
|
|
594
|
+
value_realizations: ["metrics"]
|
|
434
595
|
};
|
|
435
596
|
|
|
436
597
|
const RESERVED_RELATIONSHIP_FIELDS: Partial<Record<CollectionName, string[]>> = {
|
|
@@ -439,6 +600,7 @@ const RESERVED_RELATIONSHIP_FIELDS: Partial<Record<CollectionName, string[]>> =
|
|
|
439
600
|
deferrals: ["requirementIds", "expectationIds"],
|
|
440
601
|
estimates: ["requirementIds", "expectationIds"],
|
|
441
602
|
benefits: ["requirementIds", "expectationIds"],
|
|
603
|
+
value_realizations: ["requirementIds", "expectationIds", "commitmentIds"],
|
|
442
604
|
expectations: ["statementId"],
|
|
443
605
|
requirements: ["expectationId"],
|
|
444
606
|
tasks: ["requirementId"]
|
|
@@ -503,11 +665,11 @@ export async function createRecord(
|
|
|
503
665
|
} finally {
|
|
504
666
|
await session.endSession();
|
|
505
667
|
}
|
|
506
|
-
return record;
|
|
668
|
+
return withDerivedRecordFields(db, record);
|
|
507
669
|
}
|
|
508
670
|
|
|
509
671
|
await db.collection<DxcRecord>(recordType).insertOne(record);
|
|
510
|
-
return record;
|
|
672
|
+
return withDerivedRecordFields(db, record);
|
|
511
673
|
}
|
|
512
674
|
|
|
513
675
|
export async function listRecords(
|
|
@@ -526,12 +688,14 @@ export async function listRecords(
|
|
|
526
688
|
filter.archivedAt = { $exists: false };
|
|
527
689
|
}
|
|
528
690
|
|
|
529
|
-
|
|
691
|
+
const records = await db
|
|
530
692
|
.collection<DxcRecord>(recordType)
|
|
531
693
|
.find(filter)
|
|
532
694
|
.sort({ createdAt: -1 })
|
|
533
695
|
.limit(limit)
|
|
534
696
|
.toArray();
|
|
697
|
+
|
|
698
|
+
return Promise.all(records.map((record) => withDerivedRecordFields(db, record)));
|
|
535
699
|
}
|
|
536
700
|
|
|
537
701
|
export async function getRecord(
|
|
@@ -550,7 +714,8 @@ export async function getRecord(
|
|
|
550
714
|
filter.workspaceId = readRequiredWorkspaceId(options.workspaceId, recordType);
|
|
551
715
|
}
|
|
552
716
|
|
|
553
|
-
|
|
717
|
+
const record = await db.collection<DxcRecord>(recordType).findOne(filter);
|
|
718
|
+
return record ? withDerivedRecordFields(db, record) : null;
|
|
554
719
|
}
|
|
555
720
|
|
|
556
721
|
export async function listLinkedRecords(
|
|
@@ -648,6 +813,266 @@ export async function listLinkedRecords(
|
|
|
648
813
|
return result;
|
|
649
814
|
}
|
|
650
815
|
|
|
816
|
+
async function withDerivedRecordFields(db: Db, record: DxcRecord): Promise<DxcRecord> {
|
|
817
|
+
if (record.recordType === "maintenance_schedules") {
|
|
818
|
+
return {
|
|
819
|
+
...record,
|
|
820
|
+
derived: await deriveMaintenanceScheduleState(db, record)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (record.recordType === "value_realizations") {
|
|
825
|
+
return {
|
|
826
|
+
...record,
|
|
827
|
+
derived: deriveValueRealizationState(record)
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return record;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function deriveMaintenanceScheduleState(db: Db, record: DxcRecord): Promise<Record<string, unknown>> {
|
|
835
|
+
const cadence = parseMaintenanceCadence(record.fields.cadence);
|
|
836
|
+
const startDate = parseDateString(record.fields.startDate);
|
|
837
|
+
const occurrences = await findMaintenanceOccurrences(db, record);
|
|
838
|
+
const latestOccurrence = occurrences[0];
|
|
839
|
+
const latestOccurrenceAt = latestOccurrence?.occurredAt;
|
|
840
|
+
const basis = latestOccurrenceAt ?? startDate?.toISOString();
|
|
841
|
+
const nextDueAt = cadence && basis ? addMaintenanceCadence(new Date(basis), cadence).toISOString() : undefined;
|
|
842
|
+
|
|
843
|
+
return compactRuntimeObject({
|
|
844
|
+
occurrenceCount: occurrences.length,
|
|
845
|
+
latestOccurrence,
|
|
846
|
+
cadence: cadence ?? undefined,
|
|
847
|
+
startDate: startDate?.toISOString(),
|
|
848
|
+
nextDueAt,
|
|
849
|
+
overdue: nextDueAt ? new Date() > new Date(nextDueAt) : undefined
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function findMaintenanceOccurrences(
|
|
854
|
+
db: Db,
|
|
855
|
+
schedule: DxcRecord
|
|
856
|
+
): Promise<Array<{ recordType: "changes" | "tasks"; id: string; readableId?: string; occurredAt: string }>> {
|
|
857
|
+
const workspaceId = schedule.workspaceId;
|
|
858
|
+
if (!workspaceId) {
|
|
859
|
+
return [];
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const linkFilter = {
|
|
863
|
+
workspaceId,
|
|
864
|
+
archivedAt: { $exists: false },
|
|
865
|
+
links: {
|
|
866
|
+
$elemMatch: {
|
|
867
|
+
toType: "maintenance_schedules",
|
|
868
|
+
toId: schedule._id,
|
|
869
|
+
relationship: "assigned_to"
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
const [changes, tasks] = await Promise.all([
|
|
874
|
+
db.collection<DxcRecord>("changes").find(linkFilter).toArray(),
|
|
875
|
+
db.collection<DxcRecord>("tasks").find(linkFilter).toArray()
|
|
876
|
+
]);
|
|
877
|
+
const occurrences = [
|
|
878
|
+
...changes.flatMap((record) => occurrenceFromChange(record)),
|
|
879
|
+
...tasks.flatMap((record) => occurrenceFromTask(record))
|
|
880
|
+
];
|
|
881
|
+
|
|
882
|
+
return occurrences.sort((left, right) => right.occurredAt.localeCompare(left.occurredAt));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function occurrenceFromChange(record: DxcRecord): Array<{ recordType: "changes"; id: string; readableId?: string; occurredAt: string }> {
|
|
886
|
+
const events = Array.isArray(record.fields.events) ? record.fields.events : [];
|
|
887
|
+
const event = [...events]
|
|
888
|
+
.reverse()
|
|
889
|
+
.find((entry): entry is Record<string, unknown> =>
|
|
890
|
+
Boolean(
|
|
891
|
+
entry &&
|
|
892
|
+
typeof entry === "object" &&
|
|
893
|
+
(entry.eventType === "result_reported" || entry.eventType === "recovery_recorded") &&
|
|
894
|
+
typeof entry.createdAt === "string"
|
|
895
|
+
)
|
|
896
|
+
);
|
|
897
|
+
const plannedFor = typeof record.fields.plannedFor === "string" ? record.fields.plannedFor : undefined;
|
|
898
|
+
const occurredAt = typeof event?.createdAt === "string" ? event.createdAt : plannedFor ?? record.createdAt;
|
|
899
|
+
|
|
900
|
+
return [{ recordType: "changes", id: record._id, readableId: record.readableId, occurredAt }];
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function occurrenceFromTask(record: DxcRecord): Array<{ recordType: "tasks"; id: string; readableId?: string; occurredAt: string }> {
|
|
904
|
+
const entries = Array.isArray(record.fields.entries) ? record.fields.entries : [];
|
|
905
|
+
const doneEntry = [...entries]
|
|
906
|
+
.reverse()
|
|
907
|
+
.find((entry): entry is Record<string, unknown> =>
|
|
908
|
+
Boolean(
|
|
909
|
+
entry &&
|
|
910
|
+
typeof entry === "object" &&
|
|
911
|
+
entry.entryType === "status_change" &&
|
|
912
|
+
entry.status === "done" &&
|
|
913
|
+
typeof entry.createdAt === "string"
|
|
914
|
+
)
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
const occurredAt = typeof doneEntry?.createdAt === "string" ? doneEntry.createdAt : record.createdAt;
|
|
918
|
+
return [{ recordType: "tasks", id: record._id, readableId: record.readableId, occurredAt }];
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
type MaintenanceCadence = {
|
|
922
|
+
count: number;
|
|
923
|
+
unit: "day" | "week" | "month" | "quarter" | "year";
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
function parseMaintenanceCadence(value: unknown): MaintenanceCadence | undefined {
|
|
927
|
+
if (!value || typeof value !== "object") {
|
|
928
|
+
return undefined;
|
|
929
|
+
}
|
|
930
|
+
const cadence = value as Partial<MaintenanceCadence>;
|
|
931
|
+
if (
|
|
932
|
+
Number.isInteger(cadence.count) &&
|
|
933
|
+
(cadence.count ?? 0) > 0 &&
|
|
934
|
+
(cadence.unit === "day" ||
|
|
935
|
+
cadence.unit === "week" ||
|
|
936
|
+
cadence.unit === "month" ||
|
|
937
|
+
cadence.unit === "quarter" ||
|
|
938
|
+
cadence.unit === "year")
|
|
939
|
+
) {
|
|
940
|
+
return { count: cadence.count as number, unit: cadence.unit };
|
|
941
|
+
}
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function parseDateString(value: unknown): Date | undefined {
|
|
946
|
+
if (typeof value !== "string") {
|
|
947
|
+
return undefined;
|
|
948
|
+
}
|
|
949
|
+
const date = new Date(value);
|
|
950
|
+
return Number.isNaN(date.getTime()) ? undefined : date;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function addMaintenanceCadence(date: Date, cadence: MaintenanceCadence): Date {
|
|
954
|
+
const next = new Date(date);
|
|
955
|
+
switch (cadence.unit) {
|
|
956
|
+
case "day":
|
|
957
|
+
next.setUTCDate(next.getUTCDate() + cadence.count);
|
|
958
|
+
return next;
|
|
959
|
+
case "week":
|
|
960
|
+
next.setUTCDate(next.getUTCDate() + cadence.count * 7);
|
|
961
|
+
return next;
|
|
962
|
+
case "month":
|
|
963
|
+
next.setUTCMonth(next.getUTCMonth() + cadence.count);
|
|
964
|
+
return next;
|
|
965
|
+
case "quarter":
|
|
966
|
+
next.setUTCMonth(next.getUTCMonth() + cadence.count * 3);
|
|
967
|
+
return next;
|
|
968
|
+
case "year":
|
|
969
|
+
next.setUTCFullYear(next.getUTCFullYear() + cadence.count);
|
|
970
|
+
return next;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function deriveValueRealizationState(record: DxcRecord): Record<string, unknown> {
|
|
975
|
+
const metrics = normalizeValueRealizationMetrics(record.fields.metrics);
|
|
976
|
+
const comparisons = metrics.map((metric) => {
|
|
977
|
+
if (!metric.actual) {
|
|
978
|
+
return {
|
|
979
|
+
id: metric.id,
|
|
980
|
+
name: metric.name,
|
|
981
|
+
unit: metric.unit,
|
|
982
|
+
direction: metric.direction,
|
|
983
|
+
status: "open"
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const absoluteChange = metric.actual.value - metric.baseline.value;
|
|
988
|
+
const ratio = metric.baseline.value === 0 ? null : metric.actual.value / metric.baseline.value;
|
|
989
|
+
const percentChange = ratio === null ? null : (ratio - 1) * 100;
|
|
990
|
+
const outcome =
|
|
991
|
+
absoluteChange === 0
|
|
992
|
+
? "unchanged"
|
|
993
|
+
: metric.direction === "higher_is_better"
|
|
994
|
+
? absoluteChange > 0
|
|
995
|
+
? "improved"
|
|
996
|
+
: "regressed"
|
|
997
|
+
: absoluteChange < 0
|
|
998
|
+
? "improved"
|
|
999
|
+
: "regressed";
|
|
1000
|
+
|
|
1001
|
+
return {
|
|
1002
|
+
id: metric.id,
|
|
1003
|
+
name: metric.name,
|
|
1004
|
+
unit: metric.unit,
|
|
1005
|
+
direction: metric.direction,
|
|
1006
|
+
status: "measured",
|
|
1007
|
+
absoluteChange,
|
|
1008
|
+
percentChange,
|
|
1009
|
+
ratio,
|
|
1010
|
+
outcome,
|
|
1011
|
+
baselineMeasuredAt: metric.baseline.measuredAt,
|
|
1012
|
+
actualMeasuredAt: metric.actual.measuredAt
|
|
1013
|
+
};
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
metricCount: metrics.length,
|
|
1018
|
+
openMetricCount: comparisons.filter((comparison) => comparison.status === "open").length,
|
|
1019
|
+
measuredMetricCount: comparisons.filter((comparison) => comparison.status === "measured").length,
|
|
1020
|
+
comparisons
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
type ValueRealizationMetric = {
|
|
1025
|
+
id: string;
|
|
1026
|
+
name: string;
|
|
1027
|
+
unit: string;
|
|
1028
|
+
direction: "lower_is_better" | "higher_is_better";
|
|
1029
|
+
baseline: {
|
|
1030
|
+
value: number;
|
|
1031
|
+
measuredAt: string;
|
|
1032
|
+
};
|
|
1033
|
+
actual?: {
|
|
1034
|
+
value: number;
|
|
1035
|
+
measuredAt: string;
|
|
1036
|
+
};
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
function normalizeValueRealizationMetrics(value: unknown): ValueRealizationMetric[] {
|
|
1040
|
+
if (!Array.isArray(value)) {
|
|
1041
|
+
return [];
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return value.filter(isValueRealizationMetric);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function isValueRealizationMetric(value: unknown): value is ValueRealizationMetric {
|
|
1048
|
+
if (!value || typeof value !== "object") {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
const metric = value as ValueRealizationMetric;
|
|
1052
|
+
return (
|
|
1053
|
+
typeof metric.id === "string" &&
|
|
1054
|
+
typeof metric.name === "string" &&
|
|
1055
|
+
typeof metric.unit === "string" &&
|
|
1056
|
+
(metric.direction === "lower_is_better" || metric.direction === "higher_is_better") &&
|
|
1057
|
+
isMeasuredValue(metric.baseline) &&
|
|
1058
|
+
(metric.actual === undefined || isMeasuredValue(metric.actual))
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function isMeasuredValue(value: unknown): value is { value: number; measuredAt: string } {
|
|
1063
|
+
return (
|
|
1064
|
+
Boolean(value) &&
|
|
1065
|
+
typeof value === "object" &&
|
|
1066
|
+
typeof (value as { value?: unknown }).value === "number" &&
|
|
1067
|
+
Number.isFinite((value as { value: number }).value) &&
|
|
1068
|
+
typeof (value as { measuredAt?: unknown }).measuredAt === "string"
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function compactRuntimeObject(input: Record<string, unknown>): Record<string, unknown> {
|
|
1073
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
1074
|
+
}
|
|
1075
|
+
|
|
651
1076
|
export async function migrateLegacyIntakeItemsToDxcompleteTickets(db: Db): Promise<{ copied: number; skipped: number }> {
|
|
652
1077
|
const legacyCollection = db.collection<DxcRecord>(LEGACY_INTAKE_COLLECTION_NAME);
|
|
653
1078
|
const ticketCollection = db.collection<DxcRecord>(DXCOMPLETE_TICKET_COLLECTION_NAME);
|
|
@@ -1204,6 +1629,233 @@ export async function appendTaskEntry(
|
|
|
1204
1629
|
return updated;
|
|
1205
1630
|
}
|
|
1206
1631
|
|
|
1632
|
+
export async function appendRiskEntry(
|
|
1633
|
+
db: Db,
|
|
1634
|
+
input: AppendRiskEntryInput,
|
|
1635
|
+
actorId: string
|
|
1636
|
+
): Promise<DxcRecord> {
|
|
1637
|
+
const collection = db.collection<DxcRecord>("risks");
|
|
1638
|
+
const filter = scopedRecordFilter("risks", input.riskId, input.workspaceId);
|
|
1639
|
+
const existing = await collection.findOne(filter);
|
|
1640
|
+
|
|
1641
|
+
if (!existing) {
|
|
1642
|
+
throw new Error(`Record not found: risks/${input.riskId}`);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
assertRiskEntryInput(input);
|
|
1646
|
+
|
|
1647
|
+
const now = new Date().toISOString();
|
|
1648
|
+
const entry: RiskEntry = {
|
|
1649
|
+
id: randomUUID(),
|
|
1650
|
+
entryType: input.entryType,
|
|
1651
|
+
body: input.body,
|
|
1652
|
+
createdAt: now,
|
|
1653
|
+
createdBy: actorId,
|
|
1654
|
+
...(input.likelihood ? { likelihood: input.likelihood } : {}),
|
|
1655
|
+
...(input.impact ? { impact: input.impact } : {}),
|
|
1656
|
+
...(input.treatment ? { treatment: input.treatment } : {}),
|
|
1657
|
+
...(input.treatmentRationale ? { treatmentRationale: input.treatmentRationale } : {})
|
|
1658
|
+
};
|
|
1659
|
+
const update: Record<string, unknown> = {
|
|
1660
|
+
$push: {
|
|
1661
|
+
"fields.entries": entry
|
|
1662
|
+
},
|
|
1663
|
+
$set: {
|
|
1664
|
+
updatedAt: now,
|
|
1665
|
+
updatedBy: actorId
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
|
|
1669
|
+
if (entry.entryType === "identified" || entry.entryType === "reopened") {
|
|
1670
|
+
(update.$set as Record<string, unknown>)["fields.currentStatus"] = riskEntryToCurrentStatus(entry, "open");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
if (entry.entryType === "closed") {
|
|
1674
|
+
(update.$set as Record<string, unknown>)["fields.currentStatus"] = riskEntryToCurrentStatus(entry, "closed");
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
if (entry.entryType === "assessment") {
|
|
1678
|
+
(update.$set as Record<string, unknown>)["fields.currentAssessment"] = riskEntryToCurrentAssessment(entry);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (entry.entryType === "treatment") {
|
|
1682
|
+
(update.$set as Record<string, unknown>)["fields.currentTreatment"] = riskEntryToCurrentTreatment(entry);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
await collection.updateOne(filter, update);
|
|
1686
|
+
|
|
1687
|
+
const updated = await getRecord(db, "risks", input.riskId, { workspaceId: input.workspaceId });
|
|
1688
|
+
if (!updated) {
|
|
1689
|
+
throw new Error(`Record not found after risk entry append: risks/${input.riskId}`);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
return updated;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
export async function appendIncidentEntry(
|
|
1696
|
+
db: Db,
|
|
1697
|
+
input: AppendIncidentEntryInput,
|
|
1698
|
+
actorId: string
|
|
1699
|
+
): Promise<DxcRecord> {
|
|
1700
|
+
const collection = db.collection<DxcRecord>("incidents");
|
|
1701
|
+
const filter = scopedRecordFilter("incidents", input.incidentId, input.workspaceId);
|
|
1702
|
+
const existing = await collection.findOne(filter);
|
|
1703
|
+
|
|
1704
|
+
if (!existing) {
|
|
1705
|
+
throw new Error(`Record not found: incidents/${input.incidentId}`);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
assertIncidentEntryInput(input);
|
|
1709
|
+
|
|
1710
|
+
const now = new Date().toISOString();
|
|
1711
|
+
const entry: IncidentEntry = {
|
|
1712
|
+
id: randomUUID(),
|
|
1713
|
+
entryType: input.entryType,
|
|
1714
|
+
body: input.body,
|
|
1715
|
+
createdAt: now,
|
|
1716
|
+
createdBy: actorId,
|
|
1717
|
+
...(input.severity ? { severity: input.severity } : {})
|
|
1718
|
+
};
|
|
1719
|
+
const update: Record<string, unknown> = {
|
|
1720
|
+
$push: {
|
|
1721
|
+
"fields.entries": entry
|
|
1722
|
+
},
|
|
1723
|
+
$set: {
|
|
1724
|
+
updatedAt: now,
|
|
1725
|
+
updatedBy: actorId
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
|
|
1729
|
+
if (entry.entryType === "detected" || entry.entryType === "reopened") {
|
|
1730
|
+
(update.$set as Record<string, unknown>)["fields.currentStatus"] = incidentEntryToCurrentStatus(entry, "open");
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
if (entry.entryType === "resolved") {
|
|
1734
|
+
(update.$set as Record<string, unknown>)["fields.currentStatus"] = incidentEntryToCurrentStatus(entry, "resolved");
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
if (entry.entryType === "severity") {
|
|
1738
|
+
(update.$set as Record<string, unknown>)["fields.currentSeverity"] = incidentEntryToCurrentSeverity(entry);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
await collection.updateOne(filter, update);
|
|
1742
|
+
|
|
1743
|
+
const updated = await getRecord(db, "incidents", input.incidentId, { workspaceId: input.workspaceId });
|
|
1744
|
+
if (!updated) {
|
|
1745
|
+
throw new Error(`Record not found after incident entry append: incidents/${input.incidentId}`);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
return updated;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
export async function appendProblemEntry(
|
|
1752
|
+
db: Db,
|
|
1753
|
+
input: AppendProblemEntryInput,
|
|
1754
|
+
actorId: string
|
|
1755
|
+
): Promise<DxcRecord> {
|
|
1756
|
+
const collection = db.collection<DxcRecord>("problems");
|
|
1757
|
+
const filter = scopedRecordFilter("problems", input.problemId, input.workspaceId);
|
|
1758
|
+
const existing = await collection.findOne(filter);
|
|
1759
|
+
|
|
1760
|
+
if (!existing) {
|
|
1761
|
+
throw new Error(`Record not found: problems/${input.problemId}`);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
assertProblemEntryInput(input);
|
|
1765
|
+
|
|
1766
|
+
const now = new Date().toISOString();
|
|
1767
|
+
const entry: ProblemEntry = {
|
|
1768
|
+
id: randomUUID(),
|
|
1769
|
+
entryType: input.entryType,
|
|
1770
|
+
body: input.body,
|
|
1771
|
+
createdAt: now,
|
|
1772
|
+
createdBy: actorId,
|
|
1773
|
+
...(input.rootCause ? { rootCause: input.rootCause } : {})
|
|
1774
|
+
};
|
|
1775
|
+
const update: Record<string, unknown> = {
|
|
1776
|
+
$push: {
|
|
1777
|
+
"fields.entries": entry
|
|
1778
|
+
},
|
|
1779
|
+
$set: {
|
|
1780
|
+
updatedAt: now,
|
|
1781
|
+
updatedBy: actorId
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
if (entry.entryType === "identified" || entry.entryType === "reopened") {
|
|
1786
|
+
(update.$set as Record<string, unknown>)["fields.currentStatus"] = problemEntryToCurrentStatus(entry, "open");
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
if (entry.entryType === "known_error") {
|
|
1790
|
+
(update.$set as Record<string, unknown>)["fields.currentStatus"] = problemEntryToCurrentStatus(entry, "known_error");
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
if (entry.entryType === "resolved") {
|
|
1794
|
+
(update.$set as Record<string, unknown>)["fields.currentStatus"] = problemEntryToCurrentStatus(entry, "resolved");
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
if (entry.entryType === "root_cause") {
|
|
1798
|
+
(update.$set as Record<string, unknown>)["fields.currentRootCause"] = problemEntryToCurrentRootCause(entry);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
await collection.updateOne(filter, update);
|
|
1802
|
+
|
|
1803
|
+
const updated = await getRecord(db, "problems", input.problemId, { workspaceId: input.workspaceId });
|
|
1804
|
+
if (!updated) {
|
|
1805
|
+
throw new Error(`Record not found after problem entry append: problems/${input.problemId}`);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
return updated;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
export async function appendSupportRequestEntry(
|
|
1812
|
+
db: Db,
|
|
1813
|
+
input: AppendSupportRequestEntryInput,
|
|
1814
|
+
actorId: string
|
|
1815
|
+
): Promise<DxcRecord> {
|
|
1816
|
+
const collection = db.collection<DxcRecord>("support_requests");
|
|
1817
|
+
const filter = scopedRecordFilter("support_requests", input.supportRequestId, input.workspaceId);
|
|
1818
|
+
const existing = await collection.findOne(filter);
|
|
1819
|
+
|
|
1820
|
+
if (!existing) {
|
|
1821
|
+
throw new Error(`Record not found: support_requests/${input.supportRequestId}`);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
assertSupportRequestEntryInput(input);
|
|
1825
|
+
|
|
1826
|
+
const now = new Date().toISOString();
|
|
1827
|
+
const entry: SupportRequestEntry = {
|
|
1828
|
+
id: randomUUID(),
|
|
1829
|
+
entryType: input.entryType,
|
|
1830
|
+
body: input.body,
|
|
1831
|
+
createdAt: now,
|
|
1832
|
+
createdBy: actorId,
|
|
1833
|
+
...(input.incidentId ? { incidentId: input.incidentId } : {})
|
|
1834
|
+
};
|
|
1835
|
+
const update: Record<string, unknown> = {
|
|
1836
|
+
$push: {
|
|
1837
|
+
"fields.entries": entry
|
|
1838
|
+
},
|
|
1839
|
+
$set: {
|
|
1840
|
+
updatedAt: now,
|
|
1841
|
+
updatedBy: actorId
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
const currentStatus = supportRequestEntryToCurrentStatus(entry, readSupportRequestStatus(existing));
|
|
1845
|
+
if (currentStatus) {
|
|
1846
|
+
(update.$set as Record<string, unknown>)["fields.currentStatus"] = currentStatus;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
await collection.updateOne(filter, update);
|
|
1850
|
+
|
|
1851
|
+
const updated = await getRecord(db, "support_requests", input.supportRequestId, { workspaceId: input.workspaceId });
|
|
1852
|
+
if (!updated) {
|
|
1853
|
+
throw new Error(`Record not found after support request entry append: support_requests/${input.supportRequestId}`);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
return updated;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1207
1859
|
export function decisionEntryToCurrentDecision(entry: DecisionEntry): Record<string, unknown> {
|
|
1208
1860
|
return {
|
|
1209
1861
|
entryId: entry.id,
|
|
@@ -1229,6 +1881,138 @@ export function taskEntryToCurrentStatus(entry: TaskEntry): Record<string, unkno
|
|
|
1229
1881
|
};
|
|
1230
1882
|
}
|
|
1231
1883
|
|
|
1884
|
+
export function riskEntryToCurrentStatus(entry: RiskEntry, status: "open" | "closed"): Record<string, unknown> {
|
|
1885
|
+
return {
|
|
1886
|
+
entryId: entry.id,
|
|
1887
|
+
status,
|
|
1888
|
+
body: entry.body,
|
|
1889
|
+
createdAt: entry.createdAt,
|
|
1890
|
+
createdBy: entry.createdBy
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
export function riskEntryToCurrentAssessment(entry: RiskEntry): Record<string, unknown> {
|
|
1895
|
+
if (entry.entryType !== "assessment" || !entry.likelihood || !entry.impact) {
|
|
1896
|
+
throw new Error("Risk current assessment can only derive from an assessment entry with likelihood and impact.");
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
return {
|
|
1900
|
+
entryId: entry.id,
|
|
1901
|
+
likelihood: entry.likelihood,
|
|
1902
|
+
impact: entry.impact,
|
|
1903
|
+
body: entry.body,
|
|
1904
|
+
createdAt: entry.createdAt,
|
|
1905
|
+
createdBy: entry.createdBy
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
export function riskEntryToCurrentTreatment(entry: RiskEntry): Record<string, unknown> {
|
|
1910
|
+
if (entry.entryType !== "treatment" || !entry.treatment) {
|
|
1911
|
+
throw new Error("Risk current treatment can only derive from a treatment entry.");
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
return {
|
|
1915
|
+
entryId: entry.id,
|
|
1916
|
+
treatment: entry.treatment,
|
|
1917
|
+
body: entry.body,
|
|
1918
|
+
createdAt: entry.createdAt,
|
|
1919
|
+
createdBy: entry.createdBy,
|
|
1920
|
+
...(entry.treatmentRationale ? { treatmentRationale: entry.treatmentRationale } : {})
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
export function incidentEntryToCurrentStatus(entry: IncidentEntry, status: "open" | "resolved"): Record<string, unknown> {
|
|
1925
|
+
return {
|
|
1926
|
+
entryId: entry.id,
|
|
1927
|
+
status,
|
|
1928
|
+
body: entry.body,
|
|
1929
|
+
createdAt: entry.createdAt,
|
|
1930
|
+
createdBy: entry.createdBy
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
export function incidentEntryToCurrentSeverity(entry: IncidentEntry): Record<string, unknown> {
|
|
1935
|
+
if (entry.entryType !== "severity" || !entry.severity) {
|
|
1936
|
+
throw new Error("Incident current severity can only derive from a severity entry.");
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
return {
|
|
1940
|
+
entryId: entry.id,
|
|
1941
|
+
severity: entry.severity,
|
|
1942
|
+
body: entry.body,
|
|
1943
|
+
createdAt: entry.createdAt,
|
|
1944
|
+
createdBy: entry.createdBy
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
export function problemEntryToCurrentStatus(
|
|
1949
|
+
entry: ProblemEntry,
|
|
1950
|
+
status: "open" | "known_error" | "resolved"
|
|
1951
|
+
): Record<string, unknown> {
|
|
1952
|
+
return {
|
|
1953
|
+
entryId: entry.id,
|
|
1954
|
+
status,
|
|
1955
|
+
body: entry.body,
|
|
1956
|
+
createdAt: entry.createdAt,
|
|
1957
|
+
createdBy: entry.createdBy
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
export function problemEntryToCurrentRootCause(entry: ProblemEntry): Record<string, unknown> {
|
|
1962
|
+
if (entry.entryType !== "root_cause") {
|
|
1963
|
+
throw new Error("Problem current root cause can only derive from a root_cause entry.");
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
return {
|
|
1967
|
+
entryId: entry.id,
|
|
1968
|
+
rootCause: entry.rootCause ?? entry.body,
|
|
1969
|
+
body: entry.body,
|
|
1970
|
+
createdAt: entry.createdAt,
|
|
1971
|
+
createdBy: entry.createdBy
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
export function supportRequestEntryToCurrentStatus(
|
|
1976
|
+
entry: SupportRequestEntry,
|
|
1977
|
+
previousStatus?: Record<string, unknown>
|
|
1978
|
+
): Record<string, unknown> | undefined {
|
|
1979
|
+
const status = supportRequestStatusForEntryType(entry.entryType);
|
|
1980
|
+
if (!status) {
|
|
1981
|
+
return previousStatus;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
return {
|
|
1985
|
+
entryId: entry.id,
|
|
1986
|
+
status,
|
|
1987
|
+
body: entry.body,
|
|
1988
|
+
createdAt: entry.createdAt,
|
|
1989
|
+
createdBy: entry.createdBy
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function supportRequestStatusForEntryType(entryType: SupportRequestEntryType): "open" | "triaged" | "escalated" | "resolved" | undefined {
|
|
1994
|
+
switch (entryType) {
|
|
1995
|
+
case "raised":
|
|
1996
|
+
case "reopened":
|
|
1997
|
+
return "open";
|
|
1998
|
+
case "triage":
|
|
1999
|
+
return "triaged";
|
|
2000
|
+
case "escalated":
|
|
2001
|
+
return "escalated";
|
|
2002
|
+
case "resolved":
|
|
2003
|
+
return "resolved";
|
|
2004
|
+
case "update":
|
|
2005
|
+
case "note":
|
|
2006
|
+
return undefined;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function readSupportRequestStatus(record: DxcRecord): Record<string, unknown> | undefined {
|
|
2011
|
+
return record.fields.currentStatus && typeof record.fields.currentStatus === "object"
|
|
2012
|
+
? (record.fields.currentStatus as Record<string, unknown>)
|
|
2013
|
+
: undefined;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1232
2016
|
function assertDecisionEntryInput(input: AppendDecisionEntryInput): void {
|
|
1233
2017
|
if (input.entryType !== "decision" && (input.decidedBy || input.rationale)) {
|
|
1234
2018
|
throw new Error("decidedBy and rationale are only valid on decision entries.");
|
|
@@ -1245,6 +2029,50 @@ function assertTaskEntryInput(input: AppendTaskEntryInput): void {
|
|
|
1245
2029
|
}
|
|
1246
2030
|
}
|
|
1247
2031
|
|
|
2032
|
+
function assertRiskEntryInput(input: AppendRiskEntryInput): void {
|
|
2033
|
+
if (input.entryType === "assessment" && (!input.likelihood || !input.impact)) {
|
|
2034
|
+
throw new Error("assessment entries require likelihood and impact.");
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
if (input.entryType !== "assessment" && (input.likelihood || input.impact)) {
|
|
2038
|
+
throw new Error("likelihood and impact are only valid on assessment entries.");
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
if (input.entryType === "treatment" && !input.treatment) {
|
|
2042
|
+
throw new Error("treatment entries require treatment.");
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
if (input.entryType !== "treatment" && (input.treatment || input.treatmentRationale)) {
|
|
2046
|
+
throw new Error("treatment and treatmentRationale are only valid on treatment entries.");
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
function assertIncidentEntryInput(input: AppendIncidentEntryInput): void {
|
|
2051
|
+
if (input.entryType === "severity" && !input.severity) {
|
|
2052
|
+
throw new Error("severity entries require severity.");
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
if (input.entryType !== "severity" && input.severity) {
|
|
2056
|
+
throw new Error("severity is only valid on severity entries.");
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function assertProblemEntryInput(input: AppendProblemEntryInput): void {
|
|
2061
|
+
if (input.entryType !== "root_cause" && input.rootCause) {
|
|
2062
|
+
throw new Error("rootCause is only valid on root_cause entries.");
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
function assertSupportRequestEntryInput(input: AppendSupportRequestEntryInput): void {
|
|
2067
|
+
if (input.entryType === "escalated" && !input.incidentId) {
|
|
2068
|
+
throw new Error("escalated support request entries require incidentId.");
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
if (input.entryType !== "escalated" && input.incidentId) {
|
|
2072
|
+
throw new Error("incidentId is only valid on escalated support request entries.");
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1248
2076
|
function applyDeferralEventState(
|
|
1249
2077
|
existing: DxcRecord,
|
|
1250
2078
|
event: DeferralEvent,
|
|
@@ -1531,7 +2359,7 @@ export async function updateRecord(db: Db, input: UpdateRecordInput, actorId: st
|
|
|
1531
2359
|
const existingVersionHistory = normalizeVersionHistory(existing.fields.versionHistory);
|
|
1532
2360
|
|
|
1533
2361
|
if (changedFields.length === 0) {
|
|
1534
|
-
return existing;
|
|
2362
|
+
return withDerivedRecordFields(db, existing);
|
|
1535
2363
|
}
|
|
1536
2364
|
|
|
1537
2365
|
set["fields.versionHistory"] = [
|
|
@@ -1556,7 +2384,7 @@ export async function updateRecord(db: Db, input: UpdateRecordInput, actorId: st
|
|
|
1556
2384
|
throw new Error(`Updated record not found: ${input.recordType}/${input.id}`);
|
|
1557
2385
|
}
|
|
1558
2386
|
|
|
1559
|
-
return updated;
|
|
2387
|
+
return withDerivedRecordFields(db, updated);
|
|
1560
2388
|
}
|
|
1561
2389
|
|
|
1562
2390
|
function applyRecordUpdate(record: DxcRecord, input: UpdateRecordInput): DxcRecord {
|
|
@@ -1751,7 +2579,7 @@ export async function archiveRecord(db: Db, input: ArchiveRecordInput, actorId:
|
|
|
1751
2579
|
throw new Error(`Archived record not found: ${input.recordType}/${input.id}`);
|
|
1752
2580
|
}
|
|
1753
2581
|
|
|
1754
|
-
return updated;
|
|
2582
|
+
return withDerivedRecordFields(db, updated);
|
|
1755
2583
|
}
|
|
1756
2584
|
|
|
1757
2585
|
export async function linkRecords(
|
|
@@ -1809,7 +2637,7 @@ export async function linkRecords(
|
|
|
1809
2637
|
throw new Error(`Updated source record not found: ${input.fromType}/${input.fromId}`);
|
|
1810
2638
|
}
|
|
1811
2639
|
|
|
1812
|
-
return updated;
|
|
2640
|
+
return withDerivedRecordFields(db, updated);
|
|
1813
2641
|
}
|
|
1814
2642
|
|
|
1815
2643
|
export async function unlinkRecords(
|
|
@@ -1866,7 +2694,7 @@ export async function unlinkRecords(
|
|
|
1866
2694
|
throw new Error(`Updated source record not found: ${input.fromType}/${input.fromId}`);
|
|
1867
2695
|
}
|
|
1868
2696
|
|
|
1869
|
-
return updated;
|
|
2697
|
+
return withDerivedRecordFields(db, updated);
|
|
1870
2698
|
}
|
|
1871
2699
|
|
|
1872
2700
|
function assertFieldName(key: string): void {
|
|
@@ -1926,8 +2754,14 @@ function assertNotManagedField(recordType: CollectionName, key: string): void {
|
|
|
1926
2754
|
assertNotBenefitsManagedField(recordType, key);
|
|
1927
2755
|
assertNotEnvironmentManagedField(recordType, key);
|
|
1928
2756
|
assertNotComponentManagedField(recordType, key);
|
|
2757
|
+
assertNotMaintenanceScheduleManagedField(recordType, key);
|
|
2758
|
+
assertNotSupportRequestLedgerManagedField(recordType, key);
|
|
2759
|
+
assertNotValueRealizationManagedField(recordType, key);
|
|
1929
2760
|
assertNotDecisionLedgerManagedField(recordType, key);
|
|
1930
2761
|
assertNotTaskLedgerManagedField(recordType, key);
|
|
2762
|
+
assertNotRiskLedgerManagedField(recordType, key);
|
|
2763
|
+
assertNotIncidentLedgerManagedField(recordType, key);
|
|
2764
|
+
assertNotProblemLedgerManagedField(recordType, key);
|
|
1931
2765
|
assertNotDecisionInputManagedField(recordType, key);
|
|
1932
2766
|
assertNotJournalEntryManagedField(recordType, key);
|
|
1933
2767
|
}
|
|
@@ -2030,6 +2864,36 @@ function assertNotComponentManagedField(recordType: CollectionName, key: string)
|
|
|
2030
2864
|
);
|
|
2031
2865
|
}
|
|
2032
2866
|
|
|
2867
|
+
function assertNotMaintenanceScheduleManagedField(recordType: CollectionName, key: string): void {
|
|
2868
|
+
if (recordType !== "maintenance_schedules" || !(MAINTENANCE_SCHEDULE_MANAGED_FIELDS as readonly string[]).includes(key)) {
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
throw new Error(
|
|
2873
|
+
`fields.${key} is managed on maintenance schedules. Use create_maintenance_schedule or update_maintenance_schedule instead of setting or unsetting it directly.`
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
function assertNotSupportRequestLedgerManagedField(recordType: CollectionName, key: string): void {
|
|
2878
|
+
if (recordType !== "support_requests" || !(SUPPORT_REQUEST_LEDGER_MANAGED_FIELDS as readonly string[]).includes(key)) {
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
throw new Error(
|
|
2883
|
+
`fields.${key} is managed on support requests. Use create_support_request or append_support_request_entry instead of setting or unsetting it directly.`
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function assertNotValueRealizationManagedField(recordType: CollectionName, key: string): void {
|
|
2888
|
+
if (recordType !== "value_realizations" || !(VALUE_REALIZATION_MANAGED_FIELDS as readonly string[]).includes(key)) {
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
throw new Error(
|
|
2893
|
+
`fields.${key} is managed on value realizations. Use create_value_realization or update_value_realization instead of setting or unsetting it directly.`
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2033
2897
|
function assertNotDecisionLedgerManagedField(recordType: CollectionName, key: string): void {
|
|
2034
2898
|
if (recordType !== "decisions" || !(DECISION_LEDGER_MANAGED_FIELDS as readonly string[]).includes(key)) {
|
|
2035
2899
|
return;
|
|
@@ -2050,6 +2914,36 @@ function assertNotTaskLedgerManagedField(recordType: CollectionName, key: string
|
|
|
2050
2914
|
);
|
|
2051
2915
|
}
|
|
2052
2916
|
|
|
2917
|
+
function assertNotRiskLedgerManagedField(recordType: CollectionName, key: string): void {
|
|
2918
|
+
if (recordType !== "risks" || !(RISK_LEDGER_MANAGED_FIELDS as readonly string[]).includes(key)) {
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
throw new Error(
|
|
2923
|
+
`fields.${key} is managed on risks. Use create_risk or append_risk_entry instead of setting or unsetting it directly.`
|
|
2924
|
+
);
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
function assertNotIncidentLedgerManagedField(recordType: CollectionName, key: string): void {
|
|
2928
|
+
if (recordType !== "incidents" || !(INCIDENT_LEDGER_MANAGED_FIELDS as readonly string[]).includes(key)) {
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
throw new Error(
|
|
2933
|
+
`fields.${key} is managed on incidents. Use create_incident or append_incident_entry instead of setting or unsetting it directly.`
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
function assertNotProblemLedgerManagedField(recordType: CollectionName, key: string): void {
|
|
2938
|
+
if (recordType !== "problems" || !(PROBLEM_LEDGER_MANAGED_FIELDS as readonly string[]).includes(key)) {
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
throw new Error(
|
|
2943
|
+
`fields.${key} is managed on problems. Use create_problem or append_problem_entry instead of setting or unsetting it directly.`
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2053
2947
|
function assertNotDecisionInputManagedField(recordType: CollectionName, key: string): void {
|
|
2054
2948
|
if (recordType !== "decisions" || !(DECISION_INPUT_MANAGED_FIELDS as readonly string[]).includes(key)) {
|
|
2055
2949
|
return;
|
|
@@ -2091,6 +2985,14 @@ function versionedUpdateToolName(recordType: CollectionName): string {
|
|
|
2091
2985
|
return "update_component";
|
|
2092
2986
|
}
|
|
2093
2987
|
|
|
2988
|
+
if (recordType === "maintenance_schedules") {
|
|
2989
|
+
return "update_maintenance_schedule";
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
if (recordType === "value_realizations") {
|
|
2993
|
+
return "update_value_realization";
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2094
2996
|
if (recordType === "estimates") {
|
|
2095
2997
|
return "update_estimate";
|
|
2096
2998
|
}
|