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/mcp/server.ts
CHANGED
|
@@ -16,9 +16,13 @@ import {
|
|
|
16
16
|
appendChangeEvent,
|
|
17
17
|
appendDecisionEntry,
|
|
18
18
|
appendDeferralEvent,
|
|
19
|
+
appendIncidentEntry,
|
|
19
20
|
appendJournalNote,
|
|
20
21
|
appendJournalSummary,
|
|
22
|
+
appendProblemEntry,
|
|
21
23
|
appendReviewNote,
|
|
24
|
+
appendRiskEntry,
|
|
25
|
+
appendSupportRequestEntry,
|
|
22
26
|
appendTaskEntry,
|
|
23
27
|
archiveRecord,
|
|
24
28
|
appendDxcompleteTicket,
|
|
@@ -30,6 +34,9 @@ import {
|
|
|
30
34
|
createRecord,
|
|
31
35
|
getJournalEntry,
|
|
32
36
|
getRecord,
|
|
37
|
+
type IncidentEntry,
|
|
38
|
+
incidentEntryToCurrentSeverity,
|
|
39
|
+
incidentEntryToCurrentStatus,
|
|
33
40
|
linkRecords,
|
|
34
41
|
listDxcompleteTickets,
|
|
35
42
|
listLinkedRecords,
|
|
@@ -37,6 +44,15 @@ import {
|
|
|
37
44
|
listRecords,
|
|
38
45
|
readJournal,
|
|
39
46
|
readDxcompleteTicket,
|
|
47
|
+
type ProblemEntry,
|
|
48
|
+
problemEntryToCurrentRootCause,
|
|
49
|
+
problemEntryToCurrentStatus,
|
|
50
|
+
type RiskEntry,
|
|
51
|
+
riskEntryToCurrentAssessment,
|
|
52
|
+
riskEntryToCurrentStatus,
|
|
53
|
+
riskEntryToCurrentTreatment,
|
|
54
|
+
type SupportRequestEntry,
|
|
55
|
+
supportRequestEntryToCurrentStatus,
|
|
40
56
|
type TaskEntry,
|
|
41
57
|
taskEntryToCurrentStatus,
|
|
42
58
|
unlinkRecords,
|
|
@@ -60,6 +76,39 @@ const requirementStatusSchema = z.enum(["draft", "ready", "approved", "supersede
|
|
|
60
76
|
const taskStatusSchema = z.enum(["open", "in_progress", "blocked", "done"]);
|
|
61
77
|
const decisionEntryTypeSchema = z.enum(["argument", "decision", "note"]);
|
|
62
78
|
const taskEntryTypeSchema = z.enum(["comment", "status_change", "note"]);
|
|
79
|
+
const riskEntryTypeSchema = z.enum(["identified", "assessment", "treatment", "monitor_note", "closed", "reopened"]);
|
|
80
|
+
const riskLevelSchema = z.enum(["low", "medium", "high"]);
|
|
81
|
+
const riskTreatmentSchema = z.enum(["accept", "mitigate", "transfer", "avoid"]);
|
|
82
|
+
const incidentEntryTypeSchema = z.enum(["detected", "update", "severity", "resolved", "reopened", "note"]);
|
|
83
|
+
const incidentSeveritySchema = z.enum(["low", "medium", "high", "critical"]);
|
|
84
|
+
const problemEntryTypeSchema = z.enum([
|
|
85
|
+
"identified",
|
|
86
|
+
"investigation",
|
|
87
|
+
"root_cause",
|
|
88
|
+
"known_error",
|
|
89
|
+
"resolved",
|
|
90
|
+
"reopened",
|
|
91
|
+
"note"
|
|
92
|
+
]);
|
|
93
|
+
const supportRequestEntryTypeSchema = z.enum(["raised", "triage", "update", "escalated", "resolved", "reopened", "note"]);
|
|
94
|
+
const maintenanceCadenceSchema = z.object({
|
|
95
|
+
count: z.number().int().min(1),
|
|
96
|
+
unit: z.enum(["day", "week", "month", "quarter", "year"])
|
|
97
|
+
}).strict();
|
|
98
|
+
type MaintenanceCadenceInput = z.infer<typeof maintenanceCadenceSchema>;
|
|
99
|
+
const valueMetricMeasurementSchema = z.object({
|
|
100
|
+
value: z.number().finite(),
|
|
101
|
+
measuredAt: z.string().min(1)
|
|
102
|
+
}).strict();
|
|
103
|
+
const valueMetricSchema = z.object({
|
|
104
|
+
id: z.string().min(1).optional(),
|
|
105
|
+
name: z.string().min(1),
|
|
106
|
+
unit: z.string().min(1),
|
|
107
|
+
direction: z.enum(["lower_is_better", "higher_is_better"]),
|
|
108
|
+
baseline: valueMetricMeasurementSchema,
|
|
109
|
+
actual: valueMetricMeasurementSchema.optional()
|
|
110
|
+
}).strict();
|
|
111
|
+
type ValueMetricInput = z.infer<typeof valueMetricSchema>;
|
|
63
112
|
const reviewableRecordTypeSchema = z.enum(["expectations", "requirements"]);
|
|
64
113
|
const decisionInputRecordTypes = [
|
|
65
114
|
"expectations",
|
|
@@ -71,8 +120,13 @@ const decisionInputRecordTypes = [
|
|
|
71
120
|
"components",
|
|
72
121
|
"estimates",
|
|
73
122
|
"benefits",
|
|
123
|
+
"maintenance_schedules",
|
|
124
|
+
"support_requests",
|
|
125
|
+
"value_realizations",
|
|
74
126
|
"risks",
|
|
75
127
|
"changes",
|
|
128
|
+
"incidents",
|
|
129
|
+
"problems",
|
|
76
130
|
"decisions"
|
|
77
131
|
] as const;
|
|
78
132
|
const decisionInputRecordTypeSchema = z.enum(decisionInputRecordTypes);
|
|
@@ -80,16 +134,22 @@ type DecisionInputRecordType = (typeof decisionInputRecordTypes)[number];
|
|
|
80
134
|
const changeEventTypeSchema = z.enum([
|
|
81
135
|
"notice_given",
|
|
82
136
|
"veto_recorded",
|
|
83
|
-
"emergency_declared",
|
|
84
137
|
"decision_recorded",
|
|
85
138
|
"result_reported",
|
|
86
139
|
"recovery_recorded",
|
|
87
140
|
"plan_revised",
|
|
88
141
|
"note_added"
|
|
89
142
|
]);
|
|
143
|
+
const changeTypeSchema = z.enum(["standard", "normal", "emergency"]);
|
|
144
|
+
const changeImpactGradeSchema = z.enum(["minor", "significant", "major"]);
|
|
90
145
|
const changeVetoRoleSchema = z.enum(["Owner", "Engineer"]);
|
|
91
146
|
const changeDecisionSchema = z.enum(["proceed", "defer", "cancel"]);
|
|
92
147
|
const changeResultSchema = z.enum(["completed", "failed", "rolled_back"]);
|
|
148
|
+
const gitCommitReferenceSchema = z.object({
|
|
149
|
+
commit: z.string().min(1),
|
|
150
|
+
repository: z.string().min(1).optional(),
|
|
151
|
+
url: z.string().min(1).optional()
|
|
152
|
+
});
|
|
93
153
|
const deferralEventTypeSchema = z.enum([
|
|
94
154
|
"condition_addressed",
|
|
95
155
|
"condition_reopened",
|
|
@@ -182,6 +242,11 @@ const CHANGE_TYPED_FIELDS = [
|
|
|
182
242
|
"executionSteps",
|
|
183
243
|
"rollbackPlan",
|
|
184
244
|
"riskImpact",
|
|
245
|
+
"changeType",
|
|
246
|
+
"impactGrade",
|
|
247
|
+
"emergencyImportance",
|
|
248
|
+
"emergencyImmediacy",
|
|
249
|
+
"emergencyRationaleGaps",
|
|
185
250
|
"plannedFor",
|
|
186
251
|
"events"
|
|
187
252
|
];
|
|
@@ -200,6 +265,25 @@ const COMPONENT_TYPED_FIELDS = [
|
|
|
200
265
|
"notes",
|
|
201
266
|
"versionHistory"
|
|
202
267
|
];
|
|
268
|
+
const MAINTENANCE_SCHEDULE_TYPED_FIELDS = [
|
|
269
|
+
"name",
|
|
270
|
+
"kind",
|
|
271
|
+
"cadence",
|
|
272
|
+
"startDate",
|
|
273
|
+
"rationale",
|
|
274
|
+
"notes",
|
|
275
|
+
"versionHistory"
|
|
276
|
+
];
|
|
277
|
+
const SUPPORT_REQUEST_TYPED_FIELDS = [
|
|
278
|
+
"workspaceId",
|
|
279
|
+
"reporter",
|
|
280
|
+
"kind",
|
|
281
|
+
"reportedExperience",
|
|
282
|
+
"entries",
|
|
283
|
+
"currentStatus",
|
|
284
|
+
"status"
|
|
285
|
+
];
|
|
286
|
+
const VALUE_REALIZATION_TYPED_FIELDS = ["metrics", "versionHistory"];
|
|
203
287
|
const OBSOLETE_EXPECTATION_FIELDS = ["confirmationState", "ratifiedBy", "ratifiedAt"];
|
|
204
288
|
const DECISION_INPUT_FIELDS = ["informedBy", "informedByIds", "inputRecords"];
|
|
205
289
|
const DECISION_TYPED_FIELDS = [
|
|
@@ -216,6 +300,19 @@ const DECISION_TYPED_FIELDS = [
|
|
|
216
300
|
"status"
|
|
217
301
|
];
|
|
218
302
|
const TASK_TYPED_FIELDS = ["workspaceId", "description", "assignee", "assignor", "entries", "currentStatus", "details", "status"];
|
|
303
|
+
const RISK_TYPED_FIELDS = [
|
|
304
|
+
"workspaceId",
|
|
305
|
+
"topic",
|
|
306
|
+
"entries",
|
|
307
|
+
"currentStatus",
|
|
308
|
+
"currentAssessment",
|
|
309
|
+
"currentTreatment",
|
|
310
|
+
"likelihood",
|
|
311
|
+
"impact",
|
|
312
|
+
"mitigation"
|
|
313
|
+
];
|
|
314
|
+
const INCIDENT_TYPED_FIELDS = ["workspaceId", "description", "entries", "currentStatus", "currentSeverity", "status", "severity"];
|
|
315
|
+
const PROBLEM_TYPED_FIELDS = ["workspaceId", "description", "entries", "currentStatus", "currentRootCause", "status", "rootCause"];
|
|
219
316
|
const PROCESS_GUIDE = {
|
|
220
317
|
name: "DX Complete process guide",
|
|
221
318
|
status:
|
|
@@ -236,6 +333,10 @@ const PROCESS_GUIDE = {
|
|
|
236
333
|
{
|
|
237
334
|
record: "Decision",
|
|
238
335
|
use: "A recorded choice that can appear in any phase when a meaningful decision needs to remain legible."
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
record: "Support Request",
|
|
339
|
+
use: "Shared user-facing support follow-up for a reported experience, question, request, or issue."
|
|
239
340
|
}
|
|
240
341
|
],
|
|
241
342
|
recordRoutingGuidance: {
|
|
@@ -257,13 +358,41 @@ const PROCESS_GUIDE = {
|
|
|
257
358
|
use: "Use Task."
|
|
258
359
|
},
|
|
259
360
|
{
|
|
260
|
-
when: "The information is
|
|
261
|
-
use: "Use Change
|
|
361
|
+
when: "The information is a discrete alteration to the running service.",
|
|
362
|
+
use: "Use Change."
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
when: "The information is a specific service-impacting occurrence that needs response.",
|
|
366
|
+
use: "Use Incident."
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
when: "The information is an underlying or recurring cause behind one or more incidents.",
|
|
370
|
+
use: "Use Problem."
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
when: "The information is uncertainty or exposure.",
|
|
374
|
+
use: "Use Risk."
|
|
262
375
|
},
|
|
263
376
|
{
|
|
264
377
|
when: "The information is operational infrastructure state.",
|
|
265
378
|
use: "Use Environment and Component records in the Operational Registry; do not use Journal as the long-term home."
|
|
266
379
|
},
|
|
380
|
+
{
|
|
381
|
+
when: "The information is recurring operational hygiene, such as a scheduled review, rotation, backup check, or maintenance duty.",
|
|
382
|
+
use: "Use Maintenance Schedule."
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
when: "The information is a user-facing question, request, or issue that needs shared support follow-up.",
|
|
386
|
+
use: "Use Support Request."
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
when: "The information is measured before/after value for an approved outcome or commitment.",
|
|
390
|
+
use: "Use Value Realization."
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
when: "The information is a private question, report, request, correction, or follow-up with DX Complete.",
|
|
394
|
+
use: "Use DX Complete Ticket."
|
|
395
|
+
},
|
|
267
396
|
{
|
|
268
397
|
when: "The information is relevant context with no better home.",
|
|
269
398
|
use: "Use Journal."
|
|
@@ -272,6 +401,49 @@ const PROCESS_GUIDE = {
|
|
|
272
401
|
promotion:
|
|
273
402
|
"If Journal content becomes load-bearing, promote it to the appropriate dedicated record and link back where useful."
|
|
274
403
|
},
|
|
404
|
+
roleOperatingGuidance: [
|
|
405
|
+
{
|
|
406
|
+
role: "Owner",
|
|
407
|
+
guidance:
|
|
408
|
+
"Use Statement, Expectation, Benefits, Commitment, Deferral, Decision, Value Realization, and Risk records for direction, expected value, measured value, commitment, deferral, formal risk acceptance, and authority decisions."
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
role: "Engineer",
|
|
412
|
+
guidance:
|
|
413
|
+
"Default to Requirement -> Task for implementation work. Use Decision for meaningful choices, Risk for uncertainty, Journal for relevant context with no better home, and Change only when the work becomes a run-side alteration."
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
role: "Codex assistance",
|
|
417
|
+
guidance:
|
|
418
|
+
"Codex is a coding-capable tool used by the Engineer, not a DX Complete role. A fresh Codex instance should orient from get_process_guide, get_doc({ page: \"operating_guide\" }), and the installed Codex guidance before choosing records."
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
role: "Tester",
|
|
422
|
+
guidance:
|
|
423
|
+
"Use Task entries, review notes, Risk, Decision, or Journal to keep verification evidence visible. Do not create Change records merely because testing is happening."
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
role: "Operator",
|
|
427
|
+
guidance:
|
|
428
|
+
"Use Change for discrete run-side alterations, Incident for specific service-impacting occurrences, Problem for underlying or recurring causes, Environment and Component for operational inventory, Maintenance Schedule for recurring operational hygiene, and Risk or Decision where operations need accountability. Administration duties such as users, permissions, settings, provisioning, and run-side security live here."
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
role: "Support Agent",
|
|
432
|
+
guidance:
|
|
433
|
+
"Use Support Request for shared user-facing support follow-up. Use DX Complete Ticket only for private questions, reports, requests, corrections, and follow-ups with DX Complete itself. Promote to Statement, Requirement, Task, Incident, Problem, Risk, Decision, Change, or Journal only when the signal has that meaning."
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
role: "End User",
|
|
437
|
+
guidance:
|
|
438
|
+
"End User is not a DX Complete operator and does not approve records. Their input is captured by another role as Statement, report, request, correction, feedback signal, or ticket."
|
|
439
|
+
}
|
|
440
|
+
],
|
|
441
|
+
itsmGuidance: [
|
|
442
|
+
"Change is the current first-class run-side control record.",
|
|
443
|
+
"Incident is the current first-class record for a specific service-impacting or potentially service-impacting occurrence.",
|
|
444
|
+
"Problem is the current first-class record for an underlying or recurring cause evidenced by one or more incidents.",
|
|
445
|
+
"Do not create ITSM-style records merely because work is happening; use Change, Incident, Problem, and Risk only when the record meaning fits."
|
|
446
|
+
],
|
|
275
447
|
phases: [
|
|
276
448
|
{
|
|
277
449
|
id: "orient",
|
|
@@ -445,10 +617,11 @@ const PROCESS_GUIDE = {
|
|
|
445
617
|
clientRole: "Help the Operator record the intended service change, readiness basis, execution path, rollback path, and resulting events.",
|
|
446
618
|
conductRules: [
|
|
447
619
|
"Use Change for a discrete alteration to the running service; reserve Operations Plan for a future standing operating model.",
|
|
448
|
-
"Record the original change plan, execution steps, rollback plan, and risk
|
|
449
|
-
"
|
|
620
|
+
"Record the original change plan, execution steps, rollback plan, and risk, impact, and downstream impact as the baseline.",
|
|
621
|
+
"Classify the Change as standard, normal, or emergency, then use append-only Change events for notice, veto, decision, result, recovery, notes, and plan revisions.",
|
|
622
|
+
"When result or recovery events have Git commits behind them, record optional Git commit references for that execution attempt or rollback.",
|
|
450
623
|
"Check readiness before putting the change into use.",
|
|
451
|
-
"Make release, rollback, notice, veto,
|
|
624
|
+
"Make release, rollback, notice, veto, change type, and communication risks visible.",
|
|
452
625
|
"Do not treat vetoes or readiness checks as mechanical blockers; DX Complete records accountability and does not perform or enforce the operation.",
|
|
453
626
|
"Record open readiness risk when a readiness concern remains open."
|
|
454
627
|
],
|
|
@@ -456,13 +629,15 @@ const PROCESS_GUIDE = {
|
|
|
456
629
|
"What is changing, why, and when is it planned?",
|
|
457
630
|
"What steps will carry out the change?",
|
|
458
631
|
"What rollback or recovery path exists if something goes wrong?",
|
|
632
|
+
"What else may be affected, and what depends on what is changing?",
|
|
459
633
|
"Who needs to know about the change?",
|
|
460
634
|
"Has anyone recorded a veto or concern?",
|
|
461
635
|
"What readiness concerns are still open?"
|
|
462
636
|
],
|
|
463
637
|
expectedOutput: [
|
|
464
|
-
"Change record with change plan, execution steps, rollback plan, and risk
|
|
465
|
-
"Append-only Change events for notice, veto,
|
|
638
|
+
"Change record with change plan, execution steps, rollback plan, and risk, impact, and downstream-impact notes.",
|
|
639
|
+
"Append-only Change events for notice, veto, decision, result, recovery, notes, or revisions where they occur.",
|
|
640
|
+
"Optional Git commit references on result or recovery events where they help trace the execution attempt.",
|
|
466
641
|
"Readiness basis.",
|
|
467
642
|
"Release or service-change decision context.",
|
|
468
643
|
"Visible open risks and any Owner risk-acceptance decisions."
|
|
@@ -471,7 +646,7 @@ const PROCESS_GUIDE = {
|
|
|
471
646
|
checkpointNotes: [
|
|
472
647
|
"Readiness checks reduce launch risk.",
|
|
473
648
|
"A veto by Owner or Engineer is a serious recorded event, not a mechanical stop.",
|
|
474
|
-
"Emergency changes
|
|
649
|
+
"Emergency changes should record both importance and immediacy where available; missing rationale remains visible but does not block the record.",
|
|
475
650
|
"Open readiness concerns can move forward only when the risk remains visible or is formally accepted by the Owner."
|
|
476
651
|
]
|
|
477
652
|
}
|
|
@@ -480,14 +655,16 @@ const PROCESS_GUIDE = {
|
|
|
480
655
|
id: "operate",
|
|
481
656
|
name: "Operate",
|
|
482
657
|
purpose: "Run the service, help users, and respond when something goes wrong.",
|
|
483
|
-
commonRecords: ["Change", "DX Complete Ticket", "Risk", "Decision", "Task"],
|
|
658
|
+
commonRecords: ["Incident", "Problem", "Support Request", "Change", "Environment", "Component", "Maintenance Schedule", "DX Complete Ticket", "Risk", "Decision", "Task"],
|
|
484
659
|
handoff: "Operational signals are handled or sent into measurement, improvement, or new work.",
|
|
485
660
|
operatingGuidance: {
|
|
486
661
|
clientRole: "Help keep the service legible while it is running and route operational signals to the right follow-up.",
|
|
487
662
|
conductRules: [
|
|
488
663
|
"Separate immediate user-facing help from deeper improvement work.",
|
|
489
|
-
"Record
|
|
664
|
+
"Record Incident for a specific service-impacting occurrence, Problem for an underlying or recurring cause, and Risk, Decision, or Task when those meanings fit.",
|
|
665
|
+
"Use Support Request for shared user-facing support follow-up.",
|
|
490
666
|
"Use Change when the follow-up is a discrete alteration to the running service.",
|
|
667
|
+
"Use Maintenance Schedule for recurring operational hygiene and link completed Changes or Tasks to it when the work is performed.",
|
|
491
668
|
"Do not assume the service is finished just because there is no active build.",
|
|
492
669
|
"Surface recurring signals for later improvement or measurement."
|
|
493
670
|
],
|
|
@@ -495,11 +672,12 @@ const PROCESS_GUIDE = {
|
|
|
495
672
|
"Is the service running as intended?",
|
|
496
673
|
"What user-facing issue, operational issue, or risk needs attention?",
|
|
497
674
|
"Does this signal need immediate response, a task, a decision, or later improvement?",
|
|
675
|
+
"Is this a shared support request, an incident, a problem, or recurring maintenance?",
|
|
498
676
|
"What operational follow-up remains open?"
|
|
499
677
|
],
|
|
500
678
|
expectedOutput: [
|
|
501
679
|
"Handled operational signal or routed follow-up.",
|
|
502
|
-
"Visible incidents, risks, decisions, or tasks where needed.",
|
|
680
|
+
"Visible support requests, incidents, problems, maintenance schedules, risks, decisions, or tasks where needed.",
|
|
503
681
|
"Ongoing operating state."
|
|
504
682
|
],
|
|
505
683
|
exitCheck:
|
|
@@ -514,12 +692,13 @@ const PROCESS_GUIDE = {
|
|
|
514
692
|
id: "measure",
|
|
515
693
|
name: "Measure",
|
|
516
694
|
purpose: "Compare expected and actual cost or benefit when data is available.",
|
|
517
|
-
commonRecords: ["Estimate", "Benefits", "Decision", "Risk"],
|
|
695
|
+
commonRecords: ["Value Realization", "Estimate", "Benefits", "Decision", "Risk"],
|
|
518
696
|
handoff: "Learning from actuals is available for future estimates and decisions.",
|
|
519
697
|
operatingGuidance: {
|
|
520
698
|
clientRole: "Help compare estimates to actuals where data exists and feed learning into future decisions.",
|
|
521
699
|
conductRules: [
|
|
522
700
|
"Capture actual cost or benefit observations when available.",
|
|
701
|
+
"Use Value Realization for before/after metrics tied to expectations, requirements, or commitments.",
|
|
523
702
|
"Do not block closure or continued operation because actuals are unavailable.",
|
|
524
703
|
"Compare measured results to earlier estimates where possible.",
|
|
525
704
|
"Turn meaningful differences into learning, risk, decision context, or future work."
|
|
@@ -527,11 +706,13 @@ const PROCESS_GUIDE = {
|
|
|
527
706
|
questionsToAsk: [
|
|
528
707
|
"What actual cost or benefit data is available?",
|
|
529
708
|
"How does it compare with the estimate?",
|
|
709
|
+
"Which value metric has a baseline, an actual measurement, or an open measurement gap?",
|
|
530
710
|
"What should future estimates or decisions learn from this?",
|
|
531
711
|
"Is new work, risk, or a decision needed because of the measurement?"
|
|
532
712
|
],
|
|
533
713
|
expectedOutput: [
|
|
534
714
|
"Actual cost or benefit observations where available.",
|
|
715
|
+
"Value Realization records for measured or still-open value metrics where useful.",
|
|
535
716
|
"Comparison to estimates where possible.",
|
|
536
717
|
"Learning for future estimates and decisions."
|
|
537
718
|
],
|
|
@@ -573,7 +754,7 @@ const PROCESS_GUIDE = {
|
|
|
573
754
|
"Formal risk acceptance is an Owner-level decision; domain actors may proceed, but proceeding does not close or accept the risk.",
|
|
574
755
|
"Use Risk and Decision records to capture Owner risk acceptance or proceeding past an open checkpoint until dedicated checkpoint fields or tools are proven necessary.",
|
|
575
756
|
"Use Decision input links to preserve which records informed important choices.",
|
|
576
|
-
"Use Change records for run-side service changes where notice, veto,
|
|
757
|
+
"Use Change records for run-side service changes where change type, notice, veto, execution, rollback, result, and optional Git commit references need an ordered audit trail."
|
|
577
758
|
],
|
|
578
759
|
nextStepGuidance: {
|
|
579
760
|
responsibility:
|
|
@@ -633,7 +814,15 @@ const PROCESS_GUIDE = {
|
|
|
633
814
|
},
|
|
634
815
|
{
|
|
635
816
|
record: "Change",
|
|
636
|
-
use: "A discrete service change record with baseline plan sections and append-only events for notice, veto,
|
|
817
|
+
use: "A discrete service change record with baseline change type, plan sections, and append-only events for notice, veto, decision, result, recovery, notes, and revisions."
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
record: "Incident",
|
|
821
|
+
use: "A specific service-impacting or potentially service-impacting occurrence with an append-only entry log; current status and severity come from the latest relevant entries."
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
record: "Problem",
|
|
825
|
+
use: "An underlying or recurring cause evidenced by one or more Incidents, with an append-only entry log and current root cause derived from entries."
|
|
637
826
|
},
|
|
638
827
|
{
|
|
639
828
|
record: "Environment",
|
|
@@ -643,6 +832,10 @@ const PROCESS_GUIDE = {
|
|
|
643
832
|
record: "Component",
|
|
644
833
|
use: "One environment-specific operational component with kind, structured locator, identifiers, secret pointers, notes, and version history."
|
|
645
834
|
},
|
|
835
|
+
{
|
|
836
|
+
record: "Maintenance Schedule",
|
|
837
|
+
use: "A recurring operational hygiene record with cadence, start date, rationale, version history, and due state derived from linked completed Changes or Tasks."
|
|
838
|
+
},
|
|
646
839
|
{
|
|
647
840
|
record: "Estimate",
|
|
648
841
|
use: "An Engineer cost input for Weigh, with itemized line items, scope links, cost-only roll-up totals, and version history."
|
|
@@ -651,6 +844,14 @@ const PROCESS_GUIDE = {
|
|
|
651
844
|
record: "Benefits",
|
|
652
845
|
use: "An Owner-authored benefit input for Weigh, with qualitative or quantified benefit items, scope links, quantified roll-up totals where amounts exist, and version history."
|
|
653
846
|
},
|
|
847
|
+
{
|
|
848
|
+
record: "Value Realization",
|
|
849
|
+
use: "A measured-value record for before/after metrics tied to expectations, requirements, or commitments, with comparisons derived from baseline and actual values."
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
record: "Support Request",
|
|
853
|
+
use: "A shared support ledger for a user-facing reported experience, question, request, or issue; current status comes from ordered entries."
|
|
854
|
+
},
|
|
654
855
|
{
|
|
655
856
|
record: "Decision",
|
|
656
857
|
use: "A revisitable choice record with an append-only entry log; current decision comes from the latest decision entry while earlier arguments and decisions remain visible."
|
|
@@ -658,9 +859,10 @@ const PROCESS_GUIDE = {
|
|
|
658
859
|
],
|
|
659
860
|
clientGuidance: [
|
|
660
861
|
"Use get_process_guide when you need the current DX Complete phase language.",
|
|
661
|
-
"Use get_doc for the fuller reference on outcomes, flow, records, roles, and glossary terms.",
|
|
862
|
+
"Use get_doc for the fuller reference on outcomes, flow, records, roles, operating guide, and glossary terms.",
|
|
863
|
+
"Use get_doc({ page: \"operating_guide\" }) when a fresh client or agent needs role-by-role record routing.",
|
|
662
864
|
"Use DX Complete Ticket tools for private questions, reports, requests, corrections, or follow-ups.",
|
|
663
|
-
"Use Journal tools only after checking whether the information belongs in Statement, Expectation, Requirement, Decision, Task, Change, Risk, Environment, Component, or another dedicated record.",
|
|
865
|
+
"Use Journal tools only after checking whether the information belongs in Statement, Expectation, Requirement, Decision, Task, Change, Incident, Problem, Support Request, Risk, Environment, Component, Maintenance Schedule, Value Realization, or another dedicated record.",
|
|
664
866
|
"Use workspace-scoped records for shared process work.",
|
|
665
867
|
"Use readableId values such as REQ-0001 as human-facing references when a tool accepts an existing record id; UUID remains the primary key and link target.",
|
|
666
868
|
"Hosted MCP access derives workspace scope from installed repo config and actor scope from OAuth.",
|
|
@@ -668,6 +870,8 @@ const PROCESS_GUIDE = {
|
|
|
668
870
|
"Use append_decision_entry and append_task_entry for Decision and Task history; do not rewrite their current decision or status directly.",
|
|
669
871
|
"Use link_decision_input when a record informed a Decision; use list_linked_records with relationship informed_by to review the decision inputs.",
|
|
670
872
|
"Use Benefits for Owner-authored benefit lists; use Estimate for Engineer cost estimates.",
|
|
873
|
+
"Use Value Realization for measured before/after value, not for expected benefits.",
|
|
874
|
+
"Use Support Request for shared user-facing support follow-up; use DX Complete Ticket for private communication with DX Complete.",
|
|
671
875
|
"Use link_records and unlink_records for other relationships between records."
|
|
672
876
|
]
|
|
673
877
|
};
|
|
@@ -855,7 +1059,7 @@ export function createMcpServer(runtime: DxRuntime, options: McpServerOptions =
|
|
|
855
1059
|
"get_doc",
|
|
856
1060
|
{
|
|
857
1061
|
description:
|
|
858
|
-
"Get an on-demand DX Complete reference page for MCP clients. Use this for fuller outcomes, flow, records, roles, and glossary content without loading all documentation into the process guide.",
|
|
1062
|
+
"Get an on-demand DX Complete reference page for MCP clients. Use this for fuller outcomes, flow, records, roles, operating guide, and glossary content without loading all documentation into the process guide.",
|
|
859
1063
|
inputSchema: {
|
|
860
1064
|
page: z.enum(DOC_PAGE_IDS),
|
|
861
1065
|
term: z.string().min(1).optional()
|
|
@@ -1220,6 +1424,48 @@ export function createMcpServer(runtime: DxRuntime, options: McpServerOptions =
|
|
|
1220
1424
|
async (input) => jsonResult(await updateComponent(runtime, input, recordActorId))
|
|
1221
1425
|
);
|
|
1222
1426
|
|
|
1427
|
+
registerDxcTool(
|
|
1428
|
+
"create_maintenance_schedule",
|
|
1429
|
+
{
|
|
1430
|
+
description:
|
|
1431
|
+
"Create a Maintenance Schedule for recurring operational hygiene. Due state is derived from cadence and linked completed Changes or Tasks.",
|
|
1432
|
+
inputSchema: {
|
|
1433
|
+
workspaceId: workspaceIdSchema,
|
|
1434
|
+
name: z.string().min(1),
|
|
1435
|
+
kind: z.string().min(1),
|
|
1436
|
+
cadence: maintenanceCadenceSchema,
|
|
1437
|
+
startDate: z.string().min(1),
|
|
1438
|
+
summary: z.string().min(1).optional(),
|
|
1439
|
+
rationale: z.string().min(1).optional(),
|
|
1440
|
+
notes: z.string().min(1).optional(),
|
|
1441
|
+
fields: recordFieldsSchema
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
async (input) => jsonResult(await createMaintenanceSchedule(runtime, input, recordActorId))
|
|
1445
|
+
);
|
|
1446
|
+
|
|
1447
|
+
registerDxcTool(
|
|
1448
|
+
"update_maintenance_schedule",
|
|
1449
|
+
{
|
|
1450
|
+
description: "Update a Maintenance Schedule while preserving prior versions.",
|
|
1451
|
+
inputSchema: {
|
|
1452
|
+
workspaceId: workspaceIdSchema,
|
|
1453
|
+
id: z.string().min(1),
|
|
1454
|
+
name: z.string().min(1).optional(),
|
|
1455
|
+
kind: z.string().min(1).optional(),
|
|
1456
|
+
cadence: maintenanceCadenceSchema.optional(),
|
|
1457
|
+
startDate: z.string().min(1).optional(),
|
|
1458
|
+
summary: z.string().min(1).optional(),
|
|
1459
|
+
rationale: z.string().min(1).optional(),
|
|
1460
|
+
notes: z.string().min(1).optional(),
|
|
1461
|
+
fields: recordFieldsSchema,
|
|
1462
|
+
unsetFields: z.array(fieldNameSchema).optional(),
|
|
1463
|
+
revisionNote: z.string().min(1).optional()
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
async (input) => jsonResult(await updateMaintenanceSchedule(runtime, input, recordActorId))
|
|
1467
|
+
);
|
|
1468
|
+
|
|
1223
1469
|
registerDxcTool(
|
|
1224
1470
|
"create_estimate",
|
|
1225
1471
|
{
|
|
@@ -1290,6 +1536,43 @@ export function createMcpServer(runtime: DxRuntime, options: McpServerOptions =
|
|
|
1290
1536
|
async (input) => jsonResult(await updateBenefits(runtime, input, recordActorId))
|
|
1291
1537
|
);
|
|
1292
1538
|
|
|
1539
|
+
registerDxcTool(
|
|
1540
|
+
"create_value_realization",
|
|
1541
|
+
{
|
|
1542
|
+
description:
|
|
1543
|
+
"Create a Value Realization record for measured before/after value. Comparisons are derived from baseline and actual metric values.",
|
|
1544
|
+
inputSchema: {
|
|
1545
|
+
workspaceId: workspaceIdSchema,
|
|
1546
|
+
title: z.string().min(1),
|
|
1547
|
+
summary: z.string().min(1).optional(),
|
|
1548
|
+
metrics: z.array(valueMetricSchema).min(1),
|
|
1549
|
+
requirementIds: z.array(z.string().min(1)).optional(),
|
|
1550
|
+
expectationIds: z.array(z.string().min(1)).optional(),
|
|
1551
|
+
commitmentIds: z.array(z.string().min(1)).optional(),
|
|
1552
|
+
fields: recordFieldsSchema
|
|
1553
|
+
}
|
|
1554
|
+
},
|
|
1555
|
+
async (input) => jsonResult(await createValueRealization(runtime, input, recordActorId))
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
registerDxcTool(
|
|
1559
|
+
"update_value_realization",
|
|
1560
|
+
{
|
|
1561
|
+
description: "Update a Value Realization record while preserving prior metric versions.",
|
|
1562
|
+
inputSchema: {
|
|
1563
|
+
workspaceId: workspaceIdSchema,
|
|
1564
|
+
id: z.string().min(1),
|
|
1565
|
+
title: z.string().min(1).optional(),
|
|
1566
|
+
summary: z.string().min(1).optional(),
|
|
1567
|
+
metrics: z.array(valueMetricSchema).min(1).optional(),
|
|
1568
|
+
fields: recordFieldsSchema,
|
|
1569
|
+
unsetFields: z.array(fieldNameSchema).optional(),
|
|
1570
|
+
revisionNote: z.string().min(1).optional()
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1573
|
+
async (input) => jsonResult(await updateValueRealization(runtime, input, recordActorId))
|
|
1574
|
+
);
|
|
1575
|
+
|
|
1293
1576
|
registerDxcTool(
|
|
1294
1577
|
"create_expectation",
|
|
1295
1578
|
{
|
|
@@ -1459,15 +1742,24 @@ export function createMcpServer(runtime: DxRuntime, options: McpServerOptions =
|
|
|
1459
1742
|
"create_change",
|
|
1460
1743
|
{
|
|
1461
1744
|
description:
|
|
1462
|
-
"Create a service change record with baseline plan, execution, rollback, and risk/impact sections.",
|
|
1745
|
+
"Create a service change record with baseline type, plan, execution, rollback, and risk/impact sections. Use riskImpact to include downstream impact or blast radius when known.",
|
|
1463
1746
|
inputSchema: {
|
|
1464
1747
|
workspaceId: workspaceIdSchema,
|
|
1465
1748
|
title: z.string().min(1),
|
|
1466
1749
|
summary: z.string().min(1).optional(),
|
|
1750
|
+
changeType: changeTypeSchema.optional(),
|
|
1751
|
+
impactGrade: changeImpactGradeSchema.optional(),
|
|
1752
|
+
emergencyImportance: z.string().min(1).optional(),
|
|
1753
|
+
emergencyImmediacy: z.string().min(1).optional(),
|
|
1467
1754
|
changePlan: z.string().min(1),
|
|
1468
1755
|
executionSteps: z.array(z.string().min(1)).min(1),
|
|
1469
1756
|
rollbackPlan: z.string().min(1),
|
|
1470
|
-
riskImpact: z
|
|
1757
|
+
riskImpact: z
|
|
1758
|
+
.string()
|
|
1759
|
+
.min(1)
|
|
1760
|
+
.describe(
|
|
1761
|
+
"Risk, impact, open concerns, and downstream impact for this Change. Include what else may be affected, what depends on what is changing, and the blast-radius conclusion identified by the Engineer or their tool when available. This is documentary and absence of analysis is visible; it is not a separate blocker."
|
|
1762
|
+
),
|
|
1471
1763
|
plannedFor: z.string().min(1).optional(),
|
|
1472
1764
|
requirementId: z.string().min(1).optional(),
|
|
1473
1765
|
fields: recordFieldsSchema
|
|
@@ -1480,7 +1772,7 @@ export function createMcpServer(runtime: DxRuntime, options: McpServerOptions =
|
|
|
1480
1772
|
"append_change_event",
|
|
1481
1773
|
{
|
|
1482
1774
|
description:
|
|
1483
|
-
"Append an immutable event to a Change record. Use this for notice, veto,
|
|
1775
|
+
"Append an immutable event to a Change record. Use this for notice, veto, decision, result, recovery, plan revisions, and notes. Result and recovery events may include optional Git commit references as documentary traceability.",
|
|
1484
1776
|
inputSchema: {
|
|
1485
1777
|
workspaceId: workspaceIdSchema,
|
|
1486
1778
|
changeId: z.string().min(1),
|
|
@@ -1490,12 +1782,21 @@ export function createMcpServer(runtime: DxRuntime, options: McpServerOptions =
|
|
|
1490
1782
|
effectiveAt: z.string().min(1).optional(),
|
|
1491
1783
|
vetoByRole: changeVetoRoleSchema.optional(),
|
|
1492
1784
|
reason: z.string().min(1).optional(),
|
|
1493
|
-
importance: z.string().min(1).optional(),
|
|
1494
|
-
immediacy: z.string().min(1).optional(),
|
|
1495
1785
|
decision: changeDecisionSchema.optional(),
|
|
1496
1786
|
result: changeResultSchema.optional(),
|
|
1787
|
+
gitCommitReferences: z
|
|
1788
|
+
.array(gitCommitReferenceSchema)
|
|
1789
|
+
.min(1)
|
|
1790
|
+
.optional()
|
|
1791
|
+
.describe(
|
|
1792
|
+
"Optional Git commit references for result or recovery events. Each reference stores a commit identifier plus optional repository and URL. DX Complete records these as provided and does not validate or inspect Git."
|
|
1793
|
+
),
|
|
1497
1794
|
summary: z.string().min(1).optional(),
|
|
1498
1795
|
note: z.string().min(1).optional(),
|
|
1796
|
+
revisedChangeType: changeTypeSchema.optional(),
|
|
1797
|
+
revisedImpactGrade: changeImpactGradeSchema.optional(),
|
|
1798
|
+
revisedEmergencyImportance: z.string().min(1).optional(),
|
|
1799
|
+
revisedEmergencyImmediacy: z.string().min(1).optional(),
|
|
1499
1800
|
revisedChangePlan: z.string().min(1).optional(),
|
|
1500
1801
|
revisedExecutionSteps: z.array(z.string().min(1)).optional(),
|
|
1501
1802
|
revisedRollbackPlan: z.string().min(1).optional(),
|
|
@@ -1560,36 +1861,145 @@ export function createMcpServer(runtime: DxRuntime, options: McpServerOptions =
|
|
|
1560
1861
|
registerDxcTool(
|
|
1561
1862
|
"create_risk",
|
|
1562
1863
|
{
|
|
1563
|
-
description: "Create a
|
|
1864
|
+
description: "Create a Risk ledger for uncertainty or exposure. Current status, assessment, and treatment derive from entries.",
|
|
1564
1865
|
inputSchema: {
|
|
1565
1866
|
workspaceId: workspaceIdSchema,
|
|
1566
1867
|
title: z.string().min(1),
|
|
1868
|
+
topic: z.string().min(1),
|
|
1567
1869
|
summary: z.string().min(1).optional(),
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1870
|
+
body: z.string().min(1).optional(),
|
|
1871
|
+
likelihood: riskLevelSchema.optional(),
|
|
1872
|
+
impact: riskLevelSchema.optional(),
|
|
1873
|
+
treatment: riskTreatmentSchema.optional(),
|
|
1874
|
+
treatmentRationale: z.string().min(1).optional(),
|
|
1571
1875
|
fields: recordFieldsSchema
|
|
1572
1876
|
}
|
|
1573
1877
|
},
|
|
1574
|
-
async (
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1878
|
+
async (input) => jsonResult(await createRisk(runtime, input, recordActorId, options.workspaceRoles))
|
|
1879
|
+
);
|
|
1880
|
+
|
|
1881
|
+
registerDxcTool(
|
|
1882
|
+
"append_risk_entry",
|
|
1883
|
+
{
|
|
1884
|
+
description:
|
|
1885
|
+
"Append an immutable entry to a Risk. Use assessment entries for likelihood/impact and treatment entries for accept, mitigate, transfer, or avoid.",
|
|
1886
|
+
inputSchema: {
|
|
1887
|
+
workspaceId: workspaceIdSchema,
|
|
1888
|
+
riskId: z.string().min(1),
|
|
1889
|
+
entryType: riskEntryTypeSchema,
|
|
1890
|
+
body: z.string().min(1),
|
|
1891
|
+
likelihood: riskLevelSchema.optional(),
|
|
1892
|
+
impact: riskLevelSchema.optional(),
|
|
1893
|
+
treatment: riskTreatmentSchema.optional(),
|
|
1894
|
+
treatmentRationale: z.string().min(1).optional()
|
|
1895
|
+
}
|
|
1896
|
+
},
|
|
1897
|
+
async (input) => {
|
|
1898
|
+
assertRiskAcceptanceAccess(input.treatment, options.workspaceRoles);
|
|
1899
|
+
return jsonResult(await appendRiskEntry(runtime.db, input, recordActorId));
|
|
1900
|
+
}
|
|
1901
|
+
);
|
|
1902
|
+
|
|
1903
|
+
registerDxcTool(
|
|
1904
|
+
"create_incident",
|
|
1905
|
+
{
|
|
1906
|
+
description:
|
|
1907
|
+
"Create an Incident ledger for a specific service-impacting or potentially service-impacting occurrence.",
|
|
1908
|
+
inputSchema: {
|
|
1909
|
+
workspaceId: workspaceIdSchema,
|
|
1910
|
+
title: z.string().min(1),
|
|
1911
|
+
description: z.string().min(1),
|
|
1912
|
+
summary: z.string().min(1).optional(),
|
|
1913
|
+
severity: incidentSeveritySchema.optional(),
|
|
1914
|
+
componentIds: z.array(z.string().min(1)).optional(),
|
|
1915
|
+
fields: recordFieldsSchema
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
async (input) => jsonResult(await createIncident(runtime, input, recordActorId))
|
|
1919
|
+
);
|
|
1920
|
+
|
|
1921
|
+
registerDxcTool(
|
|
1922
|
+
"append_incident_entry",
|
|
1923
|
+
{
|
|
1924
|
+
description:
|
|
1925
|
+
"Append an immutable entry to an Incident. Current status and severity derive from the latest relevant entries.",
|
|
1926
|
+
inputSchema: {
|
|
1927
|
+
workspaceId: workspaceIdSchema,
|
|
1928
|
+
incidentId: z.string().min(1),
|
|
1929
|
+
entryType: incidentEntryTypeSchema,
|
|
1930
|
+
body: z.string().min(1),
|
|
1931
|
+
severity: incidentSeveritySchema.optional()
|
|
1932
|
+
}
|
|
1933
|
+
},
|
|
1934
|
+
async (input) => jsonResult(await appendIncidentEntry(runtime.db, input, recordActorId))
|
|
1935
|
+
);
|
|
1936
|
+
|
|
1937
|
+
registerDxcTool(
|
|
1938
|
+
"create_support_request",
|
|
1939
|
+
{
|
|
1940
|
+
description:
|
|
1941
|
+
"Create a shared Support Request ledger for a reported user experience, question, request, or issue. This is distinct from a private DX Complete Ticket.",
|
|
1942
|
+
inputSchema: {
|
|
1943
|
+
workspaceId: workspaceIdSchema,
|
|
1944
|
+
title: z.string().min(1),
|
|
1945
|
+
reporter: z.string().min(1),
|
|
1946
|
+
kind: z.string().min(1),
|
|
1947
|
+
reportedExperience: z.string().min(1),
|
|
1948
|
+
summary: z.string().min(1).optional(),
|
|
1949
|
+
fields: recordFieldsSchema
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
1952
|
+
async (input) => jsonResult(await createSupportRequest(runtime, input, recordActorId))
|
|
1953
|
+
);
|
|
1954
|
+
|
|
1955
|
+
registerDxcTool(
|
|
1956
|
+
"append_support_request_entry",
|
|
1957
|
+
{
|
|
1958
|
+
description:
|
|
1959
|
+
"Append an immutable entry to a Support Request. Escalated entries require an Incident link; current status derives from the latest status-bearing entry.",
|
|
1960
|
+
inputSchema: {
|
|
1961
|
+
workspaceId: workspaceIdSchema,
|
|
1962
|
+
supportRequestId: z.string().min(1),
|
|
1963
|
+
entryType: supportRequestEntryTypeSchema,
|
|
1964
|
+
body: z.string().min(1),
|
|
1965
|
+
incidentId: z.string().min(1).optional()
|
|
1966
|
+
}
|
|
1967
|
+
},
|
|
1968
|
+
async (input) => jsonResult(await appendSupportRequestEntryForTool(runtime, input, recordActorId))
|
|
1969
|
+
);
|
|
1970
|
+
|
|
1971
|
+
registerDxcTool(
|
|
1972
|
+
"create_problem",
|
|
1973
|
+
{
|
|
1974
|
+
description:
|
|
1975
|
+
"Create a Problem ledger for an underlying or recurring cause evidenced by one or more Incidents.",
|
|
1976
|
+
inputSchema: {
|
|
1977
|
+
workspaceId: workspaceIdSchema,
|
|
1978
|
+
title: z.string().min(1),
|
|
1979
|
+
description: z.string().min(1),
|
|
1980
|
+
summary: z.string().min(1).optional(),
|
|
1981
|
+
incidentIds: z.array(z.string().min(1)).min(1),
|
|
1982
|
+
componentIds: z.array(z.string().min(1)).optional(),
|
|
1983
|
+
fields: recordFieldsSchema
|
|
1984
|
+
}
|
|
1985
|
+
},
|
|
1986
|
+
async (input) => jsonResult(await createProblem(runtime, input, recordActorId))
|
|
1987
|
+
);
|
|
1988
|
+
|
|
1989
|
+
registerDxcTool(
|
|
1990
|
+
"append_problem_entry",
|
|
1991
|
+
{
|
|
1992
|
+
description:
|
|
1993
|
+
"Append an immutable entry to a Problem. Current status and root cause derive from the latest relevant entries.",
|
|
1994
|
+
inputSchema: {
|
|
1995
|
+
workspaceId: workspaceIdSchema,
|
|
1996
|
+
problemId: z.string().min(1),
|
|
1997
|
+
entryType: problemEntryTypeSchema,
|
|
1998
|
+
body: z.string().min(1),
|
|
1999
|
+
rootCause: z.string().min(1).optional()
|
|
2000
|
+
}
|
|
2001
|
+
},
|
|
2002
|
+
async (input) => jsonResult(await appendProblemEntry(runtime.db, input, recordActorId))
|
|
1593
2003
|
);
|
|
1594
2004
|
|
|
1595
2005
|
registerDxcTool(
|
|
@@ -1825,14 +2235,22 @@ function assertToolRoleAccess(toolName: string, roles: WorkspaceRole[] | undefin
|
|
|
1825
2235
|
return;
|
|
1826
2236
|
}
|
|
1827
2237
|
|
|
2238
|
+
if (roles.includes("tester") && testerAllowedTools.has(toolName)) {
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
1828
2242
|
if (roles.includes("operator") && operatorAllowedTools.has(toolName)) {
|
|
1829
2243
|
return;
|
|
1830
2244
|
}
|
|
1831
2245
|
|
|
2246
|
+
if (roles.includes("support_agent") && supportAgentAllowedTools.has(toolName)) {
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
1832
2250
|
if (
|
|
1833
2251
|
roles.includes("operator") &&
|
|
1834
2252
|
toolName === "archive_record" &&
|
|
1835
|
-
(input.recordType === "environments" || input.recordType === "components")
|
|
2253
|
+
(input.recordType === "environments" || input.recordType === "components" || input.recordType === "maintenance_schedules")
|
|
1836
2254
|
) {
|
|
1837
2255
|
return;
|
|
1838
2256
|
}
|
|
@@ -1866,16 +2284,39 @@ const engineerAllowedTools = new Set([
|
|
|
1866
2284
|
"update_estimate",
|
|
1867
2285
|
"create_task",
|
|
1868
2286
|
"append_task_entry",
|
|
2287
|
+
"create_risk",
|
|
2288
|
+
"append_risk_entry",
|
|
2289
|
+
"append_review_note"
|
|
2290
|
+
]);
|
|
2291
|
+
|
|
2292
|
+
const testerAllowedTools = new Set([
|
|
2293
|
+
"create_risk",
|
|
2294
|
+
"append_risk_entry",
|
|
1869
2295
|
"append_review_note"
|
|
1870
2296
|
]);
|
|
1871
2297
|
|
|
1872
2298
|
const operatorAllowedTools = new Set([
|
|
1873
2299
|
"create_change",
|
|
1874
2300
|
"append_change_event",
|
|
2301
|
+
"create_risk",
|
|
2302
|
+
"append_risk_entry",
|
|
2303
|
+
"create_incident",
|
|
2304
|
+
"append_incident_entry",
|
|
2305
|
+
"create_problem",
|
|
2306
|
+
"append_problem_entry",
|
|
1875
2307
|
"create_environment",
|
|
1876
2308
|
"update_environment",
|
|
1877
2309
|
"create_component",
|
|
1878
|
-
"update_component"
|
|
2310
|
+
"update_component",
|
|
2311
|
+
"create_maintenance_schedule",
|
|
2312
|
+
"update_maintenance_schedule"
|
|
2313
|
+
]);
|
|
2314
|
+
|
|
2315
|
+
const supportAgentAllowedTools = new Set([
|
|
2316
|
+
"create_incident",
|
|
2317
|
+
"append_incident_entry",
|
|
2318
|
+
"create_support_request",
|
|
2319
|
+
"append_support_request_entry"
|
|
1879
2320
|
]);
|
|
1880
2321
|
|
|
1881
2322
|
function jsonResult(value: unknown) {
|
|
@@ -2197,41 +2638,126 @@ async function updateComponent(
|
|
|
2197
2638
|
);
|
|
2198
2639
|
}
|
|
2199
2640
|
|
|
2200
|
-
async function
|
|
2641
|
+
async function createMaintenanceSchedule(
|
|
2201
2642
|
runtime: DxRuntime,
|
|
2202
2643
|
input: {
|
|
2203
2644
|
workspaceId: string;
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
statementId?: string;
|
|
2645
|
+
name: string;
|
|
2646
|
+
kind: string;
|
|
2647
|
+
cadence: MaintenanceCadenceInput;
|
|
2648
|
+
startDate: string;
|
|
2649
|
+
summary?: string;
|
|
2650
|
+
rationale?: string;
|
|
2651
|
+
notes?: string;
|
|
2212
2652
|
fields?: Record<string, unknown>;
|
|
2213
2653
|
},
|
|
2214
2654
|
actorId: string
|
|
2215
2655
|
) {
|
|
2216
|
-
assertNoTypedFields(input.fields, "
|
|
2217
|
-
assertNoObsoleteExpectationFields(input.fields, "create_expectation");
|
|
2656
|
+
assertNoTypedFields(input.fields, "create_maintenance_schedule", MAINTENANCE_SCHEDULE_TYPED_FIELDS);
|
|
2218
2657
|
|
|
2219
|
-
|
|
2220
|
-
const statement = await getRecord(runtime.db, "statements", input.statementId, {
|
|
2221
|
-
workspaceId: input.workspaceId
|
|
2222
|
-
});
|
|
2223
|
-
if (!statement) {
|
|
2224
|
-
throw new Error(`Statement not found: statements/${input.statementId}`);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
const expectation = await createRecord(
|
|
2658
|
+
return createRecord(
|
|
2229
2659
|
runtime.db,
|
|
2230
|
-
"
|
|
2660
|
+
"maintenance_schedules",
|
|
2231
2661
|
{
|
|
2232
2662
|
workspaceId: input.workspaceId,
|
|
2233
|
-
title: input.
|
|
2234
|
-
summary: input.
|
|
2663
|
+
title: input.name,
|
|
2664
|
+
summary: input.summary ?? input.rationale,
|
|
2665
|
+
allowManagedFields: true,
|
|
2666
|
+
fields: {
|
|
2667
|
+
...(input.fields ?? {}),
|
|
2668
|
+
name: input.name,
|
|
2669
|
+
kind: input.kind,
|
|
2670
|
+
cadence: input.cadence,
|
|
2671
|
+
startDate: input.startDate,
|
|
2672
|
+
...(input.rationale !== undefined ? { rationale: input.rationale } : {}),
|
|
2673
|
+
...(input.notes !== undefined ? { notes: input.notes } : {})
|
|
2674
|
+
}
|
|
2675
|
+
},
|
|
2676
|
+
actorId
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
async function updateMaintenanceSchedule(
|
|
2681
|
+
runtime: DxRuntime,
|
|
2682
|
+
input: {
|
|
2683
|
+
workspaceId: string;
|
|
2684
|
+
id: string;
|
|
2685
|
+
name?: string;
|
|
2686
|
+
kind?: string;
|
|
2687
|
+
cadence?: MaintenanceCadenceInput;
|
|
2688
|
+
startDate?: string;
|
|
2689
|
+
summary?: string;
|
|
2690
|
+
rationale?: string;
|
|
2691
|
+
notes?: string;
|
|
2692
|
+
fields?: Record<string, unknown>;
|
|
2693
|
+
unsetFields?: string[];
|
|
2694
|
+
revisionNote?: string;
|
|
2695
|
+
},
|
|
2696
|
+
actorId: string
|
|
2697
|
+
) {
|
|
2698
|
+
assertNoTypedFields(input.fields, "update_maintenance_schedule", MAINTENANCE_SCHEDULE_TYPED_FIELDS);
|
|
2699
|
+
assertNoTypedUnsetFields(input.unsetFields, "update_maintenance_schedule", MAINTENANCE_SCHEDULE_TYPED_FIELDS);
|
|
2700
|
+
|
|
2701
|
+
return updateRecord(
|
|
2702
|
+
runtime.db,
|
|
2703
|
+
{
|
|
2704
|
+
recordType: "maintenance_schedules",
|
|
2705
|
+
workspaceId: input.workspaceId,
|
|
2706
|
+
id: input.id,
|
|
2707
|
+
title: input.name,
|
|
2708
|
+
summary: input.summary,
|
|
2709
|
+
fields: {
|
|
2710
|
+
...(input.fields ?? {}),
|
|
2711
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
2712
|
+
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
2713
|
+
...(input.cadence !== undefined ? { cadence: input.cadence } : {}),
|
|
2714
|
+
...(input.startDate !== undefined ? { startDate: input.startDate } : {}),
|
|
2715
|
+
...(input.rationale !== undefined ? { rationale: input.rationale } : {}),
|
|
2716
|
+
...(input.notes !== undefined ? { notes: input.notes } : {})
|
|
2717
|
+
},
|
|
2718
|
+
unsetFields: input.unsetFields,
|
|
2719
|
+
allowManagedFields: true,
|
|
2720
|
+
revisionNote: input.revisionNote
|
|
2721
|
+
},
|
|
2722
|
+
actorId
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
async function createExpectation(
|
|
2727
|
+
runtime: DxRuntime,
|
|
2728
|
+
input: {
|
|
2729
|
+
workspaceId: string;
|
|
2730
|
+
title: string;
|
|
2731
|
+
statement: string;
|
|
2732
|
+
successRecognition?: string;
|
|
2733
|
+
approvalState?: "draft" | "approved" | "not_approved" | "superseded";
|
|
2734
|
+
approvedBy?: string;
|
|
2735
|
+
approvedAt?: string;
|
|
2736
|
+
source?: string;
|
|
2737
|
+
statementId?: string;
|
|
2738
|
+
fields?: Record<string, unknown>;
|
|
2739
|
+
},
|
|
2740
|
+
actorId: string
|
|
2741
|
+
) {
|
|
2742
|
+
assertNoTypedFields(input.fields, "create_expectation", EXPECTATION_TYPED_FIELDS);
|
|
2743
|
+
assertNoObsoleteExpectationFields(input.fields, "create_expectation");
|
|
2744
|
+
|
|
2745
|
+
if (input.statementId) {
|
|
2746
|
+
const statement = await getRecord(runtime.db, "statements", input.statementId, {
|
|
2747
|
+
workspaceId: input.workspaceId
|
|
2748
|
+
});
|
|
2749
|
+
if (!statement) {
|
|
2750
|
+
throw new Error(`Statement not found: statements/${input.statementId}`);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
const expectation = await createRecord(
|
|
2755
|
+
runtime.db,
|
|
2756
|
+
"expectations",
|
|
2757
|
+
{
|
|
2758
|
+
workspaceId: input.workspaceId,
|
|
2759
|
+
title: input.title,
|
|
2760
|
+
summary: input.statement,
|
|
2235
2761
|
fields: {
|
|
2236
2762
|
...(input.fields ?? {}),
|
|
2237
2763
|
statement: input.statement,
|
|
@@ -2498,6 +3024,113 @@ async function updateBenefits(
|
|
|
2498
3024
|
);
|
|
2499
3025
|
}
|
|
2500
3026
|
|
|
3027
|
+
async function createValueRealization(
|
|
3028
|
+
runtime: DxRuntime,
|
|
3029
|
+
input: {
|
|
3030
|
+
workspaceId: string;
|
|
3031
|
+
title: string;
|
|
3032
|
+
summary?: string;
|
|
3033
|
+
metrics: ValueMetricInput[];
|
|
3034
|
+
requirementIds?: string[];
|
|
3035
|
+
expectationIds?: string[];
|
|
3036
|
+
commitmentIds?: string[];
|
|
3037
|
+
fields?: Record<string, unknown>;
|
|
3038
|
+
},
|
|
3039
|
+
actorId: string
|
|
3040
|
+
) {
|
|
3041
|
+
assertNoReservedFields(input.fields, ["workspaceId", "requirementIds", "expectationIds", "commitmentIds"]);
|
|
3042
|
+
assertNoTypedFields(input.fields, "create_value_realization", VALUE_REALIZATION_TYPED_FIELDS);
|
|
3043
|
+
assertAtLeastOneValueRealizationTarget(input.requirementIds, input.expectationIds, input.commitmentIds);
|
|
3044
|
+
await assertRuntimeRecordsExist(runtime, input.workspaceId, "requirements", uniqueIds(input.requirementIds ?? []));
|
|
3045
|
+
await assertRuntimeRecordsExist(runtime, input.workspaceId, "expectations", uniqueIds(input.expectationIds ?? []));
|
|
3046
|
+
await assertRuntimeRecordsExist(runtime, input.workspaceId, "commitments", uniqueIds(input.commitmentIds ?? []));
|
|
3047
|
+
|
|
3048
|
+
let valueRealization = await createRecord(
|
|
3049
|
+
runtime.db,
|
|
3050
|
+
"value_realizations",
|
|
3051
|
+
{
|
|
3052
|
+
workspaceId: input.workspaceId,
|
|
3053
|
+
title: input.title,
|
|
3054
|
+
summary: input.summary,
|
|
3055
|
+
allowManagedFields: true,
|
|
3056
|
+
fields: {
|
|
3057
|
+
...(input.fields ?? {}),
|
|
3058
|
+
metrics: normalizeValueMetrics(input.metrics)
|
|
3059
|
+
}
|
|
3060
|
+
},
|
|
3061
|
+
actorId
|
|
3062
|
+
);
|
|
3063
|
+
|
|
3064
|
+
valueRealization = await linkManyRecords(runtime, valueRealization, "requirements", input.requirementIds ?? [], "realizes_value_for", actorId);
|
|
3065
|
+
valueRealization = await linkManyRecords(runtime, valueRealization, "expectations", input.expectationIds ?? [], "realizes_value_for", actorId);
|
|
3066
|
+
valueRealization = await linkManyRecords(runtime, valueRealization, "commitments", input.commitmentIds ?? [], "realizes_value_for", actorId);
|
|
3067
|
+
|
|
3068
|
+
return valueRealization;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
async function updateValueRealization(
|
|
3072
|
+
runtime: DxRuntime,
|
|
3073
|
+
input: {
|
|
3074
|
+
workspaceId: string;
|
|
3075
|
+
id: string;
|
|
3076
|
+
title?: string;
|
|
3077
|
+
summary?: string;
|
|
3078
|
+
metrics?: ValueMetricInput[];
|
|
3079
|
+
fields?: Record<string, unknown>;
|
|
3080
|
+
unsetFields?: string[];
|
|
3081
|
+
revisionNote?: string;
|
|
3082
|
+
},
|
|
3083
|
+
actorId: string
|
|
3084
|
+
) {
|
|
3085
|
+
assertNoTypedFields(input.fields, "update_value_realization", VALUE_REALIZATION_TYPED_FIELDS);
|
|
3086
|
+
assertNoTypedUnsetFields(input.unsetFields, "update_value_realization", VALUE_REALIZATION_TYPED_FIELDS);
|
|
3087
|
+
|
|
3088
|
+
return updateRecord(
|
|
3089
|
+
runtime.db,
|
|
3090
|
+
{
|
|
3091
|
+
recordType: "value_realizations",
|
|
3092
|
+
workspaceId: input.workspaceId,
|
|
3093
|
+
id: input.id,
|
|
3094
|
+
title: input.title,
|
|
3095
|
+
summary: input.summary,
|
|
3096
|
+
fields: {
|
|
3097
|
+
...(input.fields ?? {}),
|
|
3098
|
+
...(input.metrics ? { metrics: normalizeValueMetrics(input.metrics) } : {})
|
|
3099
|
+
},
|
|
3100
|
+
unsetFields: input.unsetFields,
|
|
3101
|
+
allowManagedFields: true,
|
|
3102
|
+
revisionNote: input.revisionNote
|
|
3103
|
+
},
|
|
3104
|
+
actorId
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
function normalizeValueMetrics(metrics: ValueMetricInput[]): ValueMetricInput[] {
|
|
3109
|
+
const seenIds = new Set<string>();
|
|
3110
|
+
return metrics.map((metric, index) => {
|
|
3111
|
+
const id = metric.id ?? randomUUID();
|
|
3112
|
+
if (seenIds.has(id)) {
|
|
3113
|
+
throw new Error(`Value metric id must be unique: ${id}.`);
|
|
3114
|
+
}
|
|
3115
|
+
seenIds.add(id);
|
|
3116
|
+
assertMeasuredAtDate(metric.baseline.measuredAt, `Value metric ${index + 1} baseline`);
|
|
3117
|
+
if (metric.actual) {
|
|
3118
|
+
assertMeasuredAtDate(metric.actual.measuredAt, `Value metric ${index + 1} actual`);
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
return {
|
|
3122
|
+
...metric,
|
|
3123
|
+
id
|
|
3124
|
+
};
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
function assertMeasuredAtDate(value: string, label: string): void {
|
|
3129
|
+
if (Number.isNaN(new Date(value).getTime())) {
|
|
3130
|
+
throw new Error(`${label} measuredAt must be a valid date string.`);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
|
|
2501
3134
|
function normalizeBenefitItems(benefitItems: BenefitItemInput[]): BenefitItem[] {
|
|
2502
3135
|
const seenIds = new Set<string>();
|
|
2503
3136
|
return benefitItems.map((item, index) => {
|
|
@@ -3088,6 +3721,18 @@ function assertAtLeastOneBenefitsTarget(requirementIds: string[] | undefined, ex
|
|
|
3088
3721
|
throw new Error("create_benefits requires at least one requirementId or expectationId.");
|
|
3089
3722
|
}
|
|
3090
3723
|
|
|
3724
|
+
function assertAtLeastOneValueRealizationTarget(
|
|
3725
|
+
requirementIds: string[] | undefined,
|
|
3726
|
+
expectationIds: string[] | undefined,
|
|
3727
|
+
commitmentIds: string[] | undefined
|
|
3728
|
+
): void {
|
|
3729
|
+
if ((requirementIds?.length ?? 0) > 0 || (expectationIds?.length ?? 0) > 0 || (commitmentIds?.length ?? 0) > 0) {
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
throw new Error("create_value_realization requires at least one requirementId, expectationId, or commitmentId.");
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3091
3736
|
async function assertRecordsExist(
|
|
3092
3737
|
runtime: DxRuntime,
|
|
3093
3738
|
recordType: "requirements" | "expectations",
|
|
@@ -3105,7 +3750,7 @@ async function assertRecordsExist(
|
|
|
3105
3750
|
async function linkManyRecords(
|
|
3106
3751
|
runtime: DxRuntime,
|
|
3107
3752
|
source: Awaited<ReturnType<typeof getRecord>>,
|
|
3108
|
-
toType: "requirements" | "expectations",
|
|
3753
|
+
toType: "requirements" | "expectations" | "commitments",
|
|
3109
3754
|
toIds: string[],
|
|
3110
3755
|
relationship: string,
|
|
3111
3756
|
actorId: string
|
|
@@ -3172,12 +3817,364 @@ async function ensureLinkRecords(
|
|
|
3172
3817
|
return linkRecords(runtime.db, input, actorId);
|
|
3173
3818
|
}
|
|
3174
3819
|
|
|
3820
|
+
async function createRisk(
|
|
3821
|
+
runtime: DxRuntime,
|
|
3822
|
+
input: {
|
|
3823
|
+
workspaceId: string;
|
|
3824
|
+
title: string;
|
|
3825
|
+
topic: string;
|
|
3826
|
+
summary?: string;
|
|
3827
|
+
body?: string;
|
|
3828
|
+
likelihood?: "low" | "medium" | "high";
|
|
3829
|
+
impact?: "low" | "medium" | "high";
|
|
3830
|
+
treatment?: "accept" | "mitigate" | "transfer" | "avoid";
|
|
3831
|
+
treatmentRationale?: string;
|
|
3832
|
+
fields?: Record<string, unknown>;
|
|
3833
|
+
},
|
|
3834
|
+
actorId: string,
|
|
3835
|
+
roles?: WorkspaceRole[]
|
|
3836
|
+
) {
|
|
3837
|
+
assertNoReservedFields(input.fields, ["workspaceId"]);
|
|
3838
|
+
assertNoTypedFields(input.fields, "create_risk", RISK_TYPED_FIELDS);
|
|
3839
|
+
assertRiskAcceptanceAccess(input.treatment, roles);
|
|
3840
|
+
if ((input.likelihood && !input.impact) || (!input.likelihood && input.impact)) {
|
|
3841
|
+
throw new Error("create_risk requires both likelihood and impact when creating an initial assessment.");
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
const now = new Date().toISOString();
|
|
3845
|
+
const identifiedEntry: RiskEntry = {
|
|
3846
|
+
id: randomUUID(),
|
|
3847
|
+
entryType: "identified",
|
|
3848
|
+
body: input.body ?? input.topic,
|
|
3849
|
+
createdAt: now,
|
|
3850
|
+
createdBy: actorId
|
|
3851
|
+
};
|
|
3852
|
+
const entries: RiskEntry[] = [identifiedEntry];
|
|
3853
|
+
const fields: Record<string, unknown> = {
|
|
3854
|
+
...(input.fields ?? {}),
|
|
3855
|
+
topic: input.topic,
|
|
3856
|
+
entries,
|
|
3857
|
+
currentStatus: riskEntryToCurrentStatus(identifiedEntry, "open")
|
|
3858
|
+
};
|
|
3859
|
+
|
|
3860
|
+
if (input.likelihood && input.impact) {
|
|
3861
|
+
const assessmentEntry: RiskEntry = {
|
|
3862
|
+
id: randomUUID(),
|
|
3863
|
+
entryType: "assessment",
|
|
3864
|
+
body: "Initial risk assessment.",
|
|
3865
|
+
likelihood: input.likelihood,
|
|
3866
|
+
impact: input.impact,
|
|
3867
|
+
createdAt: now,
|
|
3868
|
+
createdBy: actorId
|
|
3869
|
+
};
|
|
3870
|
+
entries.push(assessmentEntry);
|
|
3871
|
+
fields.currentAssessment = riskEntryToCurrentAssessment(assessmentEntry);
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3874
|
+
if (input.treatment) {
|
|
3875
|
+
const treatmentEntry: RiskEntry = {
|
|
3876
|
+
id: randomUUID(),
|
|
3877
|
+
entryType: "treatment",
|
|
3878
|
+
body: input.treatmentRationale ?? `Initial risk treatment: ${input.treatment}.`,
|
|
3879
|
+
treatment: input.treatment,
|
|
3880
|
+
...(input.treatmentRationale ? { treatmentRationale: input.treatmentRationale } : {}),
|
|
3881
|
+
createdAt: now,
|
|
3882
|
+
createdBy: actorId
|
|
3883
|
+
};
|
|
3884
|
+
entries.push(treatmentEntry);
|
|
3885
|
+
fields.currentTreatment = riskEntryToCurrentTreatment(treatmentEntry);
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
return createRecord(
|
|
3889
|
+
runtime.db,
|
|
3890
|
+
"risks",
|
|
3891
|
+
{
|
|
3892
|
+
workspaceId: input.workspaceId,
|
|
3893
|
+
title: input.title,
|
|
3894
|
+
summary: input.summary ?? input.topic,
|
|
3895
|
+
allowManagedFields: true,
|
|
3896
|
+
fields
|
|
3897
|
+
},
|
|
3898
|
+
actorId
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
async function createIncident(
|
|
3903
|
+
runtime: DxRuntime,
|
|
3904
|
+
input: {
|
|
3905
|
+
workspaceId: string;
|
|
3906
|
+
title: string;
|
|
3907
|
+
description: string;
|
|
3908
|
+
summary?: string;
|
|
3909
|
+
severity?: "low" | "medium" | "high" | "critical";
|
|
3910
|
+
componentIds?: string[];
|
|
3911
|
+
fields?: Record<string, unknown>;
|
|
3912
|
+
},
|
|
3913
|
+
actorId: string
|
|
3914
|
+
) {
|
|
3915
|
+
assertNoReservedFields(input.fields, ["workspaceId", "componentIds"]);
|
|
3916
|
+
assertNoTypedFields(input.fields, "create_incident", INCIDENT_TYPED_FIELDS);
|
|
3917
|
+
|
|
3918
|
+
const componentIds = uniqueIds(input.componentIds ?? []);
|
|
3919
|
+
await assertRuntimeRecordsExist(runtime, input.workspaceId, "components", componentIds);
|
|
3920
|
+
|
|
3921
|
+
const now = new Date().toISOString();
|
|
3922
|
+
const detectedEntry: IncidentEntry = {
|
|
3923
|
+
id: randomUUID(),
|
|
3924
|
+
entryType: "detected",
|
|
3925
|
+
body: input.description,
|
|
3926
|
+
createdAt: now,
|
|
3927
|
+
createdBy: actorId
|
|
3928
|
+
};
|
|
3929
|
+
const entries: IncidentEntry[] = [detectedEntry];
|
|
3930
|
+
const fields: Record<string, unknown> = {
|
|
3931
|
+
...(input.fields ?? {}),
|
|
3932
|
+
description: input.description,
|
|
3933
|
+
entries,
|
|
3934
|
+
currentStatus: incidentEntryToCurrentStatus(detectedEntry, "open")
|
|
3935
|
+
};
|
|
3936
|
+
|
|
3937
|
+
if (input.severity) {
|
|
3938
|
+
const severityEntry: IncidentEntry = {
|
|
3939
|
+
id: randomUUID(),
|
|
3940
|
+
entryType: "severity",
|
|
3941
|
+
body: `Initial severity: ${input.severity}.`,
|
|
3942
|
+
severity: input.severity,
|
|
3943
|
+
createdAt: now,
|
|
3944
|
+
createdBy: actorId
|
|
3945
|
+
};
|
|
3946
|
+
entries.push(severityEntry);
|
|
3947
|
+
fields.currentSeverity = incidentEntryToCurrentSeverity(severityEntry);
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
let incident = await createRecord(
|
|
3951
|
+
runtime.db,
|
|
3952
|
+
"incidents",
|
|
3953
|
+
{
|
|
3954
|
+
workspaceId: input.workspaceId,
|
|
3955
|
+
title: input.title,
|
|
3956
|
+
summary: input.summary ?? input.description,
|
|
3957
|
+
allowManagedFields: true,
|
|
3958
|
+
fields
|
|
3959
|
+
},
|
|
3960
|
+
actorId
|
|
3961
|
+
);
|
|
3962
|
+
|
|
3963
|
+
for (const componentId of componentIds) {
|
|
3964
|
+
incident = await ensureLinkRecords(
|
|
3965
|
+
runtime,
|
|
3966
|
+
{
|
|
3967
|
+
workspaceId: input.workspaceId,
|
|
3968
|
+
fromType: "incidents",
|
|
3969
|
+
fromId: incident._id,
|
|
3970
|
+
toType: "components",
|
|
3971
|
+
toId: componentId,
|
|
3972
|
+
relationship: "affects"
|
|
3973
|
+
},
|
|
3974
|
+
actorId
|
|
3975
|
+
);
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3978
|
+
return incident;
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
async function createSupportRequest(
|
|
3982
|
+
runtime: DxRuntime,
|
|
3983
|
+
input: {
|
|
3984
|
+
workspaceId: string;
|
|
3985
|
+
title: string;
|
|
3986
|
+
reporter: string;
|
|
3987
|
+
kind: string;
|
|
3988
|
+
reportedExperience: string;
|
|
3989
|
+
summary?: string;
|
|
3990
|
+
fields?: Record<string, unknown>;
|
|
3991
|
+
},
|
|
3992
|
+
actorId: string
|
|
3993
|
+
) {
|
|
3994
|
+
assertNoReservedFields(input.fields, ["workspaceId"]);
|
|
3995
|
+
assertNoTypedFields(input.fields, "create_support_request", SUPPORT_REQUEST_TYPED_FIELDS);
|
|
3996
|
+
|
|
3997
|
+
const now = new Date().toISOString();
|
|
3998
|
+
const raisedEntry: SupportRequestEntry = {
|
|
3999
|
+
id: randomUUID(),
|
|
4000
|
+
entryType: "raised",
|
|
4001
|
+
body: input.reportedExperience,
|
|
4002
|
+
createdAt: now,
|
|
4003
|
+
createdBy: actorId
|
|
4004
|
+
};
|
|
4005
|
+
|
|
4006
|
+
return createRecord(
|
|
4007
|
+
runtime.db,
|
|
4008
|
+
"support_requests",
|
|
4009
|
+
{
|
|
4010
|
+
workspaceId: input.workspaceId,
|
|
4011
|
+
title: input.title,
|
|
4012
|
+
summary: input.summary ?? input.reportedExperience,
|
|
4013
|
+
allowManagedFields: true,
|
|
4014
|
+
fields: {
|
|
4015
|
+
...(input.fields ?? {}),
|
|
4016
|
+
reporter: input.reporter,
|
|
4017
|
+
kind: input.kind,
|
|
4018
|
+
reportedExperience: input.reportedExperience,
|
|
4019
|
+
entries: [raisedEntry],
|
|
4020
|
+
currentStatus: supportRequestEntryToCurrentStatus(raisedEntry)
|
|
4021
|
+
}
|
|
4022
|
+
},
|
|
4023
|
+
actorId
|
|
4024
|
+
);
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
async function appendSupportRequestEntryForTool(
|
|
4028
|
+
runtime: DxRuntime,
|
|
4029
|
+
input: {
|
|
4030
|
+
workspaceId: string;
|
|
4031
|
+
supportRequestId: string;
|
|
4032
|
+
entryType: "raised" | "triage" | "update" | "escalated" | "resolved" | "reopened" | "note";
|
|
4033
|
+
body: string;
|
|
4034
|
+
incidentId?: string;
|
|
4035
|
+
},
|
|
4036
|
+
actorId: string
|
|
4037
|
+
) {
|
|
4038
|
+
if (input.incidentId) {
|
|
4039
|
+
const incident = await getRecord(runtime.db, "incidents", input.incidentId, {
|
|
4040
|
+
workspaceId: input.workspaceId
|
|
4041
|
+
});
|
|
4042
|
+
if (!incident) {
|
|
4043
|
+
throw new Error(`Incident not found: incidents/${input.incidentId}`);
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
const supportRequest = await appendSupportRequestEntry(runtime.db, input, actorId);
|
|
4048
|
+
|
|
4049
|
+
if (!input.incidentId) {
|
|
4050
|
+
return supportRequest;
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
return ensureLinkRecords(
|
|
4054
|
+
runtime,
|
|
4055
|
+
{
|
|
4056
|
+
workspaceId: input.workspaceId,
|
|
4057
|
+
fromType: "support_requests",
|
|
4058
|
+
fromId: supportRequest._id,
|
|
4059
|
+
toType: "incidents",
|
|
4060
|
+
toId: input.incidentId,
|
|
4061
|
+
relationship: "spawned_incident"
|
|
4062
|
+
},
|
|
4063
|
+
actorId
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
async function createProblem(
|
|
4068
|
+
runtime: DxRuntime,
|
|
4069
|
+
input: {
|
|
4070
|
+
workspaceId: string;
|
|
4071
|
+
title: string;
|
|
4072
|
+
description: string;
|
|
4073
|
+
summary?: string;
|
|
4074
|
+
incidentIds: string[];
|
|
4075
|
+
componentIds?: string[];
|
|
4076
|
+
fields?: Record<string, unknown>;
|
|
4077
|
+
},
|
|
4078
|
+
actorId: string
|
|
4079
|
+
) {
|
|
4080
|
+
assertNoReservedFields(input.fields, ["workspaceId", "incidentIds", "componentIds"]);
|
|
4081
|
+
assertNoTypedFields(input.fields, "create_problem", PROBLEM_TYPED_FIELDS);
|
|
4082
|
+
|
|
4083
|
+
const incidentIds = uniqueIds(input.incidentIds);
|
|
4084
|
+
const componentIds = uniqueIds(input.componentIds ?? []);
|
|
4085
|
+
if (incidentIds.length === 0) {
|
|
4086
|
+
throw new Error("create_problem requires at least one incidentId.");
|
|
4087
|
+
}
|
|
4088
|
+
await assertRuntimeRecordsExist(runtime, input.workspaceId, "incidents", incidentIds);
|
|
4089
|
+
await assertRuntimeRecordsExist(runtime, input.workspaceId, "components", componentIds);
|
|
4090
|
+
|
|
4091
|
+
const now = new Date().toISOString();
|
|
4092
|
+
const identifiedEntry: ProblemEntry = {
|
|
4093
|
+
id: randomUUID(),
|
|
4094
|
+
entryType: "identified",
|
|
4095
|
+
body: input.description,
|
|
4096
|
+
createdAt: now,
|
|
4097
|
+
createdBy: actorId
|
|
4098
|
+
};
|
|
4099
|
+
let problem = await createRecord(
|
|
4100
|
+
runtime.db,
|
|
4101
|
+
"problems",
|
|
4102
|
+
{
|
|
4103
|
+
workspaceId: input.workspaceId,
|
|
4104
|
+
title: input.title,
|
|
4105
|
+
summary: input.summary ?? input.description,
|
|
4106
|
+
allowManagedFields: true,
|
|
4107
|
+
fields: {
|
|
4108
|
+
...(input.fields ?? {}),
|
|
4109
|
+
description: input.description,
|
|
4110
|
+
entries: [identifiedEntry],
|
|
4111
|
+
currentStatus: problemEntryToCurrentStatus(identifiedEntry, "open")
|
|
4112
|
+
}
|
|
4113
|
+
},
|
|
4114
|
+
actorId
|
|
4115
|
+
);
|
|
4116
|
+
|
|
4117
|
+
for (const incidentId of incidentIds) {
|
|
4118
|
+
problem = await ensureLinkRecords(
|
|
4119
|
+
runtime,
|
|
4120
|
+
{
|
|
4121
|
+
workspaceId: input.workspaceId,
|
|
4122
|
+
fromType: "problems",
|
|
4123
|
+
fromId: problem._id,
|
|
4124
|
+
toType: "incidents",
|
|
4125
|
+
toId: incidentId,
|
|
4126
|
+
relationship: "evidenced_by"
|
|
4127
|
+
},
|
|
4128
|
+
actorId
|
|
4129
|
+
);
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
for (const componentId of componentIds) {
|
|
4133
|
+
problem = await ensureLinkRecords(
|
|
4134
|
+
runtime,
|
|
4135
|
+
{
|
|
4136
|
+
workspaceId: input.workspaceId,
|
|
4137
|
+
fromType: "problems",
|
|
4138
|
+
fromId: problem._id,
|
|
4139
|
+
toType: "components",
|
|
4140
|
+
toId: componentId,
|
|
4141
|
+
relationship: "affects"
|
|
4142
|
+
},
|
|
4143
|
+
actorId
|
|
4144
|
+
);
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
return problem;
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
function uniqueIds(ids: string[]): string[] {
|
|
4151
|
+
return [...new Set(ids.map((id) => id.trim()).filter(Boolean))];
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
async function assertRuntimeRecordsExist(
|
|
4155
|
+
runtime: DxRuntime,
|
|
4156
|
+
workspaceId: string,
|
|
4157
|
+
recordType: CollectionName,
|
|
4158
|
+
ids: string[]
|
|
4159
|
+
): Promise<void> {
|
|
4160
|
+
for (const id of ids) {
|
|
4161
|
+
const record = await getRecord(runtime.db, recordType, id, { workspaceId });
|
|
4162
|
+
if (!record) {
|
|
4163
|
+
throw new Error(`Record not found: ${recordType}/${id}`);
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
|
|
3175
4168
|
async function createChange(
|
|
3176
4169
|
runtime: DxRuntime,
|
|
3177
4170
|
input: {
|
|
3178
4171
|
workspaceId: string;
|
|
3179
4172
|
title: string;
|
|
3180
4173
|
summary?: string;
|
|
4174
|
+
changeType?: "standard" | "normal" | "emergency";
|
|
4175
|
+
impactGrade?: "minor" | "significant" | "major";
|
|
4176
|
+
emergencyImportance?: string;
|
|
4177
|
+
emergencyImmediacy?: string;
|
|
3181
4178
|
changePlan: string;
|
|
3182
4179
|
executionSteps: string[];
|
|
3183
4180
|
rollbackPlan: string;
|
|
@@ -3190,6 +4187,7 @@ async function createChange(
|
|
|
3190
4187
|
) {
|
|
3191
4188
|
assertNoReservedFields(input.fields, ["workspaceId", "initiativeId", "requirementId"]);
|
|
3192
4189
|
assertNoTypedFields(input.fields, "create_change", CHANGE_TYPED_FIELDS);
|
|
4190
|
+
assertChangeTypeInput(input);
|
|
3193
4191
|
|
|
3194
4192
|
if (input.requirementId) {
|
|
3195
4193
|
const requirement = await getRecord(runtime.db, "requirements", input.requirementId, {
|
|
@@ -3210,6 +4208,11 @@ async function createChange(
|
|
|
3210
4208
|
allowManagedFields: true,
|
|
3211
4209
|
fields: {
|
|
3212
4210
|
...(input.fields ?? {}),
|
|
4211
|
+
changeType: input.changeType ?? "normal",
|
|
4212
|
+
...(input.impactGrade !== undefined ? { impactGrade: input.impactGrade } : {}),
|
|
4213
|
+
...(input.emergencyImportance !== undefined ? { emergencyImportance: input.emergencyImportance } : {}),
|
|
4214
|
+
...(input.emergencyImmediacy !== undefined ? { emergencyImmediacy: input.emergencyImmediacy } : {}),
|
|
4215
|
+
...emergencyRationaleGapFields(input),
|
|
3213
4216
|
changePlan: input.changePlan,
|
|
3214
4217
|
executionSteps: input.executionSteps,
|
|
3215
4218
|
rollbackPlan: input.rollbackPlan,
|
|
@@ -3242,7 +4245,6 @@ function buildChangeEventContent(input: {
|
|
|
3242
4245
|
eventType:
|
|
3243
4246
|
| "notice_given"
|
|
3244
4247
|
| "veto_recorded"
|
|
3245
|
-
| "emergency_declared"
|
|
3246
4248
|
| "decision_recorded"
|
|
3247
4249
|
| "result_reported"
|
|
3248
4250
|
| "recovery_recorded"
|
|
@@ -3253,18 +4255,29 @@ function buildChangeEventContent(input: {
|
|
|
3253
4255
|
effectiveAt?: string;
|
|
3254
4256
|
vetoByRole?: "Owner" | "Engineer";
|
|
3255
4257
|
reason?: string;
|
|
3256
|
-
importance?: string;
|
|
3257
|
-
immediacy?: string;
|
|
3258
4258
|
decision?: "proceed" | "defer" | "cancel";
|
|
3259
4259
|
result?: "completed" | "failed" | "rolled_back";
|
|
4260
|
+
gitCommitReferences?: Array<{
|
|
4261
|
+
commit: string;
|
|
4262
|
+
repository?: string;
|
|
4263
|
+
url?: string;
|
|
4264
|
+
}>;
|
|
3260
4265
|
summary?: string;
|
|
3261
4266
|
note?: string;
|
|
4267
|
+
revisedChangeType?: "standard" | "normal" | "emergency";
|
|
4268
|
+
revisedImpactGrade?: "minor" | "significant" | "major";
|
|
4269
|
+
revisedEmergencyImportance?: string;
|
|
4270
|
+
revisedEmergencyImmediacy?: string;
|
|
3262
4271
|
revisedChangePlan?: string;
|
|
3263
4272
|
revisedExecutionSteps?: string[];
|
|
3264
4273
|
revisedRollbackPlan?: string;
|
|
3265
4274
|
revisedRiskImpact?: string;
|
|
3266
4275
|
revisedPlannedFor?: string;
|
|
3267
4276
|
}): Record<string, unknown> {
|
|
4277
|
+
if (input.gitCommitReferences && input.eventType !== "result_reported" && input.eventType !== "recovery_recorded") {
|
|
4278
|
+
throw new Error("gitCommitReferences are only valid on result_reported or recovery_recorded Change events.");
|
|
4279
|
+
}
|
|
4280
|
+
|
|
3268
4281
|
switch (input.eventType) {
|
|
3269
4282
|
case "notice_given":
|
|
3270
4283
|
return compactObject({
|
|
@@ -3278,12 +4291,6 @@ function buildChangeEventContent(input: {
|
|
|
3278
4291
|
reason: input.reason,
|
|
3279
4292
|
summary: input.summary
|
|
3280
4293
|
});
|
|
3281
|
-
case "emergency_declared":
|
|
3282
|
-
return compactObject({
|
|
3283
|
-
importance: requireChangeEventField(input.importance, input.eventType, "importance"),
|
|
3284
|
-
immediacy: requireChangeEventField(input.immediacy, input.eventType, "immediacy"),
|
|
3285
|
-
summary: input.summary
|
|
3286
|
-
});
|
|
3287
4294
|
case "decision_recorded":
|
|
3288
4295
|
return compactObject({
|
|
3289
4296
|
decision: requireChangeEventField(input.decision, input.eventType, "decision"),
|
|
@@ -3293,15 +4300,21 @@ function buildChangeEventContent(input: {
|
|
|
3293
4300
|
case "result_reported":
|
|
3294
4301
|
return compactObject({
|
|
3295
4302
|
result: requireChangeEventField(input.result, input.eventType, "result"),
|
|
4303
|
+
gitCommitReferences: input.gitCommitReferences,
|
|
3296
4304
|
summary: input.summary
|
|
3297
4305
|
});
|
|
3298
4306
|
case "recovery_recorded":
|
|
3299
4307
|
return compactObject({
|
|
3300
|
-
summary: requireChangeEventField(input.summary, input.eventType, "summary")
|
|
4308
|
+
summary: requireChangeEventField(input.summary, input.eventType, "summary"),
|
|
4309
|
+
gitCommitReferences: input.gitCommitReferences
|
|
3301
4310
|
});
|
|
3302
4311
|
case "plan_revised": {
|
|
3303
4312
|
const event = compactObject({
|
|
3304
4313
|
summary: input.summary,
|
|
4314
|
+
revisedChangeType: input.revisedChangeType,
|
|
4315
|
+
revisedImpactGrade: input.revisedImpactGrade,
|
|
4316
|
+
revisedEmergencyImportance: input.revisedEmergencyImportance,
|
|
4317
|
+
revisedEmergencyImmediacy: input.revisedEmergencyImmediacy,
|
|
3305
4318
|
revisedChangePlan: input.revisedChangePlan,
|
|
3306
4319
|
revisedExecutionSteps: input.revisedExecutionSteps,
|
|
3307
4320
|
revisedRollbackPlan: input.revisedRollbackPlan,
|
|
@@ -3328,6 +4341,48 @@ function requireChangeEventField<T>(value: T | undefined, eventType: string, fie
|
|
|
3328
4341
|
return value;
|
|
3329
4342
|
}
|
|
3330
4343
|
|
|
4344
|
+
function assertRiskAcceptanceAccess(treatment: "accept" | "mitigate" | "transfer" | "avoid" | undefined, roles: WorkspaceRole[] | undefined): void {
|
|
4345
|
+
if (treatment !== "accept" || !roles || roles.includes("owner")) {
|
|
4346
|
+
return;
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
throw new Error("Formal risk acceptance requires the Owner role. Other roles may record non-acceptance treatment or proceed with visible open risk.");
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
function assertChangeTypeInput(input: {
|
|
4353
|
+
changeType?: "standard" | "normal" | "emergency";
|
|
4354
|
+
impactGrade?: "minor" | "significant" | "major";
|
|
4355
|
+
emergencyImportance?: string;
|
|
4356
|
+
emergencyImmediacy?: string;
|
|
4357
|
+
}): void {
|
|
4358
|
+
const changeType = input.changeType ?? "normal";
|
|
4359
|
+
|
|
4360
|
+
if (input.impactGrade && changeType !== "normal") {
|
|
4361
|
+
throw new Error("impactGrade is only valid for normal changes.");
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
if ((input.emergencyImportance || input.emergencyImmediacy) && changeType !== "emergency") {
|
|
4365
|
+
throw new Error("emergencyImportance and emergencyImmediacy are only valid for emergency changes.");
|
|
4366
|
+
}
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
function emergencyRationaleGapFields(input: {
|
|
4370
|
+
changeType?: "standard" | "normal" | "emergency";
|
|
4371
|
+
emergencyImportance?: string;
|
|
4372
|
+
emergencyImmediacy?: string;
|
|
4373
|
+
}): Record<string, unknown> {
|
|
4374
|
+
if ((input.changeType ?? "normal") !== "emergency") {
|
|
4375
|
+
return {};
|
|
4376
|
+
}
|
|
4377
|
+
|
|
4378
|
+
const missing = [
|
|
4379
|
+
...(input.emergencyImportance ? [] : ["emergencyImportance"]),
|
|
4380
|
+
...(input.emergencyImmediacy ? [] : ["emergencyImmediacy"])
|
|
4381
|
+
];
|
|
4382
|
+
|
|
4383
|
+
return missing.length > 0 ? { emergencyRationaleGaps: missing } : {};
|
|
4384
|
+
}
|
|
4385
|
+
|
|
3331
4386
|
function compactObject(input: Record<string, unknown>): Record<string, unknown> {
|
|
3332
4387
|
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
3333
4388
|
}
|