dxcomplete 0.1.0
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/.env.example +11 -0
- package/README.md +215 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +212 -0
- package/dist/http/server.d.ts +7 -0
- package/dist/http/server.js +236 -0
- package/dist/http/service.d.ts +7 -0
- package/dist/http/service.js +725 -0
- package/dist/init.d.ts +13 -0
- package/dist/init.js +128 -0
- package/dist/install-manifest.d.ts +25 -0
- package/dist/install-manifest.js +96 -0
- package/dist/mcp/docs.d.ts +98 -0
- package/dist/mcp/docs.js +438 -0
- package/dist/mcp/server.d.ts +20 -0
- package/dist/mcp/server.js +2345 -0
- package/dist/package-root.d.ts +2 -0
- package/dist/package-root.js +28 -0
- package/dist/runtime/actor.d.ts +14 -0
- package/dist/runtime/actor.js +42 -0
- package/dist/runtime/auth.d.ts +162 -0
- package/dist/runtime/auth.js +394 -0
- package/dist/runtime/check.d.ts +7 -0
- package/dist/runtime/check.js +16 -0
- package/dist/runtime/config.d.ts +17 -0
- package/dist/runtime/config.js +93 -0
- package/dist/runtime/mongo.d.ts +9 -0
- package/dist/runtime/mongo.js +56 -0
- package/dist/runtime/records.d.ts +336 -0
- package/dist/runtime/records.js +1463 -0
- package/dist/runtime/workspace.d.ts +19 -0
- package/dist/runtime/workspace.js +102 -0
- package/dist/upgrade.d.ts +20 -0
- package/dist/upgrade.js +246 -0
- package/dist/validate.d.ts +10 -0
- package/dist/validate.js +119 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +12 -0
- package/docs/codex-integration.md +29 -0
- package/docs/cost-model.md +61 -0
- package/docs/decision-basis.md +57 -0
- package/docs/diagrams.md +31 -0
- package/docs/glossary.md +147 -0
- package/docs/index.md +60 -0
- package/docs/model.md +110 -0
- package/docs/open-questions.md +61 -0
- package/docs/roles.md +42 -0
- package/docs/taxonomy.md +96 -0
- package/docs/workflows.md +60 -0
- package/package.json +62 -0
- package/scripts/check-env-surface.mjs +136 -0
- package/scripts/check-public-copy.mjs +263 -0
- package/scripts/check-service-boundary.mjs +63 -0
- package/scripts/dogfood-work-order.mjs +506 -0
- package/scripts/smoke-mcp-http.mjs +3572 -0
- package/src/cli.ts +268 -0
- package/src/http/server.ts +314 -0
- package/src/http/service.ts +934 -0
- package/src/init.ts +227 -0
- package/src/install-manifest.ts +144 -0
- package/src/mcp/docs.ts +557 -0
- package/src/mcp/server.ts +3525 -0
- package/src/package-root.ts +31 -0
- package/src/runtime/actor.ts +61 -0
- package/src/runtime/auth.ts +673 -0
- package/src/runtime/check.ts +18 -0
- package/src/runtime/config.ts +128 -0
- package/src/runtime/mongo.ts +89 -0
- package/src/runtime/records.ts +2303 -0
- package/src/runtime/workspace.ts +155 -0
- package/src/upgrade.ts +356 -0
- package/src/validate.ts +139 -0
- package/src/version.ts +16 -0
- package/templates/github/workflows/dxcomplete.yml +16 -0
- package/templates/next/pages/api/auth/callback/google.js +12 -0
- package/templates/next/pages/api/dxcomplete/[...path].js +12 -0
- package/templates/next/pages/api/dxcomplete.js +12 -0
- package/templates/next/pages/api/mcp.js +12 -0
- package/templates/next/vercel.json +18 -0
- package/templates/process/README.md +38 -0
- package/templates/process/controls.yml +113 -0
- package/templates/process/cost-model.yml +71 -0
- package/templates/process/decision-basis.yml +53 -0
- package/templates/process/decisions/.gitkeep +1 -0
- package/templates/process/diagrams/00-decision-basis.mmd +24 -0
- package/templates/process/diagrams/00-overview.mmd +20 -0
- package/templates/process/diagrams/01-intake-triage.mmd +20 -0
- package/templates/process/diagrams/02-product-definition.mmd +14 -0
- package/templates/process/diagrams/03-engineering-execution.mmd +15 -0
- package/templates/process/diagrams/04-qa-verification.mmd +12 -0
- package/templates/process/diagrams/05-product-validation.mmd +12 -0
- package/templates/process/diagrams/06-change-release-control.mmd +16 -0
- package/templates/process/diagrams/07-deployment-operations.mmd +16 -0
- package/templates/process/diagrams/08-support-incident-management.mmd +16 -0
- package/templates/process/diagrams/09-problem-improvement.mmd +14 -0
- package/templates/process/diagrams/10-risk-control-management.mmd +14 -0
- package/templates/process/diagrams/11-audit-evidence-capture.mmd +13 -0
- package/templates/process/evidence/.gitkeep +1 -0
- package/templates/process/risks/.gitkeep +1 -0
- package/templates/process/roles.yml +96 -0
- package/templates/process/taxonomy.yml +514 -0
- package/templates/process/workflows.yml +210 -0
- package/website/.well-known/oauth-authorization-server +22 -0
- package/website/.well-known/oauth-protected-resource/api/dxcomplete/mcp +10 -0
- package/website/.well-known/oauth-protected-resource/api/mcp +10 -0
- package/website/README.md +12 -0
- package/website/app.js +36 -0
- package/website/flow.html +85 -0
- package/website/glossary.html +280 -0
- package/website/index.html +90 -0
- package/website/objects.html +287 -0
- package/website/outcomes.html +117 -0
- package/website/phase-build.html +101 -0
- package/website/phase-elicit.html +102 -0
- package/website/phase-go-live.html +103 -0
- package/website/phase-measure.html +93 -0
- package/website/phase-operate.html +102 -0
- package/website/phase-orient.html +92 -0
- package/website/phase-weigh.html +98 -0
- package/website/roles.html +52 -0
- package/website/styles.css +1169 -0
|
@@ -0,0 +1,3525 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import * as z from "zod/v4";
|
|
4
|
+
import { DOC_PAGE_IDS, DOC_REFERENCE, getDocReference } from "./docs.js";
|
|
5
|
+
import { createLocalActorContext, type ActorContext } from "../runtime/actor.js";
|
|
6
|
+
import type { WorkspaceRole } from "../runtime/auth.js";
|
|
7
|
+
import type { DxRuntime } from "../runtime/mongo.js";
|
|
8
|
+
import {
|
|
9
|
+
DXCOMPLETE_PACKAGE_VERSION,
|
|
10
|
+
MCP_SURFACE_ID,
|
|
11
|
+
WORKSPACE_COMPATIBILITY_VERSION
|
|
12
|
+
} from "../version.js";
|
|
13
|
+
import {
|
|
14
|
+
COLLECTION_NAMES,
|
|
15
|
+
RUNTIME_ACTOR_ID,
|
|
16
|
+
appendChangeEvent,
|
|
17
|
+
appendDecisionEntry,
|
|
18
|
+
appendDeferralEvent,
|
|
19
|
+
appendJournalNote,
|
|
20
|
+
appendJournalSummary,
|
|
21
|
+
appendReviewNote,
|
|
22
|
+
appendTaskEntry,
|
|
23
|
+
archiveRecord,
|
|
24
|
+
appendDxcompleteTicket,
|
|
25
|
+
archiveDxcompleteTicket,
|
|
26
|
+
type CollectionName,
|
|
27
|
+
type DecisionEntry,
|
|
28
|
+
decisionEntryToCurrentDecision,
|
|
29
|
+
createDxcompleteTicket,
|
|
30
|
+
createRecord,
|
|
31
|
+
getJournalEntry,
|
|
32
|
+
getRecord,
|
|
33
|
+
linkRecords,
|
|
34
|
+
listDxcompleteTickets,
|
|
35
|
+
listLinkedRecords,
|
|
36
|
+
listUnreadDxcompleteTicketReplies,
|
|
37
|
+
listRecords,
|
|
38
|
+
readJournal,
|
|
39
|
+
readDxcompleteTicket,
|
|
40
|
+
type TaskEntry,
|
|
41
|
+
taskEntryToCurrentStatus,
|
|
42
|
+
unlinkRecords,
|
|
43
|
+
updateRecord
|
|
44
|
+
} from "../runtime/records.js";
|
|
45
|
+
|
|
46
|
+
const collectionSchema = z.enum(COLLECTION_NAMES);
|
|
47
|
+
type WorkspaceScopedCollectionName = Exclude<CollectionName, "workspaces">;
|
|
48
|
+
const workspaceScopedCollectionNames = COLLECTION_NAMES.filter(
|
|
49
|
+
(name): name is WorkspaceScopedCollectionName => name !== "workspaces"
|
|
50
|
+
) as [WorkspaceScopedCollectionName, ...WorkspaceScopedCollectionName[]];
|
|
51
|
+
const hostedCollectionSchema = z.enum(workspaceScopedCollectionNames);
|
|
52
|
+
const amountSchema = z.number().finite().optional();
|
|
53
|
+
const requiredAmountSchema = z.number().finite().nonnegative();
|
|
54
|
+
const currencySchema = z.string().min(3).max(12).optional();
|
|
55
|
+
const requiredCurrencySchema = z.string().min(3).max(12);
|
|
56
|
+
const workspaceIdSchema = z.string().min(1);
|
|
57
|
+
const workspaceModeSchema = z.enum(["transformation", "greenfield", "limited-disclosure"]);
|
|
58
|
+
const expectationApprovalStateSchema = z.enum(["draft", "approved", "not_approved", "superseded"]);
|
|
59
|
+
const requirementStatusSchema = z.enum(["draft", "ready", "approved", "superseded"]);
|
|
60
|
+
const taskStatusSchema = z.enum(["open", "in_progress", "blocked", "done"]);
|
|
61
|
+
const decisionEntryTypeSchema = z.enum(["argument", "decision", "note"]);
|
|
62
|
+
const taskEntryTypeSchema = z.enum(["comment", "status_change", "note"]);
|
|
63
|
+
const reviewableRecordTypeSchema = z.enum(["expectations", "requirements"]);
|
|
64
|
+
const decisionInputRecordTypes = [
|
|
65
|
+
"expectations",
|
|
66
|
+
"requirements",
|
|
67
|
+
"commitments",
|
|
68
|
+
"deferrals",
|
|
69
|
+
"journal_entries",
|
|
70
|
+
"environments",
|
|
71
|
+
"components",
|
|
72
|
+
"estimates",
|
|
73
|
+
"benefits",
|
|
74
|
+
"risks",
|
|
75
|
+
"changes",
|
|
76
|
+
"decisions"
|
|
77
|
+
] as const;
|
|
78
|
+
const decisionInputRecordTypeSchema = z.enum(decisionInputRecordTypes);
|
|
79
|
+
type DecisionInputRecordType = (typeof decisionInputRecordTypes)[number];
|
|
80
|
+
const changeEventTypeSchema = z.enum([
|
|
81
|
+
"notice_given",
|
|
82
|
+
"veto_recorded",
|
|
83
|
+
"emergency_declared",
|
|
84
|
+
"decision_recorded",
|
|
85
|
+
"result_reported",
|
|
86
|
+
"recovery_recorded",
|
|
87
|
+
"plan_revised",
|
|
88
|
+
"note_added"
|
|
89
|
+
]);
|
|
90
|
+
const changeVetoRoleSchema = z.enum(["Owner", "Engineer"]);
|
|
91
|
+
const changeDecisionSchema = z.enum(["proceed", "defer", "cancel"]);
|
|
92
|
+
const changeResultSchema = z.enum(["completed", "failed", "rolled_back"]);
|
|
93
|
+
const deferralEventTypeSchema = z.enum([
|
|
94
|
+
"condition_addressed",
|
|
95
|
+
"condition_reopened",
|
|
96
|
+
"condition_note_added",
|
|
97
|
+
"deferral_resolved",
|
|
98
|
+
"deferral_abandoned"
|
|
99
|
+
]);
|
|
100
|
+
const journalTagSchema = z.string().min(1).max(80);
|
|
101
|
+
const recordFieldsSchema = z.record(z.string(), z.unknown()).optional();
|
|
102
|
+
const amountInputSchema = z.union([
|
|
103
|
+
z.object({
|
|
104
|
+
kind: z.literal("single"),
|
|
105
|
+
value: requiredAmountSchema
|
|
106
|
+
}),
|
|
107
|
+
z.object({
|
|
108
|
+
kind: z.literal("range"),
|
|
109
|
+
min: requiredAmountSchema,
|
|
110
|
+
expected: requiredAmountSchema,
|
|
111
|
+
max: requiredAmountSchema
|
|
112
|
+
})
|
|
113
|
+
]);
|
|
114
|
+
const timingSchema = z.enum(["one_time", "recurring"]);
|
|
115
|
+
const estimateLineItemSchema = z.object({
|
|
116
|
+
id: z.string().min(1).optional(),
|
|
117
|
+
label: z.string().min(1),
|
|
118
|
+
timing: timingSchema,
|
|
119
|
+
period: z.string().min(1).optional(),
|
|
120
|
+
currency: requiredCurrencySchema,
|
|
121
|
+
amount: amountInputSchema
|
|
122
|
+
}).strict();
|
|
123
|
+
type EstimateLineItemInput = z.infer<typeof estimateLineItemSchema>;
|
|
124
|
+
const benefitItemSchema = z.object({
|
|
125
|
+
id: z.string().min(1).optional(),
|
|
126
|
+
label: z.string().min(1),
|
|
127
|
+
timing: timingSchema.optional(),
|
|
128
|
+
period: z.string().min(1).optional(),
|
|
129
|
+
currency: requiredCurrencySchema.optional(),
|
|
130
|
+
amount: amountInputSchema.optional()
|
|
131
|
+
}).strict();
|
|
132
|
+
type BenefitItemInput = z.infer<typeof benefitItemSchema>;
|
|
133
|
+
const componentLocatorSchema = z.record(z.string().min(1), z.unknown()).refine(
|
|
134
|
+
(value) => Object.keys(value).length > 0,
|
|
135
|
+
"locator must be a non-empty structured object."
|
|
136
|
+
);
|
|
137
|
+
const componentIdentifiersSchema = z.record(z.string().min(1), z.unknown()).optional();
|
|
138
|
+
const secretPointerSchema = z.object({
|
|
139
|
+
store: z.string().min(1),
|
|
140
|
+
key: z.string().min(1),
|
|
141
|
+
location: z.string().min(1).optional(),
|
|
142
|
+
url: z.string().min(1).optional(),
|
|
143
|
+
note: z.string().min(1).optional()
|
|
144
|
+
}).strict();
|
|
145
|
+
type ComponentLocatorInput = z.infer<typeof componentLocatorSchema>;
|
|
146
|
+
type ComponentIdentifiersInput = z.infer<typeof componentIdentifiersSchema>;
|
|
147
|
+
type SecretPointerInput = z.infer<typeof secretPointerSchema>;
|
|
148
|
+
const decisionInitialEntrySchema = z.object({
|
|
149
|
+
entryType: decisionEntryTypeSchema,
|
|
150
|
+
body: z.string().min(1),
|
|
151
|
+
decidedBy: z.string().min(1).optional(),
|
|
152
|
+
rationale: z.string().min(1).optional()
|
|
153
|
+
}).strict();
|
|
154
|
+
type DecisionInitialEntryInput = z.infer<typeof decisionInitialEntrySchema>;
|
|
155
|
+
const initialDecisionSchema = z.object({
|
|
156
|
+
body: z.string().min(1),
|
|
157
|
+
decidedBy: z.string().min(1).optional(),
|
|
158
|
+
rationale: z.string().min(1).optional()
|
|
159
|
+
}).strict();
|
|
160
|
+
type InitialDecisionInput = z.infer<typeof initialDecisionSchema>;
|
|
161
|
+
const fieldNameSchema = z
|
|
162
|
+
.string()
|
|
163
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_-]*$/, "Only top-level field names are supported.");
|
|
164
|
+
const SERVER_STARTED_AT = new Date().toISOString();
|
|
165
|
+
const STATEMENT_TYPED_FIELDS = ["statement", "source"];
|
|
166
|
+
const EXPECTATION_TYPED_FIELDS = [
|
|
167
|
+
"statement",
|
|
168
|
+
"successRecognition",
|
|
169
|
+
"approvalState",
|
|
170
|
+
"approvedBy",
|
|
171
|
+
"approvedAt",
|
|
172
|
+
"source"
|
|
173
|
+
];
|
|
174
|
+
const REQUIREMENT_TYPED_FIELDS = [
|
|
175
|
+
"statement",
|
|
176
|
+
"acceptanceCriteria",
|
|
177
|
+
"priority",
|
|
178
|
+
"status"
|
|
179
|
+
];
|
|
180
|
+
const CHANGE_TYPED_FIELDS = [
|
|
181
|
+
"changePlan",
|
|
182
|
+
"executionSteps",
|
|
183
|
+
"rollbackPlan",
|
|
184
|
+
"riskImpact",
|
|
185
|
+
"plannedFor",
|
|
186
|
+
"events"
|
|
187
|
+
];
|
|
188
|
+
const COMMITMENT_TYPED_FIELDS = ["commitmentStatement", "reservations"];
|
|
189
|
+
const DEFERRAL_TYPED_FIELDS = ["reason", "status", "conditions", "conditionEvents"];
|
|
190
|
+
const ESTIMATE_TYPED_FIELDS = ["lineItems", "rollup", "versionHistory"];
|
|
191
|
+
const BENEFITS_TYPED_FIELDS = ["benefitItems", "rollup", "versionHistory"];
|
|
192
|
+
const ENVIRONMENT_TYPED_FIELDS = ["name", "description", "versionHistory"];
|
|
193
|
+
const COMPONENT_TYPED_FIELDS = [
|
|
194
|
+
"name",
|
|
195
|
+
"environmentId",
|
|
196
|
+
"kind",
|
|
197
|
+
"locator",
|
|
198
|
+
"identifiers",
|
|
199
|
+
"secretPointers",
|
|
200
|
+
"notes",
|
|
201
|
+
"versionHistory"
|
|
202
|
+
];
|
|
203
|
+
const OBSOLETE_EXPECTATION_FIELDS = ["confirmationState", "ratifiedBy", "ratifiedAt"];
|
|
204
|
+
const DECISION_INPUT_FIELDS = ["informedBy", "informedByIds", "inputRecords"];
|
|
205
|
+
const DECISION_TYPED_FIELDS = [
|
|
206
|
+
"workspaceId",
|
|
207
|
+
"matter",
|
|
208
|
+
"entries",
|
|
209
|
+
"currentDecision",
|
|
210
|
+
"question",
|
|
211
|
+
"decision",
|
|
212
|
+
"decidedBy",
|
|
213
|
+
"rationale",
|
|
214
|
+
"argumentsConsidered",
|
|
215
|
+
"concerns",
|
|
216
|
+
"status"
|
|
217
|
+
];
|
|
218
|
+
const TASK_TYPED_FIELDS = ["workspaceId", "description", "assignee", "assignor", "entries", "currentStatus", "details", "status"];
|
|
219
|
+
const PROCESS_GUIDE = {
|
|
220
|
+
name: "DX Complete process guide",
|
|
221
|
+
status:
|
|
222
|
+
"Current operating guidance for working inside DX Complete. It is a lean guide for MCP clients; it is not the full reference, not a final model, and not a validation rule.",
|
|
223
|
+
currentFlow: ["Statement", "Expectation", "Requirement", "Commitment"],
|
|
224
|
+
phaseGuidance: [
|
|
225
|
+
"These phases describe a typical first-pass order and each has a target end-state, but they are non-terminal and re-enterable: at any point the most useful next activity may belong to any phase, including returning to an earlier one, and no phase is ever closed or locked behind you."
|
|
226
|
+
],
|
|
227
|
+
crossCuttingRecords: [
|
|
228
|
+
{
|
|
229
|
+
record: "Journal",
|
|
230
|
+
use: "Shared workspace context that does not yet belong in a dedicated record; use dedicated records first and Journal only as the fallback."
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
record: "Task",
|
|
234
|
+
use: "Execution work that can be created whenever a phase needs concrete action; Task is not part of the Statement, Expectation, Requirement, Commitment sequence."
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
record: "Decision",
|
|
238
|
+
use: "A recorded choice that can appear in any phase when a meaningful decision needs to remain legible."
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
recordRoutingGuidance: {
|
|
242
|
+
principle:
|
|
243
|
+
"Use the dedicated record first. Journal is the fallback for relevant workspace context only when no dedicated record fits.",
|
|
244
|
+
sharpTest:
|
|
245
|
+
"Will anything reference or depend on this? If yes, prefer a dedicated record that can carry the relationship.",
|
|
246
|
+
routingOrder: [
|
|
247
|
+
{
|
|
248
|
+
when: "The information is desired truth, success criteria, or a buildable commitment.",
|
|
249
|
+
use: "Use Statement, Expectation, or Requirement."
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
when: "The information is a choice weighed between alternatives.",
|
|
253
|
+
use: "Use Decision."
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
when: "The information is an action someone needs to do.",
|
|
257
|
+
use: "Use Task."
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
when: "The information is an event or controlled service/process history.",
|
|
261
|
+
use: "Use Change, Risk, or the matching event/history record."
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
when: "The information is operational infrastructure state.",
|
|
265
|
+
use: "Use Environment and Component records in the Operational Registry; do not use Journal as the long-term home."
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
when: "The information is relevant context with no better home.",
|
|
269
|
+
use: "Use Journal."
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
promotion:
|
|
273
|
+
"If Journal content becomes load-bearing, promote it to the appropriate dedicated record and link back where useful."
|
|
274
|
+
},
|
|
275
|
+
phases: [
|
|
276
|
+
{
|
|
277
|
+
id: "orient",
|
|
278
|
+
name: "Orient",
|
|
279
|
+
purpose:
|
|
280
|
+
"Capture the desired outcome in plain terms, restate expectations, and confirm how success will be recognized where possible.",
|
|
281
|
+
commonRecords: ["Workspace", "Statement", "Expectation", "DX Complete Ticket"],
|
|
282
|
+
processConcepts: [],
|
|
283
|
+
handoff:
|
|
284
|
+
"Expectations describe the desired outcome clearly enough to elicit requirements, or any missing approval is visible as open risk.",
|
|
285
|
+
operatingGuidance: {
|
|
286
|
+
clientRole: "Help the user express the desired outcome in their terms, confirm captured wording before recording, and prepare expectations for approval where needed.",
|
|
287
|
+
conductRules: [
|
|
288
|
+
"Elicit the person's Statement; do not invent it.",
|
|
289
|
+
"When the client articulates Statement or Expectation wording on a user's behalf, confirm that wording before recording it.",
|
|
290
|
+
"Do not store same-person capture-confirmation as record state.",
|
|
291
|
+
"Stay in plain outcome language.",
|
|
292
|
+
"Do not move into technical solution design during Orient.",
|
|
293
|
+
"Treat missing separate authority approval as visible open risk, not as a process block."
|
|
294
|
+
],
|
|
295
|
+
questionsToAsk: [
|
|
296
|
+
"What problem, need, or outcome should DX Complete help clarify?",
|
|
297
|
+
"What would make this successful from the user's point of view?",
|
|
298
|
+
"How will people recognize that the expected outcome has been met?",
|
|
299
|
+
"Is this wording accurate enough to record and use as the basis for requirements?"
|
|
300
|
+
],
|
|
301
|
+
expectedOutput: [
|
|
302
|
+
"Statement record in the person's language.",
|
|
303
|
+
"Restated Expectations with success recognition.",
|
|
304
|
+
"Owner approval where available, or visible open risk where approval is missing."
|
|
305
|
+
],
|
|
306
|
+
exitCheck:
|
|
307
|
+
"Move to Elicit when expectations are clear enough to translate into requirements, or when proceeding without separate authority approval is visible as open risk.",
|
|
308
|
+
checkpointNotes: [
|
|
309
|
+
"Owner approval reduces wrong-outcome risk.",
|
|
310
|
+
"Unapproved expectations can move forward only when the risk is explicit."
|
|
311
|
+
]
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: "elicit",
|
|
316
|
+
name: "Elicit",
|
|
317
|
+
purpose: "Translate expectations into requirements, dependencies, assumptions, unknowns, and risk.",
|
|
318
|
+
commonRecords: ["Statement", "Expectation", "Requirement", "Risk", "Decision", "DX Complete Ticket"],
|
|
319
|
+
processConcepts: ["Requirement"],
|
|
320
|
+
handoff: "The requirement set is clear enough to estimate cost, value, risk, and confidence, or the open risk is visible.",
|
|
321
|
+
operatingGuidance: {
|
|
322
|
+
clientRole: "Translate approved or visibly unapproved expectations into requirements and related delivery facts.",
|
|
323
|
+
conductRules: [
|
|
324
|
+
"Keep each requirement tied to the expectation it is meant to satisfy.",
|
|
325
|
+
"Separate requirements from implementation tasks.",
|
|
326
|
+
"Surface dependencies, assumptions, unknowns, and risks before estimating.",
|
|
327
|
+
"Use review notes for Engineer input on expectations or requirements; do not treat review notes as approval, objection state, or blockers.",
|
|
328
|
+
"Do not treat uncertain details as settled requirements."
|
|
329
|
+
],
|
|
330
|
+
questionsToAsk: [
|
|
331
|
+
"What must be true for each expectation to be satisfied?",
|
|
332
|
+
"What dependencies, constraints, or unknowns could affect the work?",
|
|
333
|
+
"What assumptions are being made because information is incomplete?",
|
|
334
|
+
"Which risks should be visible before the Owner weighs the work?"
|
|
335
|
+
],
|
|
336
|
+
expectedOutput: [
|
|
337
|
+
"A requirement set.",
|
|
338
|
+
"Review notes where Engineer input should stay visible.",
|
|
339
|
+
"Known dependencies, assumptions, unknowns, and risks.",
|
|
340
|
+
"Open questions, visible risks, or follow-ups where clarity is incomplete."
|
|
341
|
+
],
|
|
342
|
+
exitCheck:
|
|
343
|
+
"Move to Weigh when the requirement set is clear enough to support cost, value, risk, and confidence discussion.",
|
|
344
|
+
checkpointNotes: [
|
|
345
|
+
"Requirement clarity reduces estimate risk.",
|
|
346
|
+
"Open unknowns can move forward when they are recorded as visible risk or follow-up."
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: "weigh",
|
|
352
|
+
name: "Weigh",
|
|
353
|
+
purpose:
|
|
354
|
+
"Compare expected cost, expected value, risks, and confidence before recording a Commitment or Deferral.",
|
|
355
|
+
commonRecords: [
|
|
356
|
+
"Estimate",
|
|
357
|
+
"Benefits",
|
|
358
|
+
"Commitment",
|
|
359
|
+
"Deferral",
|
|
360
|
+
"Decision",
|
|
361
|
+
"Risk"
|
|
362
|
+
],
|
|
363
|
+
handoff:
|
|
364
|
+
"The Owner records a Commitment that can move into Build, or a Deferral that makes unmet conditions visible.",
|
|
365
|
+
operatingGuidance: {
|
|
366
|
+
clientRole: "Help the decision authority compare the requirement set against cost, value, risk, and confidence.",
|
|
367
|
+
conductRules: [
|
|
368
|
+
"Make current-state cost context visible where relevant, but do not require complete baseline data.",
|
|
369
|
+
"Generate or review a scope-linked Engineer cost Estimate by default from the elicited requirement set.",
|
|
370
|
+
"Record Owner-authored Benefits separately from Engineer cost Estimates.",
|
|
371
|
+
"Consider important review notes, but do not require the Owner to acknowledge or answer them.",
|
|
372
|
+
"Keep the phase outcome-neutral; do not force a Commitment.",
|
|
373
|
+
"If budget or benefit basis was considered, link the Estimate, Benefits, or Decision rationale to the Commitment; if not, do not fabricate a waiver.",
|
|
374
|
+
"Use Commitment when the Owner is moving forward, with any reservations recorded.",
|
|
375
|
+
"Use Deferral when the Owner is not moving forward yet and wants the unmet conditions made explicit.",
|
|
376
|
+
"Record uncertainty and confidence rather than hiding it.",
|
|
377
|
+
"Do not compute a verdict; DX Complete preserves the basis for the Owner's judgment."
|
|
378
|
+
],
|
|
379
|
+
questionsToAsk: [
|
|
380
|
+
"What is known about current cost, proposed cost, and expected benefit?",
|
|
381
|
+
"How confident is the cost estimate?",
|
|
382
|
+
"Which Benefits, Estimate, or Decision rationale, if any, should inform the Commitment?",
|
|
383
|
+
"What risks or missing information affect whether the Owner can commit?",
|
|
384
|
+
"Is the Owner committing now, or deferring until named conditions are addressed?",
|
|
385
|
+
"If committing, what reservations should be kept visible?",
|
|
386
|
+
"If deferring, what conditions would make a future Commitment possible?"
|
|
387
|
+
],
|
|
388
|
+
expectedOutput: [
|
|
389
|
+
"Decision basis for the Owner's judgment.",
|
|
390
|
+
"Scope-linked Engineer cost Estimate where available.",
|
|
391
|
+
"Owner-authored Benefits where available, including qualitative benefits when they cannot be quantified.",
|
|
392
|
+
"Important review notes considered as input where they exist.",
|
|
393
|
+
"Commitment with linked requirements or expectations, or Deferral with explicit conditions.",
|
|
394
|
+
"Decision with linked inputs where a separate rationale record is useful.",
|
|
395
|
+
"Visible risks."
|
|
396
|
+
],
|
|
397
|
+
exitCheck:
|
|
398
|
+
"Move to Build only after a Commitment; otherwise keep the Deferral conditions visible or return to Elicit as recorded.",
|
|
399
|
+
checkpointNotes: [
|
|
400
|
+
"Incomplete cost or benefit data does not block the decision.",
|
|
401
|
+
"Proceeding with low confidence should leave the open risk visible.",
|
|
402
|
+
"A Deferral is not failure; it is the recorded path to a possible future Commitment."
|
|
403
|
+
]
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
id: "build",
|
|
408
|
+
name: "Build",
|
|
409
|
+
purpose: "Turn committed requirements into working changes.",
|
|
410
|
+
commonRecords: ["Commitment", "Requirement", "Task", "Decision", "Risk"],
|
|
411
|
+
handoff: "The change is ready for release preparation and readiness checks.",
|
|
412
|
+
operatingGuidance: {
|
|
413
|
+
clientRole: "Help the team break committed requirements into tasks and track implementation evidence.",
|
|
414
|
+
conductRules: [
|
|
415
|
+
"Create tasks from requirements covered by a Commitment.",
|
|
416
|
+
"Keep implementation detail connected to the requirement it supports.",
|
|
417
|
+
"Use decisions for meaningful design or tradeoff choices.",
|
|
418
|
+
"Surface blockers and delivery risk early."
|
|
419
|
+
],
|
|
420
|
+
questionsToAsk: [
|
|
421
|
+
"Which requirement does this task implement?",
|
|
422
|
+
"What working change will satisfy the requirement?",
|
|
423
|
+
"What decision, risk, or blocker affects the build?",
|
|
424
|
+
"What evidence will show that the task is complete?"
|
|
425
|
+
],
|
|
426
|
+
expectedOutput: [
|
|
427
|
+
"Tasks linked to requirements.",
|
|
428
|
+
"Working changes or implementation evidence.",
|
|
429
|
+
"Recorded decisions and risks where needed."
|
|
430
|
+
],
|
|
431
|
+
exitCheck: "Move to Go Live when the committed change is built and ready for release preparation.",
|
|
432
|
+
checkpointNotes: [
|
|
433
|
+
"Task completion does not replace requirement verification.",
|
|
434
|
+
"Known build risk should remain visible through release preparation."
|
|
435
|
+
]
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
id: "go_live",
|
|
440
|
+
name: "Go Live",
|
|
441
|
+
purpose: "Prepare the change, confirm readiness, and put it into use.",
|
|
442
|
+
commonRecords: ["Change", "Task", "Decision", "Risk"],
|
|
443
|
+
handoff: "The change is completed, deferred, cancelled, rolled back, or left with its event history visible.",
|
|
444
|
+
operatingGuidance: {
|
|
445
|
+
clientRole: "Help the Operator record the intended service change, readiness basis, execution path, rollback path, and resulting events.",
|
|
446
|
+
conductRules: [
|
|
447
|
+
"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 or impact as the baseline.",
|
|
449
|
+
"Use append-only Change events for notice, veto, emergency, decision, result, recovery, notes, and plan revisions.",
|
|
450
|
+
"Check readiness before putting the change into use.",
|
|
451
|
+
"Make release, rollback, notice, veto, emergency, and communication risks visible.",
|
|
452
|
+
"Do not treat vetoes or readiness checks as mechanical blockers; DX Complete records accountability and does not perform or enforce the operation.",
|
|
453
|
+
"Record open readiness risk when a readiness concern remains open."
|
|
454
|
+
],
|
|
455
|
+
questionsToAsk: [
|
|
456
|
+
"What is changing, why, and when is it planned?",
|
|
457
|
+
"What steps will carry out the change?",
|
|
458
|
+
"What rollback or recovery path exists if something goes wrong?",
|
|
459
|
+
"Who needs to know about the change?",
|
|
460
|
+
"Has anyone recorded a veto or concern?",
|
|
461
|
+
"What readiness concerns are still open?"
|
|
462
|
+
],
|
|
463
|
+
expectedOutput: [
|
|
464
|
+
"Change record with change plan, execution steps, rollback plan, and risk or impact.",
|
|
465
|
+
"Append-only Change events for notice, veto, emergency, decision, result, recovery, notes, or revisions where they occur.",
|
|
466
|
+
"Readiness basis.",
|
|
467
|
+
"Release or service-change decision context.",
|
|
468
|
+
"Visible open risks and any Owner risk-acceptance decisions."
|
|
469
|
+
],
|
|
470
|
+
exitCheck: "Move to Operate when the change event history shows the change is completed, deferred, cancelled, rolled back, or ready to be monitored.",
|
|
471
|
+
checkpointNotes: [
|
|
472
|
+
"Readiness checks reduce launch risk.",
|
|
473
|
+
"A veto by Owner or Engineer is a serious recorded event, not a mechanical stop.",
|
|
474
|
+
"Emergency changes require both importance and immediacy to be recorded.",
|
|
475
|
+
"Open readiness concerns can move forward only when the risk remains visible or is formally accepted by the Owner."
|
|
476
|
+
]
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: "operate",
|
|
481
|
+
name: "Operate",
|
|
482
|
+
purpose: "Run the service, help users, and respond when something goes wrong.",
|
|
483
|
+
commonRecords: ["Change", "DX Complete Ticket", "Risk", "Decision", "Task"],
|
|
484
|
+
handoff: "Operational signals are handled or sent into measurement, improvement, or new work.",
|
|
485
|
+
operatingGuidance: {
|
|
486
|
+
clientRole: "Help keep the service legible while it is running and route operational signals to the right follow-up.",
|
|
487
|
+
conductRules: [
|
|
488
|
+
"Separate immediate user-facing help from deeper improvement work.",
|
|
489
|
+
"Record incidents, risks, decisions, or tasks when operational signals require shared follow-up.",
|
|
490
|
+
"Use Change when the follow-up is a discrete alteration to the running service.",
|
|
491
|
+
"Do not assume the service is finished just because there is no active build.",
|
|
492
|
+
"Surface recurring signals for later improvement or measurement."
|
|
493
|
+
],
|
|
494
|
+
questionsToAsk: [
|
|
495
|
+
"Is the service running as intended?",
|
|
496
|
+
"What user-facing issue, operational issue, or risk needs attention?",
|
|
497
|
+
"Does this signal need immediate response, a task, a decision, or later improvement?",
|
|
498
|
+
"What operational follow-up remains open?"
|
|
499
|
+
],
|
|
500
|
+
expectedOutput: [
|
|
501
|
+
"Handled operational signal or routed follow-up.",
|
|
502
|
+
"Visible incidents, risks, decisions, or tasks where needed.",
|
|
503
|
+
"Ongoing operating state."
|
|
504
|
+
],
|
|
505
|
+
exitCheck:
|
|
506
|
+
"Move to Measure when cost, benefit, reliability, or other operating data is available for comparison or learning.",
|
|
507
|
+
checkpointNotes: [
|
|
508
|
+
"Operational work is part of the lifecycle, not an exception.",
|
|
509
|
+
"Unresolved operating signals should remain visible until handled or accepted."
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: "measure",
|
|
515
|
+
name: "Measure",
|
|
516
|
+
purpose: "Compare expected and actual cost or benefit when data is available.",
|
|
517
|
+
commonRecords: ["Estimate", "Benefits", "Decision", "Risk"],
|
|
518
|
+
handoff: "Learning from actuals is available for future estimates and decisions.",
|
|
519
|
+
operatingGuidance: {
|
|
520
|
+
clientRole: "Help compare estimates to actuals where data exists and feed learning into future decisions.",
|
|
521
|
+
conductRules: [
|
|
522
|
+
"Capture actual cost or benefit observations when available.",
|
|
523
|
+
"Do not block closure or continued operation because actuals are unavailable.",
|
|
524
|
+
"Compare measured results to earlier estimates where possible.",
|
|
525
|
+
"Turn meaningful differences into learning, risk, decision context, or future work."
|
|
526
|
+
],
|
|
527
|
+
questionsToAsk: [
|
|
528
|
+
"What actual cost or benefit data is available?",
|
|
529
|
+
"How does it compare with the estimate?",
|
|
530
|
+
"What should future estimates or decisions learn from this?",
|
|
531
|
+
"Is new work, risk, or a decision needed because of the measurement?"
|
|
532
|
+
],
|
|
533
|
+
expectedOutput: [
|
|
534
|
+
"Actual cost or benefit observations where available.",
|
|
535
|
+
"Comparison to estimates where possible.",
|
|
536
|
+
"Learning for future estimates and decisions."
|
|
537
|
+
],
|
|
538
|
+
exitCheck:
|
|
539
|
+
"Keep operating, refine estimates, or start new Orient/Elicit work when measurement reveals a new need.",
|
|
540
|
+
checkpointNotes: [
|
|
541
|
+
"Actuals improve future estimates but do not block project closure.",
|
|
542
|
+
"Missing measurement data should be visible without being treated as failure."
|
|
543
|
+
]
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
],
|
|
547
|
+
processConcepts: [
|
|
548
|
+
{
|
|
549
|
+
name: "Statement",
|
|
550
|
+
status: "runtime_record",
|
|
551
|
+
currentRuntimeRecordType: "statements",
|
|
552
|
+
use: "The user's own words before DX Complete interprets or translates them.",
|
|
553
|
+
handling: "Track as a workspace-scoped record. Expectations can derive from Statement records through the derives_from relationship."
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
name: "Expectation",
|
|
557
|
+
status: "runtime_record",
|
|
558
|
+
currentRuntimeRecordType: "expectations",
|
|
559
|
+
use: "The expected result and how success will be recognized, captured in user-facing language.",
|
|
560
|
+
handling: "Track as a workspace-scoped record that can link to statements, requirements, risks, and decisions. Separate authority approval is tracked as risk-reducing state; same-person capture-confirmation is client conduct."
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: "Requirement detail",
|
|
564
|
+
status: "requirement_detail",
|
|
565
|
+
currentRuntimeRecordType: "requirements",
|
|
566
|
+
use: "Optional implementation or verification detail kept with the requirement when needed.",
|
|
567
|
+
handling: "Do not treat Specification as a current first-class runtime record."
|
|
568
|
+
}
|
|
569
|
+
],
|
|
570
|
+
checkpointGuidance: [
|
|
571
|
+
"Approval and other checkpoints reduce risk; they do not block progress by themselves.",
|
|
572
|
+
"A checkpoint can be approved or mitigated, formally accepted as risk by the Owner, or proceeded past with open risk still visible.",
|
|
573
|
+
"Formal risk acceptance is an Owner-level decision; domain actors may proceed, but proceeding does not close or accept the risk.",
|
|
574
|
+
"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
|
+
"Use Decision input links to preserve which records informed important choices.",
|
|
576
|
+
"Use Change records for run-side service changes where notice, veto, emergency posture, execution, rollback, and result need an ordered audit trail."
|
|
577
|
+
],
|
|
578
|
+
nextStepGuidance: {
|
|
579
|
+
responsibility:
|
|
580
|
+
"The MCP client derives the next step from the current records, phase handoffs, operating guidance, open risks, and unanswered questions. DX Complete does not compute or prescribe a single server-side next step.",
|
|
581
|
+
conductRules: [
|
|
582
|
+
"Use the current phase, record state, links, open questions, risks, and decisions to reason about what should happen next.",
|
|
583
|
+
"When the next step is uncertain, name the missing information or checkpoint rather than inventing certainty.",
|
|
584
|
+
"Treat the next-step answer as guidance for the user, not as a hidden server decision."
|
|
585
|
+
]
|
|
586
|
+
},
|
|
587
|
+
runtimeRecordTypes: COLLECTION_NAMES,
|
|
588
|
+
deploymentModel: {
|
|
589
|
+
default: "one_mcp_endpoint_per_workspace",
|
|
590
|
+
workspaceBinding:
|
|
591
|
+
"Hosted MCP endpoints get workspace scope from installed repo config, then proxy to the central DX Complete service. Shared record access is scoped by authenticated actor and authorized workspace context. Workspace scope is not an environment variable.",
|
|
592
|
+
databaseModel:
|
|
593
|
+
"Workspace MCP servers do not hold database credentials. The central service owns database access, OAuth exchange, membership checks, readable ID allocation, and MCP tool execution.",
|
|
594
|
+
authorizationModel:
|
|
595
|
+
"Workspace servers authenticate to the central service with a workspace service client. The central service verifies the human actor and derives allowed tool access from stored workspace roles."
|
|
596
|
+
},
|
|
597
|
+
recordGuidance: [
|
|
598
|
+
{
|
|
599
|
+
record: "Workspace",
|
|
600
|
+
use: "The boundary for one service scope, including its name, summary, and mode when useful."
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
record: "Statement",
|
|
604
|
+
use: "A user's own words before interpretation, kept as the traceable root for expectations and downstream work."
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
record: "Journal",
|
|
608
|
+
use: "Shared, append-only workspace notes for relevant background, preferences, observations, and context that has no dedicated record home. Journal notes are visible to workspace members and compacted through summary entries when the hot context grows."
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
record: "DX Complete Ticket",
|
|
612
|
+
use: "Private, appendable communication between the current actor and DX Complete before anyone decides whether shared follow-up is needed."
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
record: "Expectation",
|
|
616
|
+
use: "A user-facing condition of satisfaction that can be approved by separate authority, left not approved with open risk visible, linked to requirements, reviewed with non-blocking notes, and revised with version history."
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
record: "Requirement",
|
|
620
|
+
use: "A team-owned commitment that translates expectations into something buildable and verifiable, with optional non-blocking review notes and version history."
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
record: "Commitment",
|
|
624
|
+
use: "An Owner point-in-time authority record that commits requirements or expectations into Build, with optional reservations kept visible."
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
record: "Deferral",
|
|
628
|
+
use: "An Owner record for not committing yet, with explicit conditions and append-only condition history that can resolve into a Commitment."
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
record: "Task",
|
|
632
|
+
use: "Execution work with an append-only entry log; current status comes from the latest status_change entry."
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
record: "Change",
|
|
636
|
+
use: "A discrete service change record with baseline plan sections and append-only events for notice, veto, emergency posture, decision, result, recovery, notes, and revisions."
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
record: "Environment",
|
|
640
|
+
use: "A named operating context such as local, staging, or production, with version history for operational-state changes."
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
record: "Component",
|
|
644
|
+
use: "One environment-specific operational component with kind, structured locator, identifiers, secret pointers, notes, and version history."
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
record: "Estimate",
|
|
648
|
+
use: "An Engineer cost input for Weigh, with itemized line items, scope links, cost-only roll-up totals, and version history."
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
record: "Benefits",
|
|
652
|
+
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
|
+
},
|
|
654
|
+
{
|
|
655
|
+
record: "Decision",
|
|
656
|
+
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."
|
|
657
|
+
}
|
|
658
|
+
],
|
|
659
|
+
clientGuidance: [
|
|
660
|
+
"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.",
|
|
662
|
+
"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.",
|
|
664
|
+
"Use workspace-scoped records for shared process work.",
|
|
665
|
+
"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
|
+
"Hosted MCP access derives workspace scope from installed repo config and actor scope from OAuth.",
|
|
667
|
+
"Use update_expectation and update_requirement for current content changes so prior versions are preserved.",
|
|
668
|
+
"Use append_decision_entry and append_task_entry for Decision and Task history; do not rewrite their current decision or status directly.",
|
|
669
|
+
"Use link_decision_input when a record informed a Decision; use list_linked_records with relationship informed_by to review the decision inputs.",
|
|
670
|
+
"Use Benefits for Owner-authored benefit lists; use Estimate for Engineer cost estimates.",
|
|
671
|
+
"Use link_records and unlink_records for other relationships between records."
|
|
672
|
+
]
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
type ToolSchema = Record<string, z.ZodType>;
|
|
676
|
+
type ToolHandler = (...args: any[]) => unknown;
|
|
677
|
+
type ToolManifestEntry = {
|
|
678
|
+
name: string;
|
|
679
|
+
description: string;
|
|
680
|
+
inputFields: string[];
|
|
681
|
+
inputSchemaFingerprint: string;
|
|
682
|
+
};
|
|
683
|
+
type ToolManifest = {
|
|
684
|
+
toolCount: number;
|
|
685
|
+
tools: ToolManifestEntry[];
|
|
686
|
+
};
|
|
687
|
+
type McpSurface = ToolManifest & {
|
|
688
|
+
version: string;
|
|
689
|
+
fingerprint: string;
|
|
690
|
+
workspaceCompatibility: number;
|
|
691
|
+
};
|
|
692
|
+
export type McpServerOptions = {
|
|
693
|
+
actor?: ActorContext;
|
|
694
|
+
recordActorId?: string;
|
|
695
|
+
hostedWorkspace?: {
|
|
696
|
+
workspaceId: string;
|
|
697
|
+
name?: string;
|
|
698
|
+
};
|
|
699
|
+
workspaceRoles?: WorkspaceRole[];
|
|
700
|
+
hostedHttp?: {
|
|
701
|
+
canonicalMcpPath: string;
|
|
702
|
+
canonicalMcpUrl: string;
|
|
703
|
+
protectedResourceMetadataUrl: string;
|
|
704
|
+
googleCallbackUrl: string;
|
|
705
|
+
};
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
export function createMcpServer(runtime: DxRuntime, options: McpServerOptions = {}): McpServer {
|
|
709
|
+
const server = new McpServer({
|
|
710
|
+
name: "dxcomplete",
|
|
711
|
+
version: DXCOMPLETE_PACKAGE_VERSION
|
|
712
|
+
});
|
|
713
|
+
const actor = options.actor ?? createLocalActorContext();
|
|
714
|
+
const recordActorId = options.recordActorId ?? (options.actor ? actor.actorId : RUNTIME_ACTOR_ID);
|
|
715
|
+
const hostedWorkspace = options.hostedWorkspace;
|
|
716
|
+
const hostedHttp = options.hostedHttp;
|
|
717
|
+
const activeCollectionSchema = hostedWorkspace ? hostedCollectionSchema : collectionSchema;
|
|
718
|
+
const toolManifestEntries: ToolManifestEntry[] = [];
|
|
719
|
+
|
|
720
|
+
function registerDxcTool<TInputSchema extends ToolSchema>(
|
|
721
|
+
name: string,
|
|
722
|
+
config: {
|
|
723
|
+
description: string;
|
|
724
|
+
inputSchema: TInputSchema;
|
|
725
|
+
},
|
|
726
|
+
handler: ToolHandler
|
|
727
|
+
): void {
|
|
728
|
+
if (hostedWorkspace && name === "create_workspace") {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const inputSchema = hostedWorkspace ? omitWorkspaceId(config.inputSchema) : config.inputSchema;
|
|
733
|
+
|
|
734
|
+
toolManifestEntries.push({
|
|
735
|
+
name,
|
|
736
|
+
description: config.description,
|
|
737
|
+
inputFields: Object.keys(inputSchema).sort(),
|
|
738
|
+
inputSchemaFingerprint: getInputSchemaFingerprint(inputSchema)
|
|
739
|
+
});
|
|
740
|
+
server.registerTool(
|
|
741
|
+
name,
|
|
742
|
+
{
|
|
743
|
+
...config,
|
|
744
|
+
inputSchema
|
|
745
|
+
},
|
|
746
|
+
(async (input: Record<string, unknown>) => {
|
|
747
|
+
const handlerInput = hostedWorkspace
|
|
748
|
+
? injectHostedWorkspace(input, config.inputSchema, hostedWorkspace.workspaceId)
|
|
749
|
+
: input;
|
|
750
|
+
assertHostedWorkspaceAccess(name, handlerInput, hostedWorkspace?.workspaceId);
|
|
751
|
+
assertToolRoleAccess(name, options.workspaceRoles, handlerInput);
|
|
752
|
+
return handler(handlerInput);
|
|
753
|
+
}) as any
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function getToolManifest(): ToolManifest {
|
|
758
|
+
const tools = [...toolManifestEntries].sort((left, right) => left.name.localeCompare(right.name));
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
toolCount: tools.length,
|
|
762
|
+
tools
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function getMcpSurface(): McpSurface {
|
|
767
|
+
const toolManifest = getToolManifest();
|
|
768
|
+
const fingerprint = createHash("sha256")
|
|
769
|
+
.update(
|
|
770
|
+
stableStringify({
|
|
771
|
+
surfaceId: MCP_SURFACE_ID,
|
|
772
|
+
workspaceCompatibility: WORKSPACE_COMPATIBILITY_VERSION,
|
|
773
|
+
tools: toolManifest.tools,
|
|
774
|
+
processGuide: PROCESS_GUIDE,
|
|
775
|
+
docReferences: DOC_REFERENCE
|
|
776
|
+
})
|
|
777
|
+
)
|
|
778
|
+
.digest("hex")
|
|
779
|
+
.slice(0, 16);
|
|
780
|
+
|
|
781
|
+
return {
|
|
782
|
+
version: MCP_SURFACE_ID,
|
|
783
|
+
fingerprint,
|
|
784
|
+
workspaceCompatibility: WORKSPACE_COMPATIBILITY_VERSION,
|
|
785
|
+
...toolManifest
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
registerDxcTool(
|
|
790
|
+
"runtime_status",
|
|
791
|
+
{
|
|
792
|
+
description: "Check the DX Complete runtime connection and configured record collections.",
|
|
793
|
+
inputSchema: {}
|
|
794
|
+
},
|
|
795
|
+
async () => {
|
|
796
|
+
const surface = getMcpSurface();
|
|
797
|
+
|
|
798
|
+
return jsonResult({
|
|
799
|
+
ok: true,
|
|
800
|
+
databaseName: runtime.config.databaseName,
|
|
801
|
+
actorId: recordActorId,
|
|
802
|
+
actor,
|
|
803
|
+
...(hostedWorkspace
|
|
804
|
+
? {
|
|
805
|
+
workspace: {
|
|
806
|
+
workspaceId: hostedWorkspace.workspaceId,
|
|
807
|
+
...(hostedWorkspace.name ? { name: hostedWorkspace.name } : {}),
|
|
808
|
+
source: "central_service_membership",
|
|
809
|
+
...(options.workspaceRoles ? { roles: options.workspaceRoles } : {})
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
: {}),
|
|
813
|
+
collections: COLLECTION_NAMES,
|
|
814
|
+
server: {
|
|
815
|
+
pid: process.pid,
|
|
816
|
+
version: DXCOMPLETE_PACKAGE_VERSION,
|
|
817
|
+
packageVersion: DXCOMPLETE_PACKAGE_VERSION,
|
|
818
|
+
workspaceCompatibility: surface.workspaceCompatibility,
|
|
819
|
+
startedAt: SERVER_STARTED_AT,
|
|
820
|
+
surfaceVersion: surface.version,
|
|
821
|
+
surfaceFingerprint: surface.fingerprint,
|
|
822
|
+
surfaceIncludes: ["tools", "toolSchemas", "processGuide", "docReferences"],
|
|
823
|
+
...(hostedHttp
|
|
824
|
+
? {
|
|
825
|
+
http: hostedHttp
|
|
826
|
+
}
|
|
827
|
+
: {}),
|
|
828
|
+
toolCount: surface.toolCount,
|
|
829
|
+
tools: surface.tools.map((tool) => tool.name),
|
|
830
|
+
toolManifest: surface.tools
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
registerDxcTool(
|
|
837
|
+
"get_process_guide",
|
|
838
|
+
{
|
|
839
|
+
description: "Get the current DX Complete process guide, including phases, common records, and handoff guidance.",
|
|
840
|
+
inputSchema: {}
|
|
841
|
+
},
|
|
842
|
+
async () => {
|
|
843
|
+
const surface = getMcpSurface();
|
|
844
|
+
|
|
845
|
+
return jsonResult({
|
|
846
|
+
surfaceVersion: surface.version,
|
|
847
|
+
surfaceFingerprint: surface.fingerprint,
|
|
848
|
+
workspaceCompatibility: surface.workspaceCompatibility,
|
|
849
|
+
...PROCESS_GUIDE
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
registerDxcTool(
|
|
855
|
+
"get_doc",
|
|
856
|
+
{
|
|
857
|
+
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.",
|
|
859
|
+
inputSchema: {
|
|
860
|
+
page: z.enum(DOC_PAGE_IDS),
|
|
861
|
+
term: z.string().min(1).optional()
|
|
862
|
+
}
|
|
863
|
+
},
|
|
864
|
+
async ({ page, term }) => {
|
|
865
|
+
const surface = getMcpSurface();
|
|
866
|
+
const doc = getDocReference(page, term);
|
|
867
|
+
|
|
868
|
+
return jsonResult({
|
|
869
|
+
surfaceVersion: surface.version,
|
|
870
|
+
surfaceFingerprint: surface.fingerprint,
|
|
871
|
+
workspaceCompatibility: surface.workspaceCompatibility,
|
|
872
|
+
...doc
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
registerDxcTool(
|
|
878
|
+
"create_workspace",
|
|
879
|
+
{
|
|
880
|
+
description: "Create a workspace, the shared-runtime boundary for one service scope.",
|
|
881
|
+
inputSchema: {
|
|
882
|
+
id: workspaceIdSchema.optional(),
|
|
883
|
+
name: z.string().min(1),
|
|
884
|
+
summary: z.string().min(1).optional(),
|
|
885
|
+
mode: workspaceModeSchema.optional(),
|
|
886
|
+
fields: recordFieldsSchema
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
async ({ id, name, summary, mode, fields }) => {
|
|
890
|
+
const existing = id
|
|
891
|
+
? await getRecord(runtime.db, "workspaces", id, { allowAnyWorkspace: true })
|
|
892
|
+
: null;
|
|
893
|
+
|
|
894
|
+
if (existing) {
|
|
895
|
+
return jsonResult(existing);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
return jsonResult(
|
|
899
|
+
await createRecord(
|
|
900
|
+
runtime.db,
|
|
901
|
+
"workspaces",
|
|
902
|
+
{
|
|
903
|
+
...(id ? { id } : {}),
|
|
904
|
+
title: name,
|
|
905
|
+
summary,
|
|
906
|
+
fields: {
|
|
907
|
+
name,
|
|
908
|
+
...(mode ? { mode } : {}),
|
|
909
|
+
...(fields ?? {})
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
recordActorId
|
|
913
|
+
)
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
registerDxcTool(
|
|
919
|
+
"create_record",
|
|
920
|
+
{
|
|
921
|
+
description: "Create a record in one of the DX Complete collections.",
|
|
922
|
+
inputSchema: {
|
|
923
|
+
recordType: activeCollectionSchema,
|
|
924
|
+
workspaceId: workspaceIdSchema.optional(),
|
|
925
|
+
title: z.string().min(1).optional(),
|
|
926
|
+
summary: z.string().min(1).optional(),
|
|
927
|
+
fields: recordFieldsSchema
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
async ({ recordType, workspaceId, title, summary, fields }) =>
|
|
931
|
+
jsonResult(await createRecord(runtime.db, recordType, { workspaceId, title, summary, fields }, recordActorId))
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
registerDxcTool(
|
|
935
|
+
"get_record",
|
|
936
|
+
{
|
|
937
|
+
description: "Get one DX Complete record by type and id.",
|
|
938
|
+
inputSchema: {
|
|
939
|
+
recordType: activeCollectionSchema,
|
|
940
|
+
workspaceId: workspaceIdSchema.optional(),
|
|
941
|
+
id: z.string().min(1)
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
async ({ recordType, workspaceId, id }) => {
|
|
945
|
+
const record = await getRecord(runtime.db, recordType, id, { workspaceId });
|
|
946
|
+
|
|
947
|
+
if (!record) {
|
|
948
|
+
throw new Error(`Record not found: ${recordType}/${id}`);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return jsonResult(record);
|
|
952
|
+
}
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
registerDxcTool(
|
|
956
|
+
"create_dxcomplete_ticket",
|
|
957
|
+
{
|
|
958
|
+
description:
|
|
959
|
+
"Create a private DX Complete Ticket for a question, report, request, correction, or follow-up from the current actor. The body is stored as the first entry; summary is optional and is not generated automatically.",
|
|
960
|
+
inputSchema: {
|
|
961
|
+
title: z.string().min(1),
|
|
962
|
+
body: z.string().min(1)
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
async (input) => jsonResult(await createDxcompleteTicket(runtime.db, input, actor))
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
registerDxcTool(
|
|
969
|
+
"append_dxcomplete_ticket",
|
|
970
|
+
{
|
|
971
|
+
description:
|
|
972
|
+
"Append a follow-up entry to one of the current actor's DX Complete Tickets while preserving earlier entries.",
|
|
973
|
+
inputSchema: {
|
|
974
|
+
id: z.string().min(1),
|
|
975
|
+
body: z.string().min(1)
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
async (input) => jsonResult(await appendDxcompleteTicket(runtime.db, input, actor))
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
registerDxcTool(
|
|
982
|
+
"list_dxcomplete_tickets",
|
|
983
|
+
{
|
|
984
|
+
description:
|
|
985
|
+
"List the current actor's own DX Complete Tickets. Archived tickets are hidden unless includeArchived is true.",
|
|
986
|
+
inputSchema: {
|
|
987
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
988
|
+
includeArchived: z.boolean().optional()
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
async ({ limit, includeArchived }) =>
|
|
992
|
+
jsonResult(await listDxcompleteTickets(runtime.db, actor, limit ?? 20, { includeArchived }))
|
|
993
|
+
);
|
|
994
|
+
|
|
995
|
+
registerDxcTool(
|
|
996
|
+
"list_unread_dxcomplete_ticket_replies",
|
|
997
|
+
{
|
|
998
|
+
description:
|
|
999
|
+
"List tickets with unread DX Complete replies addressed to the current actor. This returns reply identifiers and timestamps only; use read_dxcomplete_ticket to read the ticket and mark replies read.",
|
|
1000
|
+
inputSchema: {
|
|
1001
|
+
limit: z.number().int().min(1).max(100).optional()
|
|
1002
|
+
}
|
|
1003
|
+
},
|
|
1004
|
+
async ({ limit }) => jsonResult(await listUnreadDxcompleteTicketReplies(runtime.db, actor, limit ?? 20))
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
registerDxcTool(
|
|
1008
|
+
"read_dxcomplete_ticket",
|
|
1009
|
+
{
|
|
1010
|
+
description: "Read one DX Complete Ticket owned by the current actor. Reading the ticket marks its unread DX Complete replies as read.",
|
|
1011
|
+
inputSchema: {
|
|
1012
|
+
id: z.string().min(1)
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
async (input) => jsonResult(await readDxcompleteTicket(runtime.db, input, actor))
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
registerDxcTool(
|
|
1019
|
+
"archive_dxcomplete_ticket",
|
|
1020
|
+
{
|
|
1021
|
+
description: "Hide one DX Complete Ticket from the current actor's active ticket list. This is personal cleanup.",
|
|
1022
|
+
inputSchema: {
|
|
1023
|
+
id: z.string().min(1)
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
async ({ id }) => jsonResult(await archiveDxcompleteTicket(runtime.db, { id }, actor))
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
registerDxcTool(
|
|
1030
|
+
"append_journal_note",
|
|
1031
|
+
{
|
|
1032
|
+
description:
|
|
1033
|
+
"Append a shared workspace Journal note only after checking that the information has no better dedicated record home. Any workspace member may append; author and timestamp are captured automatically.",
|
|
1034
|
+
inputSchema: {
|
|
1035
|
+
workspaceId: workspaceIdSchema,
|
|
1036
|
+
body: z.string().min(1),
|
|
1037
|
+
tag: journalTagSchema.optional()
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
async (input) => jsonResult(await appendJournalNote(runtime.db, input, recordActorId))
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
registerDxcTool(
|
|
1044
|
+
"read_journal",
|
|
1045
|
+
{
|
|
1046
|
+
description:
|
|
1047
|
+
"Read the workspace Journal fallback context. By default this returns the hot tier: active summaries plus active raw notes, not the full archive.",
|
|
1048
|
+
inputSchema: {
|
|
1049
|
+
workspaceId: workspaceIdSchema,
|
|
1050
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
1051
|
+
includeArchived: z.boolean().optional()
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
async (input) => jsonResult(await readJournal(runtime.db, input))
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
registerDxcTool(
|
|
1058
|
+
"get_journal_entry",
|
|
1059
|
+
{
|
|
1060
|
+
description:
|
|
1061
|
+
"Fetch one Journal entry by UUID or readable ID, including fallback context entries that have aged out of the hot Journal read.",
|
|
1062
|
+
inputSchema: {
|
|
1063
|
+
workspaceId: workspaceIdSchema,
|
|
1064
|
+
id: z.string().min(1)
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
async (input) => jsonResult(await getJournalEntry(runtime.db, input))
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
registerDxcTool(
|
|
1071
|
+
"append_journal_summary",
|
|
1072
|
+
{
|
|
1073
|
+
description:
|
|
1074
|
+
"Append a Journal summary for fallback context that covers existing Journal entries by UUID or readable ID, then archives the covered entries out of the hot read without deleting them.",
|
|
1075
|
+
inputSchema: {
|
|
1076
|
+
workspaceId: workspaceIdSchema,
|
|
1077
|
+
body: z.string().min(1),
|
|
1078
|
+
covers: z.array(z.string().min(1)).min(1),
|
|
1079
|
+
tag: journalTagSchema.optional()
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
async (input) => jsonResult(await appendJournalSummary(runtime.db, input, recordActorId))
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
registerDxcTool(
|
|
1086
|
+
"create_statement",
|
|
1087
|
+
{
|
|
1088
|
+
description: "Create a Statement record that preserves a person's own words before interpretation.",
|
|
1089
|
+
inputSchema: {
|
|
1090
|
+
workspaceId: workspaceIdSchema,
|
|
1091
|
+
title: z.string().min(1),
|
|
1092
|
+
statement: z.string().min(1),
|
|
1093
|
+
source: z.string().min(1).optional(),
|
|
1094
|
+
fields: recordFieldsSchema
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
async (input) => jsonResult(await createStatement(runtime, input, recordActorId))
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
registerDxcTool(
|
|
1101
|
+
"update_statement",
|
|
1102
|
+
{
|
|
1103
|
+
description: "Update a Statement record while preserving prior versions.",
|
|
1104
|
+
inputSchema: {
|
|
1105
|
+
workspaceId: workspaceIdSchema,
|
|
1106
|
+
id: z.string().min(1),
|
|
1107
|
+
title: z.string().min(1).optional(),
|
|
1108
|
+
statement: z.string().min(1).optional(),
|
|
1109
|
+
source: z.string().min(1).optional(),
|
|
1110
|
+
fields: recordFieldsSchema,
|
|
1111
|
+
unsetFields: z.array(fieldNameSchema).optional(),
|
|
1112
|
+
revisionNote: z.string().min(1).optional()
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
async (input) => jsonResult(await updateStatement(runtime, input, recordActorId))
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
registerDxcTool(
|
|
1119
|
+
"list_environments",
|
|
1120
|
+
{
|
|
1121
|
+
description: "List Operational Registry Environment records for the workspace.",
|
|
1122
|
+
inputSchema: {
|
|
1123
|
+
workspaceId: workspaceIdSchema,
|
|
1124
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1125
|
+
includeArchived: z.boolean().optional()
|
|
1126
|
+
}
|
|
1127
|
+
},
|
|
1128
|
+
async ({ workspaceId, limit, includeArchived }) =>
|
|
1129
|
+
jsonResult(await listRecords(runtime.db, "environments", limit ?? 20, { workspaceId, includeArchived }))
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
registerDxcTool(
|
|
1133
|
+
"create_environment",
|
|
1134
|
+
{
|
|
1135
|
+
description: "Create an Operational Registry Environment, such as local, staging, or production.",
|
|
1136
|
+
inputSchema: {
|
|
1137
|
+
workspaceId: workspaceIdSchema,
|
|
1138
|
+
name: z.string().min(1),
|
|
1139
|
+
summary: z.string().min(1).optional(),
|
|
1140
|
+
description: z.string().min(1).optional(),
|
|
1141
|
+
fields: recordFieldsSchema
|
|
1142
|
+
}
|
|
1143
|
+
},
|
|
1144
|
+
async (input) => jsonResult(await createEnvironment(runtime, input, recordActorId))
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
registerDxcTool(
|
|
1148
|
+
"update_environment",
|
|
1149
|
+
{
|
|
1150
|
+
description: "Update an Operational Registry Environment while preserving prior versions.",
|
|
1151
|
+
inputSchema: {
|
|
1152
|
+
workspaceId: workspaceIdSchema,
|
|
1153
|
+
id: z.string().min(1),
|
|
1154
|
+
name: z.string().min(1).optional(),
|
|
1155
|
+
summary: z.string().min(1).optional(),
|
|
1156
|
+
description: z.string().min(1).optional(),
|
|
1157
|
+
fields: recordFieldsSchema,
|
|
1158
|
+
unsetFields: z.array(fieldNameSchema).optional(),
|
|
1159
|
+
revisionNote: z.string().min(1).optional()
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
async (input) => jsonResult(await updateEnvironment(runtime, input, recordActorId))
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
registerDxcTool(
|
|
1166
|
+
"list_components",
|
|
1167
|
+
{
|
|
1168
|
+
description: "List Operational Registry Components that belong to one Environment.",
|
|
1169
|
+
inputSchema: {
|
|
1170
|
+
workspaceId: workspaceIdSchema,
|
|
1171
|
+
environmentId: z.string().min(1),
|
|
1172
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1173
|
+
includeArchived: z.boolean().optional()
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
async (input) => jsonResult(await listComponents(runtime, input))
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
registerDxcTool(
|
|
1180
|
+
"create_component",
|
|
1181
|
+
{
|
|
1182
|
+
description:
|
|
1183
|
+
"Create an Operational Registry Component for one Environment. Secret pointers are pointers only; do not paste credentials or secret values.",
|
|
1184
|
+
inputSchema: {
|
|
1185
|
+
workspaceId: workspaceIdSchema,
|
|
1186
|
+
name: z.string().min(1),
|
|
1187
|
+
environmentId: z.string().min(1),
|
|
1188
|
+
kind: z.string().min(1),
|
|
1189
|
+
locator: componentLocatorSchema,
|
|
1190
|
+
summary: z.string().min(1).optional(),
|
|
1191
|
+
identifiers: componentIdentifiersSchema,
|
|
1192
|
+
secretPointers: z.array(secretPointerSchema).optional(),
|
|
1193
|
+
notes: z.string().min(1).optional(),
|
|
1194
|
+
fields: recordFieldsSchema
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
async (input) => jsonResult(await createComponent(runtime, input, recordActorId))
|
|
1198
|
+
);
|
|
1199
|
+
|
|
1200
|
+
registerDxcTool(
|
|
1201
|
+
"update_component",
|
|
1202
|
+
{
|
|
1203
|
+
description:
|
|
1204
|
+
"Update an Operational Registry Component while preserving prior versions. Secret pointers are pointers only; do not paste credentials or secret values.",
|
|
1205
|
+
inputSchema: {
|
|
1206
|
+
workspaceId: workspaceIdSchema,
|
|
1207
|
+
id: z.string().min(1),
|
|
1208
|
+
name: z.string().min(1).optional(),
|
|
1209
|
+
kind: z.string().min(1).optional(),
|
|
1210
|
+
locator: componentLocatorSchema.optional(),
|
|
1211
|
+
summary: z.string().min(1).optional(),
|
|
1212
|
+
identifiers: componentIdentifiersSchema,
|
|
1213
|
+
secretPointers: z.array(secretPointerSchema).optional(),
|
|
1214
|
+
notes: z.string().min(1).optional(),
|
|
1215
|
+
fields: recordFieldsSchema,
|
|
1216
|
+
unsetFields: z.array(fieldNameSchema).optional(),
|
|
1217
|
+
revisionNote: z.string().min(1).optional()
|
|
1218
|
+
}
|
|
1219
|
+
},
|
|
1220
|
+
async (input) => jsonResult(await updateComponent(runtime, input, recordActorId))
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1223
|
+
registerDxcTool(
|
|
1224
|
+
"create_estimate",
|
|
1225
|
+
{
|
|
1226
|
+
description: "Create an itemized Engineer cost estimate for Weigh.",
|
|
1227
|
+
inputSchema: {
|
|
1228
|
+
workspaceId: workspaceIdSchema,
|
|
1229
|
+
title: z.string().min(1),
|
|
1230
|
+
summary: z.string().min(1).optional(),
|
|
1231
|
+
lineItems: z.array(estimateLineItemSchema).min(1),
|
|
1232
|
+
requirementIds: z.array(z.string().min(1)).optional(),
|
|
1233
|
+
expectationIds: z.array(z.string().min(1)).optional(),
|
|
1234
|
+
fields: recordFieldsSchema
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
async (input) => jsonResult(await createEstimate(runtime, input, recordActorId))
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
registerDxcTool(
|
|
1241
|
+
"update_estimate",
|
|
1242
|
+
{
|
|
1243
|
+
description: "Update an itemized Engineer cost estimate while preserving prior versions.",
|
|
1244
|
+
inputSchema: {
|
|
1245
|
+
workspaceId: workspaceIdSchema,
|
|
1246
|
+
id: z.string().min(1),
|
|
1247
|
+
title: z.string().min(1).optional(),
|
|
1248
|
+
summary: z.string().min(1).optional(),
|
|
1249
|
+
lineItems: z.array(estimateLineItemSchema).min(1).optional(),
|
|
1250
|
+
fields: recordFieldsSchema,
|
|
1251
|
+
unsetFields: z.array(fieldNameSchema).optional(),
|
|
1252
|
+
revisionNote: z.string().min(1).optional()
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
async (input) => jsonResult(await updateEstimate(runtime, input, recordActorId))
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
registerDxcTool(
|
|
1259
|
+
"create_benefits",
|
|
1260
|
+
{
|
|
1261
|
+
description: "Create an Owner-authored Benefits record for Weigh.",
|
|
1262
|
+
inputSchema: {
|
|
1263
|
+
workspaceId: workspaceIdSchema,
|
|
1264
|
+
title: z.string().min(1),
|
|
1265
|
+
summary: z.string().min(1).optional(),
|
|
1266
|
+
benefitItems: z.array(benefitItemSchema).min(1),
|
|
1267
|
+
requirementIds: z.array(z.string().min(1)).optional(),
|
|
1268
|
+
expectationIds: z.array(z.string().min(1)).optional(),
|
|
1269
|
+
fields: recordFieldsSchema
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
async (input) => jsonResult(await createBenefits(runtime, input, recordActorId))
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
registerDxcTool(
|
|
1276
|
+
"update_benefits",
|
|
1277
|
+
{
|
|
1278
|
+
description: "Update an Owner-authored Benefits record while preserving prior versions.",
|
|
1279
|
+
inputSchema: {
|
|
1280
|
+
workspaceId: workspaceIdSchema,
|
|
1281
|
+
id: z.string().min(1),
|
|
1282
|
+
title: z.string().min(1).optional(),
|
|
1283
|
+
summary: z.string().min(1).optional(),
|
|
1284
|
+
benefitItems: z.array(benefitItemSchema).min(1).optional(),
|
|
1285
|
+
fields: recordFieldsSchema,
|
|
1286
|
+
unsetFields: z.array(fieldNameSchema).optional(),
|
|
1287
|
+
revisionNote: z.string().min(1).optional()
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
async (input) => jsonResult(await updateBenefits(runtime, input, recordActorId))
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1293
|
+
registerDxcTool(
|
|
1294
|
+
"create_expectation",
|
|
1295
|
+
{
|
|
1296
|
+
description: "Create an expectation record.",
|
|
1297
|
+
inputSchema: {
|
|
1298
|
+
workspaceId: workspaceIdSchema,
|
|
1299
|
+
title: z.string().min(1),
|
|
1300
|
+
statement: z.string().min(1),
|
|
1301
|
+
successRecognition: z.string().min(1).optional(),
|
|
1302
|
+
approvalState: expectationApprovalStateSchema.optional(),
|
|
1303
|
+
approvedBy: z.string().min(1).optional(),
|
|
1304
|
+
approvedAt: z.string().min(1).optional(),
|
|
1305
|
+
source: z.string().min(1).optional(),
|
|
1306
|
+
statementId: z.string().min(1).optional(),
|
|
1307
|
+
fields: recordFieldsSchema
|
|
1308
|
+
}
|
|
1309
|
+
},
|
|
1310
|
+
async ({
|
|
1311
|
+
workspaceId,
|
|
1312
|
+
title,
|
|
1313
|
+
statement,
|
|
1314
|
+
successRecognition,
|
|
1315
|
+
approvalState,
|
|
1316
|
+
approvedBy,
|
|
1317
|
+
approvedAt,
|
|
1318
|
+
source,
|
|
1319
|
+
statementId,
|
|
1320
|
+
fields
|
|
1321
|
+
}) =>
|
|
1322
|
+
jsonResult(
|
|
1323
|
+
await createExpectation(
|
|
1324
|
+
runtime,
|
|
1325
|
+
{
|
|
1326
|
+
workspaceId,
|
|
1327
|
+
title,
|
|
1328
|
+
statement,
|
|
1329
|
+
successRecognition,
|
|
1330
|
+
approvalState,
|
|
1331
|
+
approvedBy,
|
|
1332
|
+
approvedAt,
|
|
1333
|
+
source,
|
|
1334
|
+
statementId,
|
|
1335
|
+
fields
|
|
1336
|
+
},
|
|
1337
|
+
recordActorId
|
|
1338
|
+
)
|
|
1339
|
+
)
|
|
1340
|
+
);
|
|
1341
|
+
|
|
1342
|
+
registerDxcTool(
|
|
1343
|
+
"create_requirement",
|
|
1344
|
+
{
|
|
1345
|
+
description: "Create a requirement record.",
|
|
1346
|
+
inputSchema: {
|
|
1347
|
+
workspaceId: workspaceIdSchema,
|
|
1348
|
+
title: z.string().min(1),
|
|
1349
|
+
expectationId: z.string().min(1).optional(),
|
|
1350
|
+
statement: z.string().min(1),
|
|
1351
|
+
acceptanceCriteria: z.array(z.string().min(1)).optional(),
|
|
1352
|
+
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
1353
|
+
status: requirementStatusSchema.optional(),
|
|
1354
|
+
fields: recordFieldsSchema
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
async ({ workspaceId, title, expectationId, statement, acceptanceCriteria, priority, status, fields }) =>
|
|
1358
|
+
jsonResult(
|
|
1359
|
+
await createRequirement(
|
|
1360
|
+
runtime,
|
|
1361
|
+
{ workspaceId, title, expectationId, statement, acceptanceCriteria, priority, status, fields },
|
|
1362
|
+
recordActorId
|
|
1363
|
+
)
|
|
1364
|
+
)
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
registerDxcTool(
|
|
1368
|
+
"create_task",
|
|
1369
|
+
{
|
|
1370
|
+
description:
|
|
1371
|
+
"Create an execution task ledger. Current status is derived from the latest status_change entry.",
|
|
1372
|
+
inputSchema: {
|
|
1373
|
+
workspaceId: workspaceIdSchema,
|
|
1374
|
+
title: z.string().min(1),
|
|
1375
|
+
description: z.string().min(1),
|
|
1376
|
+
assignee: z.string().min(1).optional(),
|
|
1377
|
+
assignor: z.string().min(1).optional(),
|
|
1378
|
+
requirementId: z.string().min(1).optional(),
|
|
1379
|
+
initialStatus: taskStatusSchema.optional(),
|
|
1380
|
+
fields: recordFieldsSchema
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
async (input) => jsonResult(await createTask(runtime, input, recordActorId))
|
|
1384
|
+
);
|
|
1385
|
+
|
|
1386
|
+
registerDxcTool(
|
|
1387
|
+
"append_task_entry",
|
|
1388
|
+
{
|
|
1389
|
+
description:
|
|
1390
|
+
"Append an immutable entry to a Task. Use status_change entries to change the derived current status.",
|
|
1391
|
+
inputSchema: {
|
|
1392
|
+
workspaceId: workspaceIdSchema,
|
|
1393
|
+
taskId: z.string().min(1),
|
|
1394
|
+
entryType: taskEntryTypeSchema,
|
|
1395
|
+
body: z.string().min(1),
|
|
1396
|
+
status: taskStatusSchema.optional()
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
async (input) => jsonResult(await appendTaskEntry(runtime.db, input, recordActorId))
|
|
1400
|
+
);
|
|
1401
|
+
|
|
1402
|
+
registerDxcTool(
|
|
1403
|
+
"create_commitment",
|
|
1404
|
+
{
|
|
1405
|
+
description: "Create an Owner commitment record that moves requirements or expectations toward Build.",
|
|
1406
|
+
inputSchema: {
|
|
1407
|
+
workspaceId: workspaceIdSchema,
|
|
1408
|
+
title: z.string().min(1),
|
|
1409
|
+
summary: z.string().min(1).optional(),
|
|
1410
|
+
commitmentStatement: z.string().min(1),
|
|
1411
|
+
requirementIds: z.array(z.string().min(1)).optional(),
|
|
1412
|
+
expectationIds: z.array(z.string().min(1)).optional(),
|
|
1413
|
+
reservationNotes: z.array(z.string().min(1)).optional(),
|
|
1414
|
+
deferralId: z.string().min(1).optional(),
|
|
1415
|
+
fields: recordFieldsSchema
|
|
1416
|
+
}
|
|
1417
|
+
},
|
|
1418
|
+
async (input) => jsonResult(await createCommitment(runtime, input, recordActorId))
|
|
1419
|
+
);
|
|
1420
|
+
|
|
1421
|
+
registerDxcTool(
|
|
1422
|
+
"create_deferral",
|
|
1423
|
+
{
|
|
1424
|
+
description: "Create an Owner deferral record with explicit conditions required for a future Commitment.",
|
|
1425
|
+
inputSchema: {
|
|
1426
|
+
workspaceId: workspaceIdSchema,
|
|
1427
|
+
title: z.string().min(1),
|
|
1428
|
+
summary: z.string().min(1).optional(),
|
|
1429
|
+
reason: z.string().min(1),
|
|
1430
|
+
conditions: z.array(z.string().min(1)).min(1),
|
|
1431
|
+
requirementIds: z.array(z.string().min(1)).optional(),
|
|
1432
|
+
expectationIds: z.array(z.string().min(1)).optional(),
|
|
1433
|
+
fields: recordFieldsSchema
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
async (input) => jsonResult(await createDeferral(runtime, input, recordActorId))
|
|
1437
|
+
);
|
|
1438
|
+
|
|
1439
|
+
registerDxcTool(
|
|
1440
|
+
"append_deferral_event",
|
|
1441
|
+
{
|
|
1442
|
+
description:
|
|
1443
|
+
"Append an immutable event to a Deferral record. Use this for condition updates, notes, resolution into Commitment, or abandonment.",
|
|
1444
|
+
inputSchema: {
|
|
1445
|
+
workspaceId: workspaceIdSchema,
|
|
1446
|
+
deferralId: z.string().min(1),
|
|
1447
|
+
eventType: deferralEventTypeSchema,
|
|
1448
|
+
conditionId: z.string().min(1).optional(),
|
|
1449
|
+
note: z.string().min(1).optional(),
|
|
1450
|
+
summary: z.string().min(1).optional(),
|
|
1451
|
+
reason: z.string().min(1).optional(),
|
|
1452
|
+
commitmentId: z.string().min(1).optional()
|
|
1453
|
+
}
|
|
1454
|
+
},
|
|
1455
|
+
async (input) => jsonResult(await appendDeferralEventForTool(runtime, input, recordActorId))
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
registerDxcTool(
|
|
1459
|
+
"create_change",
|
|
1460
|
+
{
|
|
1461
|
+
description:
|
|
1462
|
+
"Create a service change record with baseline plan, execution, rollback, and risk/impact sections.",
|
|
1463
|
+
inputSchema: {
|
|
1464
|
+
workspaceId: workspaceIdSchema,
|
|
1465
|
+
title: z.string().min(1),
|
|
1466
|
+
summary: z.string().min(1).optional(),
|
|
1467
|
+
changePlan: z.string().min(1),
|
|
1468
|
+
executionSteps: z.array(z.string().min(1)).min(1),
|
|
1469
|
+
rollbackPlan: z.string().min(1),
|
|
1470
|
+
riskImpact: z.string().min(1),
|
|
1471
|
+
plannedFor: z.string().min(1).optional(),
|
|
1472
|
+
requirementId: z.string().min(1).optional(),
|
|
1473
|
+
fields: recordFieldsSchema
|
|
1474
|
+
}
|
|
1475
|
+
},
|
|
1476
|
+
async (input) => jsonResult(await createChange(runtime, input, recordActorId))
|
|
1477
|
+
);
|
|
1478
|
+
|
|
1479
|
+
registerDxcTool(
|
|
1480
|
+
"append_change_event",
|
|
1481
|
+
{
|
|
1482
|
+
description:
|
|
1483
|
+
"Append an immutable event to a Change record. Use this for notice, veto, emergency posture, decision, result, recovery, plan revisions, and notes.",
|
|
1484
|
+
inputSchema: {
|
|
1485
|
+
workspaceId: workspaceIdSchema,
|
|
1486
|
+
changeId: z.string().min(1),
|
|
1487
|
+
eventType: changeEventTypeSchema,
|
|
1488
|
+
notice: z.string().min(1).optional(),
|
|
1489
|
+
noticeTo: z.array(z.string().min(1)).optional(),
|
|
1490
|
+
effectiveAt: z.string().min(1).optional(),
|
|
1491
|
+
vetoByRole: changeVetoRoleSchema.optional(),
|
|
1492
|
+
reason: z.string().min(1).optional(),
|
|
1493
|
+
importance: z.string().min(1).optional(),
|
|
1494
|
+
immediacy: z.string().min(1).optional(),
|
|
1495
|
+
decision: changeDecisionSchema.optional(),
|
|
1496
|
+
result: changeResultSchema.optional(),
|
|
1497
|
+
summary: z.string().min(1).optional(),
|
|
1498
|
+
note: z.string().min(1).optional(),
|
|
1499
|
+
revisedChangePlan: z.string().min(1).optional(),
|
|
1500
|
+
revisedExecutionSteps: z.array(z.string().min(1)).optional(),
|
|
1501
|
+
revisedRollbackPlan: z.string().min(1).optional(),
|
|
1502
|
+
revisedRiskImpact: z.string().min(1).optional(),
|
|
1503
|
+
revisedPlannedFor: z.string().min(1).optional()
|
|
1504
|
+
}
|
|
1505
|
+
},
|
|
1506
|
+
async (input) =>
|
|
1507
|
+
jsonResult(
|
|
1508
|
+
await appendChangeEvent(
|
|
1509
|
+
runtime.db,
|
|
1510
|
+
{
|
|
1511
|
+
workspaceId: input.workspaceId,
|
|
1512
|
+
changeId: input.changeId,
|
|
1513
|
+
eventType: input.eventType,
|
|
1514
|
+
event: buildChangeEventContent(input)
|
|
1515
|
+
},
|
|
1516
|
+
recordActorId
|
|
1517
|
+
)
|
|
1518
|
+
)
|
|
1519
|
+
);
|
|
1520
|
+
|
|
1521
|
+
registerDxcTool(
|
|
1522
|
+
"create_decision",
|
|
1523
|
+
{
|
|
1524
|
+
description:
|
|
1525
|
+
"Create a revisitable decision ledger. Current decision is derived from the latest decision entry.",
|
|
1526
|
+
inputSchema: {
|
|
1527
|
+
workspaceId: workspaceIdSchema,
|
|
1528
|
+
title: z.string().min(1),
|
|
1529
|
+
matter: z.string().min(1),
|
|
1530
|
+
initialEntries: z.array(decisionInitialEntrySchema).optional(),
|
|
1531
|
+
initialDecision: initialDecisionSchema.optional(),
|
|
1532
|
+
fields: recordFieldsSchema
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
async ({ workspaceId, title, matter, initialEntries, initialDecision, fields }) => {
|
|
1536
|
+
assertNoTypedFields(fields, "create_decision", DECISION_TYPED_FIELDS);
|
|
1537
|
+
assertNoDecisionInputFields(fields);
|
|
1538
|
+
|
|
1539
|
+
return jsonResult(await createDecision(runtime, { workspaceId, title, matter, initialEntries, initialDecision, fields }, recordActorId));
|
|
1540
|
+
}
|
|
1541
|
+
);
|
|
1542
|
+
|
|
1543
|
+
registerDxcTool(
|
|
1544
|
+
"append_decision_entry",
|
|
1545
|
+
{
|
|
1546
|
+
description:
|
|
1547
|
+
"Append an immutable entry to a Decision. Use decision entries to change the derived current decision.",
|
|
1548
|
+
inputSchema: {
|
|
1549
|
+
workspaceId: workspaceIdSchema,
|
|
1550
|
+
decisionId: z.string().min(1),
|
|
1551
|
+
entryType: decisionEntryTypeSchema,
|
|
1552
|
+
body: z.string().min(1),
|
|
1553
|
+
decidedBy: z.string().min(1).optional(),
|
|
1554
|
+
rationale: z.string().min(1).optional()
|
|
1555
|
+
}
|
|
1556
|
+
},
|
|
1557
|
+
async (input) => jsonResult(await appendDecisionEntry(runtime.db, input, recordActorId))
|
|
1558
|
+
);
|
|
1559
|
+
|
|
1560
|
+
registerDxcTool(
|
|
1561
|
+
"create_risk",
|
|
1562
|
+
{
|
|
1563
|
+
description: "Create a risk record.",
|
|
1564
|
+
inputSchema: {
|
|
1565
|
+
workspaceId: workspaceIdSchema,
|
|
1566
|
+
title: z.string().min(1),
|
|
1567
|
+
summary: z.string().min(1).optional(),
|
|
1568
|
+
likelihood: z.enum(["low", "medium", "high"]).optional(),
|
|
1569
|
+
impact: z.enum(["low", "medium", "high"]).optional(),
|
|
1570
|
+
mitigation: z.string().min(1).optional(),
|
|
1571
|
+
fields: recordFieldsSchema
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
async ({ workspaceId, title, summary, likelihood, impact, mitigation, fields }) =>
|
|
1575
|
+
jsonResult(
|
|
1576
|
+
await createRecord(
|
|
1577
|
+
runtime.db,
|
|
1578
|
+
"risks",
|
|
1579
|
+
{
|
|
1580
|
+
workspaceId,
|
|
1581
|
+
title,
|
|
1582
|
+
summary,
|
|
1583
|
+
fields: {
|
|
1584
|
+
...(likelihood ? { likelihood } : {}),
|
|
1585
|
+
...(impact ? { impact } : {}),
|
|
1586
|
+
...(mitigation ? { mitigation } : {}),
|
|
1587
|
+
...(fields ?? {})
|
|
1588
|
+
}
|
|
1589
|
+
},
|
|
1590
|
+
recordActorId
|
|
1591
|
+
)
|
|
1592
|
+
)
|
|
1593
|
+
);
|
|
1594
|
+
|
|
1595
|
+
registerDxcTool(
|
|
1596
|
+
"list_records",
|
|
1597
|
+
{
|
|
1598
|
+
description: "List records from a DX Complete collection.",
|
|
1599
|
+
inputSchema: {
|
|
1600
|
+
recordType: activeCollectionSchema,
|
|
1601
|
+
workspaceId: workspaceIdSchema.optional(),
|
|
1602
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
1603
|
+
includeArchived: z.boolean().optional()
|
|
1604
|
+
}
|
|
1605
|
+
},
|
|
1606
|
+
async ({ recordType, workspaceId, limit, includeArchived }) =>
|
|
1607
|
+
jsonResult(await listRecords(runtime.db, recordType, limit ?? 20, { workspaceId, includeArchived }))
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
registerDxcTool(
|
|
1611
|
+
"list_linked_records",
|
|
1612
|
+
{
|
|
1613
|
+
description: "List records linked to or from a DX Complete record.",
|
|
1614
|
+
inputSchema: {
|
|
1615
|
+
recordType: activeCollectionSchema,
|
|
1616
|
+
workspaceId: workspaceIdSchema.optional(),
|
|
1617
|
+
id: z.string().min(1),
|
|
1618
|
+
direction: z.enum(["outbound", "inbound", "both"]).optional(),
|
|
1619
|
+
relationship: z.string().min(1).optional(),
|
|
1620
|
+
includeArchived: z.boolean().optional()
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
async (input) => jsonResult(await listLinkedRecords(runtime.db, input))
|
|
1624
|
+
);
|
|
1625
|
+
|
|
1626
|
+
registerDxcTool(
|
|
1627
|
+
"link_decision_input",
|
|
1628
|
+
{
|
|
1629
|
+
description: "Link a Decision to one record that informed it using the informed_by relationship.",
|
|
1630
|
+
inputSchema: {
|
|
1631
|
+
workspaceId: workspaceIdSchema,
|
|
1632
|
+
decisionId: z.string().min(1),
|
|
1633
|
+
inputRecordType: decisionInputRecordTypeSchema,
|
|
1634
|
+
inputId: z.string().min(1)
|
|
1635
|
+
}
|
|
1636
|
+
},
|
|
1637
|
+
async (input) => jsonResult(await linkDecisionInput(runtime, input, recordActorId))
|
|
1638
|
+
);
|
|
1639
|
+
|
|
1640
|
+
registerDxcTool(
|
|
1641
|
+
"update_record",
|
|
1642
|
+
{
|
|
1643
|
+
description: "Update a DX Complete record and optionally clear top-level fields.",
|
|
1644
|
+
inputSchema: {
|
|
1645
|
+
recordType: activeCollectionSchema,
|
|
1646
|
+
workspaceId: workspaceIdSchema.optional(),
|
|
1647
|
+
id: z.string().min(1),
|
|
1648
|
+
title: z.string().min(1).optional(),
|
|
1649
|
+
summary: z.string().min(1).optional(),
|
|
1650
|
+
fields: recordFieldsSchema,
|
|
1651
|
+
unsetFields: z.array(fieldNameSchema).optional()
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
async (input) => jsonResult(await updateRecord(runtime.db, input, recordActorId))
|
|
1655
|
+
);
|
|
1656
|
+
|
|
1657
|
+
registerDxcTool(
|
|
1658
|
+
"update_expectation",
|
|
1659
|
+
{
|
|
1660
|
+
description: "Update an expectation record with typed expectation fields.",
|
|
1661
|
+
inputSchema: {
|
|
1662
|
+
workspaceId: workspaceIdSchema,
|
|
1663
|
+
id: z.string().min(1),
|
|
1664
|
+
title: z.string().min(1).optional(),
|
|
1665
|
+
statement: z.string().min(1).optional(),
|
|
1666
|
+
successRecognition: z.string().min(1).optional(),
|
|
1667
|
+
approvalState: expectationApprovalStateSchema.optional(),
|
|
1668
|
+
approvedBy: z.string().min(1).optional(),
|
|
1669
|
+
approvedAt: z.string().min(1).optional(),
|
|
1670
|
+
source: z.string().min(1).optional(),
|
|
1671
|
+
revisionNote: z.string().min(1).optional(),
|
|
1672
|
+
fields: recordFieldsSchema,
|
|
1673
|
+
unsetFields: z.array(fieldNameSchema).optional()
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
async (input) => jsonResult(await updateExpectation(runtime, input, recordActorId))
|
|
1677
|
+
);
|
|
1678
|
+
|
|
1679
|
+
registerDxcTool(
|
|
1680
|
+
"update_requirement",
|
|
1681
|
+
{
|
|
1682
|
+
description: "Update a requirement record with typed requirement fields.",
|
|
1683
|
+
inputSchema: {
|
|
1684
|
+
workspaceId: workspaceIdSchema,
|
|
1685
|
+
id: z.string().min(1),
|
|
1686
|
+
title: z.string().min(1).optional(),
|
|
1687
|
+
statement: z.string().min(1).optional(),
|
|
1688
|
+
acceptanceCriteria: z.array(z.string().min(1)).optional(),
|
|
1689
|
+
priority: z.enum(["low", "medium", "high"]).optional(),
|
|
1690
|
+
status: requirementStatusSchema.optional(),
|
|
1691
|
+
revisionNote: z.string().min(1).optional(),
|
|
1692
|
+
fields: recordFieldsSchema,
|
|
1693
|
+
unsetFields: z.array(fieldNameSchema).optional()
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
async (input) => jsonResult(await updateRequirement(runtime, input, recordActorId))
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
registerDxcTool(
|
|
1700
|
+
"append_review_note",
|
|
1701
|
+
{
|
|
1702
|
+
description:
|
|
1703
|
+
"Append a non-blocking review note to an expectation or requirement. The optional important flag only surfaces the note; it does not create an approval duty or workflow block.",
|
|
1704
|
+
inputSchema: {
|
|
1705
|
+
workspaceId: workspaceIdSchema,
|
|
1706
|
+
recordType: reviewableRecordTypeSchema,
|
|
1707
|
+
id: z.string().min(1),
|
|
1708
|
+
body: z.string().min(1),
|
|
1709
|
+
important: z.boolean().optional()
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
async (input) => jsonResult(await appendReviewNote(runtime.db, input, recordActorId))
|
|
1713
|
+
);
|
|
1714
|
+
|
|
1715
|
+
registerDxcTool(
|
|
1716
|
+
"archive_record",
|
|
1717
|
+
{
|
|
1718
|
+
description: "Archive a DX Complete record without deleting it.",
|
|
1719
|
+
inputSchema: {
|
|
1720
|
+
recordType: activeCollectionSchema,
|
|
1721
|
+
workspaceId: workspaceIdSchema.optional(),
|
|
1722
|
+
id: z.string().min(1),
|
|
1723
|
+
reason: z.string().min(1).optional(),
|
|
1724
|
+
supersededByType: activeCollectionSchema.optional(),
|
|
1725
|
+
supersededById: z.string().min(1).optional()
|
|
1726
|
+
}
|
|
1727
|
+
},
|
|
1728
|
+
async (input) => jsonResult(await archiveRecord(runtime.db, input, recordActorId))
|
|
1729
|
+
);
|
|
1730
|
+
|
|
1731
|
+
registerDxcTool(
|
|
1732
|
+
"link_records",
|
|
1733
|
+
{
|
|
1734
|
+
description: "Link one DX Complete record to another.",
|
|
1735
|
+
inputSchema: {
|
|
1736
|
+
workspaceId: workspaceIdSchema,
|
|
1737
|
+
fromType: activeCollectionSchema,
|
|
1738
|
+
fromId: z.string().min(1),
|
|
1739
|
+
toType: activeCollectionSchema,
|
|
1740
|
+
toId: z.string().min(1),
|
|
1741
|
+
relationship: z.string().min(1).optional()
|
|
1742
|
+
}
|
|
1743
|
+
},
|
|
1744
|
+
async (input) => jsonResult(await linkRecords(runtime.db, input, recordActorId))
|
|
1745
|
+
);
|
|
1746
|
+
|
|
1747
|
+
registerDxcTool(
|
|
1748
|
+
"unlink_records",
|
|
1749
|
+
{
|
|
1750
|
+
description: "Remove one relationship link from a DX Complete record to another record.",
|
|
1751
|
+
inputSchema: {
|
|
1752
|
+
workspaceId: workspaceIdSchema,
|
|
1753
|
+
fromType: activeCollectionSchema,
|
|
1754
|
+
fromId: z.string().min(1),
|
|
1755
|
+
toType: activeCollectionSchema,
|
|
1756
|
+
toId: z.string().min(1),
|
|
1757
|
+
relationship: z.string().min(1).optional()
|
|
1758
|
+
}
|
|
1759
|
+
},
|
|
1760
|
+
async (input) => jsonResult(await unlinkRecords(runtime.db, input, recordActorId))
|
|
1761
|
+
);
|
|
1762
|
+
|
|
1763
|
+
return server;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function omitWorkspaceId(schema: ToolSchema): ToolSchema {
|
|
1767
|
+
const result: ToolSchema = {};
|
|
1768
|
+
|
|
1769
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
1770
|
+
if (key !== "workspaceId") {
|
|
1771
|
+
result[key] = value;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
return result;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function injectHostedWorkspace(
|
|
1779
|
+
input: Record<string, unknown>,
|
|
1780
|
+
originalSchema: ToolSchema,
|
|
1781
|
+
workspaceId: string
|
|
1782
|
+
): Record<string, unknown> {
|
|
1783
|
+
if (!Object.hasOwn(originalSchema, "workspaceId")) {
|
|
1784
|
+
return input;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
return {
|
|
1788
|
+
...input,
|
|
1789
|
+
workspaceId
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
function assertHostedWorkspaceAccess(
|
|
1794
|
+
toolName: string,
|
|
1795
|
+
input: Record<string, unknown>,
|
|
1796
|
+
workspaceId: string | undefined
|
|
1797
|
+
): void {
|
|
1798
|
+
if (!workspaceId) {
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
for (const key of ["recordType", "fromType", "toType", "supersededByType"]) {
|
|
1803
|
+
if (input[key] === "workspaces") {
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
`${toolName} cannot operate on workspace records through a hosted workspace MCP endpoint.`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
if (typeof input.workspaceId === "string" && input.workspaceId !== workspaceId) {
|
|
1811
|
+
throw new Error("Hosted MCP workspace scope mismatch.");
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function assertToolRoleAccess(toolName: string, roles: WorkspaceRole[] | undefined, input: Record<string, unknown>): void {
|
|
1816
|
+
if (!roles) {
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
if (roles.includes("owner") || memberAllowedTools.has(toolName)) {
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
if (roles.includes("engineer") && engineerAllowedTools.has(toolName)) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
if (roles.includes("operator") && operatorAllowedTools.has(toolName)) {
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
if (
|
|
1833
|
+
roles.includes("operator") &&
|
|
1834
|
+
toolName === "archive_record" &&
|
|
1835
|
+
(input.recordType === "environments" || input.recordType === "components")
|
|
1836
|
+
) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
throw new Error(`Workspace role access denied for ${toolName}.`);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const memberAllowedTools = new Set([
|
|
1844
|
+
"runtime_status",
|
|
1845
|
+
"get_process_guide",
|
|
1846
|
+
"get_doc",
|
|
1847
|
+
"get_record",
|
|
1848
|
+
"list_records",
|
|
1849
|
+
"list_linked_records",
|
|
1850
|
+
"list_environments",
|
|
1851
|
+
"list_components",
|
|
1852
|
+
"create_dxcomplete_ticket",
|
|
1853
|
+
"append_dxcomplete_ticket",
|
|
1854
|
+
"list_dxcomplete_tickets",
|
|
1855
|
+
"list_unread_dxcomplete_ticket_replies",
|
|
1856
|
+
"read_dxcomplete_ticket",
|
|
1857
|
+
"archive_dxcomplete_ticket",
|
|
1858
|
+
"append_journal_note",
|
|
1859
|
+
"read_journal",
|
|
1860
|
+
"get_journal_entry",
|
|
1861
|
+
"append_journal_summary"
|
|
1862
|
+
]);
|
|
1863
|
+
|
|
1864
|
+
const engineerAllowedTools = new Set([
|
|
1865
|
+
"create_estimate",
|
|
1866
|
+
"update_estimate",
|
|
1867
|
+
"create_task",
|
|
1868
|
+
"append_task_entry",
|
|
1869
|
+
"append_review_note"
|
|
1870
|
+
]);
|
|
1871
|
+
|
|
1872
|
+
const operatorAllowedTools = new Set([
|
|
1873
|
+
"create_change",
|
|
1874
|
+
"append_change_event",
|
|
1875
|
+
"create_environment",
|
|
1876
|
+
"update_environment",
|
|
1877
|
+
"create_component",
|
|
1878
|
+
"update_component"
|
|
1879
|
+
]);
|
|
1880
|
+
|
|
1881
|
+
function jsonResult(value: unknown) {
|
|
1882
|
+
return {
|
|
1883
|
+
content: [
|
|
1884
|
+
{
|
|
1885
|
+
type: "text" as const,
|
|
1886
|
+
text: JSON.stringify(value, null, 2)
|
|
1887
|
+
}
|
|
1888
|
+
]
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function getInputSchemaFingerprint(inputSchema: ToolSchema): string {
|
|
1893
|
+
return createHash("sha256")
|
|
1894
|
+
.update(stableStringify(z.toJSONSchema(z.object(inputSchema))))
|
|
1895
|
+
.digest("hex")
|
|
1896
|
+
.slice(0, 16);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function stableStringify(value: unknown): string {
|
|
1900
|
+
return JSON.stringify(sortJsonValue(value));
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function sortJsonValue(value: unknown): unknown {
|
|
1904
|
+
if (Array.isArray(value)) {
|
|
1905
|
+
return value.map(sortJsonValue);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
if (value && typeof value === "object") {
|
|
1909
|
+
const input = value as Record<string, unknown>;
|
|
1910
|
+
const output: Record<string, unknown> = {};
|
|
1911
|
+
|
|
1912
|
+
for (const key of Object.keys(input).sort()) {
|
|
1913
|
+
output[key] = sortJsonValue(input[key]);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
return output;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
return value;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
|
|
1923
|
+
async function createStatement(
|
|
1924
|
+
runtime: DxRuntime,
|
|
1925
|
+
input: {
|
|
1926
|
+
workspaceId: string;
|
|
1927
|
+
title: string;
|
|
1928
|
+
statement: string;
|
|
1929
|
+
source?: string;
|
|
1930
|
+
fields?: Record<string, unknown>;
|
|
1931
|
+
},
|
|
1932
|
+
actorId: string
|
|
1933
|
+
) {
|
|
1934
|
+
assertNoTypedFields(input.fields, "create_statement", STATEMENT_TYPED_FIELDS);
|
|
1935
|
+
|
|
1936
|
+
return createRecord(
|
|
1937
|
+
runtime.db,
|
|
1938
|
+
"statements",
|
|
1939
|
+
{
|
|
1940
|
+
workspaceId: input.workspaceId,
|
|
1941
|
+
title: input.title,
|
|
1942
|
+
summary: input.statement,
|
|
1943
|
+
fields: {
|
|
1944
|
+
...(input.fields ?? {}),
|
|
1945
|
+
statement: input.statement,
|
|
1946
|
+
...(input.source !== undefined ? { source: input.source } : {})
|
|
1947
|
+
}
|
|
1948
|
+
},
|
|
1949
|
+
actorId
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
async function updateStatement(
|
|
1954
|
+
runtime: DxRuntime,
|
|
1955
|
+
input: {
|
|
1956
|
+
workspaceId: string;
|
|
1957
|
+
id: string;
|
|
1958
|
+
title?: string;
|
|
1959
|
+
statement?: string;
|
|
1960
|
+
source?: string;
|
|
1961
|
+
fields?: Record<string, unknown>;
|
|
1962
|
+
unsetFields?: string[];
|
|
1963
|
+
revisionNote?: string;
|
|
1964
|
+
},
|
|
1965
|
+
actorId: string
|
|
1966
|
+
) {
|
|
1967
|
+
assertNoTypedFields(input.fields, "update_statement", STATEMENT_TYPED_FIELDS);
|
|
1968
|
+
|
|
1969
|
+
return updateRecord(
|
|
1970
|
+
runtime.db,
|
|
1971
|
+
{
|
|
1972
|
+
recordType: "statements",
|
|
1973
|
+
workspaceId: input.workspaceId,
|
|
1974
|
+
id: input.id,
|
|
1975
|
+
title: input.title,
|
|
1976
|
+
summary: input.statement,
|
|
1977
|
+
fields: {
|
|
1978
|
+
...(input.fields ?? {}),
|
|
1979
|
+
...(input.statement !== undefined ? { statement: input.statement } : {}),
|
|
1980
|
+
...(input.source !== undefined ? { source: input.source } : {})
|
|
1981
|
+
},
|
|
1982
|
+
unsetFields: input.unsetFields,
|
|
1983
|
+
allowManagedFields: true,
|
|
1984
|
+
revisionNote: input.revisionNote
|
|
1985
|
+
},
|
|
1986
|
+
actorId
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
async function createEnvironment(
|
|
1991
|
+
runtime: DxRuntime,
|
|
1992
|
+
input: {
|
|
1993
|
+
workspaceId: string;
|
|
1994
|
+
name: string;
|
|
1995
|
+
summary?: string;
|
|
1996
|
+
description?: string;
|
|
1997
|
+
fields?: Record<string, unknown>;
|
|
1998
|
+
},
|
|
1999
|
+
actorId: string
|
|
2000
|
+
) {
|
|
2001
|
+
assertNoTypedFields(input.fields, "create_environment", ENVIRONMENT_TYPED_FIELDS);
|
|
2002
|
+
|
|
2003
|
+
return createRecord(
|
|
2004
|
+
runtime.db,
|
|
2005
|
+
"environments",
|
|
2006
|
+
{
|
|
2007
|
+
workspaceId: input.workspaceId,
|
|
2008
|
+
title: input.name,
|
|
2009
|
+
summary: input.summary,
|
|
2010
|
+
allowManagedFields: true,
|
|
2011
|
+
fields: {
|
|
2012
|
+
...(input.fields ?? {}),
|
|
2013
|
+
name: input.name,
|
|
2014
|
+
...(input.description !== undefined ? { description: input.description } : {})
|
|
2015
|
+
}
|
|
2016
|
+
},
|
|
2017
|
+
actorId
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
async function updateEnvironment(
|
|
2022
|
+
runtime: DxRuntime,
|
|
2023
|
+
input: {
|
|
2024
|
+
workspaceId: string;
|
|
2025
|
+
id: string;
|
|
2026
|
+
name?: string;
|
|
2027
|
+
summary?: string;
|
|
2028
|
+
description?: string;
|
|
2029
|
+
fields?: Record<string, unknown>;
|
|
2030
|
+
unsetFields?: string[];
|
|
2031
|
+
revisionNote?: string;
|
|
2032
|
+
},
|
|
2033
|
+
actorId: string
|
|
2034
|
+
) {
|
|
2035
|
+
assertNoTypedFields(input.fields, "update_environment", ENVIRONMENT_TYPED_FIELDS);
|
|
2036
|
+
assertNoTypedUnsetFields(input.unsetFields, "update_environment", ENVIRONMENT_TYPED_FIELDS);
|
|
2037
|
+
|
|
2038
|
+
return updateRecord(
|
|
2039
|
+
runtime.db,
|
|
2040
|
+
{
|
|
2041
|
+
recordType: "environments",
|
|
2042
|
+
workspaceId: input.workspaceId,
|
|
2043
|
+
id: input.id,
|
|
2044
|
+
title: input.name,
|
|
2045
|
+
summary: input.summary,
|
|
2046
|
+
fields: {
|
|
2047
|
+
...(input.fields ?? {}),
|
|
2048
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
2049
|
+
...(input.description !== undefined ? { description: input.description } : {})
|
|
2050
|
+
},
|
|
2051
|
+
unsetFields: input.unsetFields,
|
|
2052
|
+
allowManagedFields: true,
|
|
2053
|
+
revisionNote: input.revisionNote
|
|
2054
|
+
},
|
|
2055
|
+
actorId
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
async function listComponents(
|
|
2060
|
+
runtime: DxRuntime,
|
|
2061
|
+
input: {
|
|
2062
|
+
workspaceId: string;
|
|
2063
|
+
environmentId: string;
|
|
2064
|
+
limit?: number;
|
|
2065
|
+
includeArchived?: boolean;
|
|
2066
|
+
}
|
|
2067
|
+
) {
|
|
2068
|
+
const environment = await getRecord(runtime.db, "environments", input.environmentId, {
|
|
2069
|
+
workspaceId: input.workspaceId
|
|
2070
|
+
});
|
|
2071
|
+
if (!environment) {
|
|
2072
|
+
throw new Error(`Environment not found: environments/${input.environmentId}`);
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
const filter: Record<string, unknown> = {
|
|
2076
|
+
workspaceId: input.workspaceId,
|
|
2077
|
+
"fields.environmentId": environment._id
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
if (!input.includeArchived) {
|
|
2081
|
+
filter.archivedAt = { $exists: false };
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
return runtime.db
|
|
2085
|
+
.collection("components")
|
|
2086
|
+
.find(filter)
|
|
2087
|
+
.sort({ createdAt: 1, _id: 1 })
|
|
2088
|
+
.limit(input.limit ?? 20)
|
|
2089
|
+
.toArray();
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
async function createComponent(
|
|
2093
|
+
runtime: DxRuntime,
|
|
2094
|
+
input: {
|
|
2095
|
+
workspaceId: string;
|
|
2096
|
+
name: string;
|
|
2097
|
+
environmentId: string;
|
|
2098
|
+
kind: string;
|
|
2099
|
+
locator: ComponentLocatorInput;
|
|
2100
|
+
summary?: string;
|
|
2101
|
+
identifiers?: ComponentIdentifiersInput;
|
|
2102
|
+
secretPointers?: SecretPointerInput[];
|
|
2103
|
+
notes?: string;
|
|
2104
|
+
fields?: Record<string, unknown>;
|
|
2105
|
+
},
|
|
2106
|
+
actorId: string
|
|
2107
|
+
) {
|
|
2108
|
+
assertNoReservedFields(input.fields, ["workspaceId", "environmentId"]);
|
|
2109
|
+
assertNoTypedFields(input.fields, "create_component", COMPONENT_TYPED_FIELDS);
|
|
2110
|
+
|
|
2111
|
+
const environment = await getRecord(runtime.db, "environments", input.environmentId, {
|
|
2112
|
+
workspaceId: input.workspaceId
|
|
2113
|
+
});
|
|
2114
|
+
if (!environment) {
|
|
2115
|
+
throw new Error(`Environment not found: environments/${input.environmentId}`);
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
const component = await createRecord(
|
|
2119
|
+
runtime.db,
|
|
2120
|
+
"components",
|
|
2121
|
+
{
|
|
2122
|
+
workspaceId: input.workspaceId,
|
|
2123
|
+
title: input.name,
|
|
2124
|
+
summary: input.summary,
|
|
2125
|
+
allowManagedFields: true,
|
|
2126
|
+
fields: {
|
|
2127
|
+
...(input.fields ?? {}),
|
|
2128
|
+
name: input.name,
|
|
2129
|
+
environmentId: environment._id,
|
|
2130
|
+
kind: input.kind,
|
|
2131
|
+
locator: input.locator,
|
|
2132
|
+
...(input.identifiers !== undefined ? { identifiers: input.identifiers } : {}),
|
|
2133
|
+
...(input.secretPointers !== undefined ? { secretPointers: input.secretPointers } : {}),
|
|
2134
|
+
...(input.notes !== undefined ? { notes: input.notes } : {})
|
|
2135
|
+
}
|
|
2136
|
+
},
|
|
2137
|
+
actorId
|
|
2138
|
+
);
|
|
2139
|
+
|
|
2140
|
+
return ensureLinkRecords(
|
|
2141
|
+
runtime,
|
|
2142
|
+
{
|
|
2143
|
+
workspaceId: input.workspaceId,
|
|
2144
|
+
fromType: "components",
|
|
2145
|
+
fromId: component._id,
|
|
2146
|
+
toType: "environments",
|
|
2147
|
+
toId: environment._id,
|
|
2148
|
+
relationship: "belongs_to_environment"
|
|
2149
|
+
},
|
|
2150
|
+
actorId
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
async function updateComponent(
|
|
2155
|
+
runtime: DxRuntime,
|
|
2156
|
+
input: {
|
|
2157
|
+
workspaceId: string;
|
|
2158
|
+
id: string;
|
|
2159
|
+
name?: string;
|
|
2160
|
+
kind?: string;
|
|
2161
|
+
locator?: ComponentLocatorInput;
|
|
2162
|
+
summary?: string;
|
|
2163
|
+
identifiers?: ComponentIdentifiersInput;
|
|
2164
|
+
secretPointers?: SecretPointerInput[];
|
|
2165
|
+
notes?: string;
|
|
2166
|
+
fields?: Record<string, unknown>;
|
|
2167
|
+
unsetFields?: string[];
|
|
2168
|
+
revisionNote?: string;
|
|
2169
|
+
},
|
|
2170
|
+
actorId: string
|
|
2171
|
+
) {
|
|
2172
|
+
assertNoTypedFields(input.fields, "update_component", COMPONENT_TYPED_FIELDS);
|
|
2173
|
+
assertNoTypedUnsetFields(input.unsetFields, "update_component", COMPONENT_TYPED_FIELDS);
|
|
2174
|
+
|
|
2175
|
+
return updateRecord(
|
|
2176
|
+
runtime.db,
|
|
2177
|
+
{
|
|
2178
|
+
recordType: "components",
|
|
2179
|
+
workspaceId: input.workspaceId,
|
|
2180
|
+
id: input.id,
|
|
2181
|
+
title: input.name,
|
|
2182
|
+
summary: input.summary,
|
|
2183
|
+
fields: {
|
|
2184
|
+
...(input.fields ?? {}),
|
|
2185
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
2186
|
+
...(input.kind !== undefined ? { kind: input.kind } : {}),
|
|
2187
|
+
...(input.locator !== undefined ? { locator: input.locator } : {}),
|
|
2188
|
+
...(input.identifiers !== undefined ? { identifiers: input.identifiers } : {}),
|
|
2189
|
+
...(input.secretPointers !== undefined ? { secretPointers: input.secretPointers } : {}),
|
|
2190
|
+
...(input.notes !== undefined ? { notes: input.notes } : {})
|
|
2191
|
+
},
|
|
2192
|
+
unsetFields: input.unsetFields,
|
|
2193
|
+
allowManagedFields: true,
|
|
2194
|
+
revisionNote: input.revisionNote
|
|
2195
|
+
},
|
|
2196
|
+
actorId
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
async function createExpectation(
|
|
2201
|
+
runtime: DxRuntime,
|
|
2202
|
+
input: {
|
|
2203
|
+
workspaceId: string;
|
|
2204
|
+
title: string;
|
|
2205
|
+
statement: string;
|
|
2206
|
+
successRecognition?: string;
|
|
2207
|
+
approvalState?: "draft" | "approved" | "not_approved" | "superseded";
|
|
2208
|
+
approvedBy?: string;
|
|
2209
|
+
approvedAt?: string;
|
|
2210
|
+
source?: string;
|
|
2211
|
+
statementId?: string;
|
|
2212
|
+
fields?: Record<string, unknown>;
|
|
2213
|
+
},
|
|
2214
|
+
actorId: string
|
|
2215
|
+
) {
|
|
2216
|
+
assertNoTypedFields(input.fields, "create_expectation", EXPECTATION_TYPED_FIELDS);
|
|
2217
|
+
assertNoObsoleteExpectationFields(input.fields, "create_expectation");
|
|
2218
|
+
|
|
2219
|
+
if (input.statementId) {
|
|
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(
|
|
2229
|
+
runtime.db,
|
|
2230
|
+
"expectations",
|
|
2231
|
+
{
|
|
2232
|
+
workspaceId: input.workspaceId,
|
|
2233
|
+
title: input.title,
|
|
2234
|
+
summary: input.statement,
|
|
2235
|
+
fields: {
|
|
2236
|
+
...(input.fields ?? {}),
|
|
2237
|
+
statement: input.statement,
|
|
2238
|
+
...(input.successRecognition !== undefined ? { successRecognition: input.successRecognition } : {}),
|
|
2239
|
+
approvalState: input.approvalState ?? "draft",
|
|
2240
|
+
...(input.approvedBy !== undefined ? { approvedBy: input.approvedBy } : {}),
|
|
2241
|
+
...(input.approvedAt !== undefined ? { approvedAt: input.approvedAt } : {}),
|
|
2242
|
+
...(input.source !== undefined ? { source: input.source } : {})
|
|
2243
|
+
}
|
|
2244
|
+
},
|
|
2245
|
+
actorId
|
|
2246
|
+
);
|
|
2247
|
+
|
|
2248
|
+
if (!input.statementId) {
|
|
2249
|
+
return expectation;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
return linkRecords(
|
|
2253
|
+
runtime.db,
|
|
2254
|
+
{
|
|
2255
|
+
workspaceId: input.workspaceId,
|
|
2256
|
+
fromType: "expectations",
|
|
2257
|
+
fromId: expectation._id,
|
|
2258
|
+
toType: "statements",
|
|
2259
|
+
toId: input.statementId,
|
|
2260
|
+
relationship: "derives_from"
|
|
2261
|
+
},
|
|
2262
|
+
actorId
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
type EstimateLineItem = {
|
|
2267
|
+
id: string;
|
|
2268
|
+
label: string;
|
|
2269
|
+
timing: "one_time" | "recurring";
|
|
2270
|
+
period?: string;
|
|
2271
|
+
currency: string;
|
|
2272
|
+
amount:
|
|
2273
|
+
| {
|
|
2274
|
+
kind: "single";
|
|
2275
|
+
value: number;
|
|
2276
|
+
}
|
|
2277
|
+
| {
|
|
2278
|
+
kind: "range";
|
|
2279
|
+
min: number;
|
|
2280
|
+
expected: number;
|
|
2281
|
+
max: number;
|
|
2282
|
+
};
|
|
2283
|
+
};
|
|
2284
|
+
|
|
2285
|
+
type BenefitItem = {
|
|
2286
|
+
id: string;
|
|
2287
|
+
label: string;
|
|
2288
|
+
timing?: "one_time" | "recurring";
|
|
2289
|
+
period?: string;
|
|
2290
|
+
currency?: string;
|
|
2291
|
+
amount?: EstimateLineItem["amount"];
|
|
2292
|
+
};
|
|
2293
|
+
|
|
2294
|
+
type RollupAmount = {
|
|
2295
|
+
min: number;
|
|
2296
|
+
expected: number;
|
|
2297
|
+
max: number;
|
|
2298
|
+
};
|
|
2299
|
+
|
|
2300
|
+
type QuantifiedRollupItem = {
|
|
2301
|
+
timing: "one_time" | "recurring";
|
|
2302
|
+
period?: string;
|
|
2303
|
+
currency: string;
|
|
2304
|
+
amount: EstimateLineItem["amount"];
|
|
2305
|
+
};
|
|
2306
|
+
|
|
2307
|
+
async function createEstimate(
|
|
2308
|
+
runtime: DxRuntime,
|
|
2309
|
+
input: {
|
|
2310
|
+
workspaceId: string;
|
|
2311
|
+
title: string;
|
|
2312
|
+
summary?: string;
|
|
2313
|
+
lineItems: EstimateLineItemInput[];
|
|
2314
|
+
requirementIds?: string[];
|
|
2315
|
+
expectationIds?: string[];
|
|
2316
|
+
fields?: Record<string, unknown>;
|
|
2317
|
+
},
|
|
2318
|
+
actorId: string
|
|
2319
|
+
) {
|
|
2320
|
+
assertNoReservedFields(input.fields, ["workspaceId", "initiativeId", "requirementIds", "expectationIds"]);
|
|
2321
|
+
assertNoTypedFields(input.fields, "create_estimate", ESTIMATE_TYPED_FIELDS);
|
|
2322
|
+
assertAtLeastOneEstimateTarget(input.requirementIds, input.expectationIds);
|
|
2323
|
+
await assertRecordsExist(runtime, "requirements", input.requirementIds ?? [], input.workspaceId);
|
|
2324
|
+
await assertRecordsExist(runtime, "expectations", input.expectationIds ?? [], input.workspaceId);
|
|
2325
|
+
|
|
2326
|
+
const lineItems = normalizeEstimateLineItems(input.lineItems);
|
|
2327
|
+
let estimate = await createRecord(
|
|
2328
|
+
runtime.db,
|
|
2329
|
+
"estimates",
|
|
2330
|
+
{
|
|
2331
|
+
workspaceId: input.workspaceId,
|
|
2332
|
+
title: input.title,
|
|
2333
|
+
summary: input.summary,
|
|
2334
|
+
allowManagedFields: true,
|
|
2335
|
+
fields: {
|
|
2336
|
+
...(input.fields ?? {}),
|
|
2337
|
+
lineItems,
|
|
2338
|
+
rollup: computeQuantifiedRollup(lineItems)
|
|
2339
|
+
}
|
|
2340
|
+
},
|
|
2341
|
+
actorId
|
|
2342
|
+
);
|
|
2343
|
+
|
|
2344
|
+
estimate = await linkManyRecords(runtime, estimate, "requirements", input.requirementIds ?? [], "estimates", actorId);
|
|
2345
|
+
estimate = await linkManyRecords(runtime, estimate, "expectations", input.expectationIds ?? [], "estimates", actorId);
|
|
2346
|
+
|
|
2347
|
+
return estimate;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
async function updateEstimate(
|
|
2351
|
+
runtime: DxRuntime,
|
|
2352
|
+
input: {
|
|
2353
|
+
workspaceId: string;
|
|
2354
|
+
id: string;
|
|
2355
|
+
title?: string;
|
|
2356
|
+
summary?: string;
|
|
2357
|
+
lineItems?: EstimateLineItemInput[];
|
|
2358
|
+
fields?: Record<string, unknown>;
|
|
2359
|
+
unsetFields?: string[];
|
|
2360
|
+
revisionNote?: string;
|
|
2361
|
+
},
|
|
2362
|
+
actorId: string
|
|
2363
|
+
) {
|
|
2364
|
+
assertNoTypedFields(input.fields, "update_estimate", ESTIMATE_TYPED_FIELDS);
|
|
2365
|
+
assertNoTypedUnsetFields(input.unsetFields, "update_estimate", ESTIMATE_TYPED_FIELDS);
|
|
2366
|
+
|
|
2367
|
+
const lineItems = input.lineItems ? normalizeEstimateLineItems(input.lineItems) : undefined;
|
|
2368
|
+
|
|
2369
|
+
return updateRecord(
|
|
2370
|
+
runtime.db,
|
|
2371
|
+
{
|
|
2372
|
+
recordType: "estimates",
|
|
2373
|
+
workspaceId: input.workspaceId,
|
|
2374
|
+
id: input.id,
|
|
2375
|
+
title: input.title,
|
|
2376
|
+
summary: input.summary,
|
|
2377
|
+
fields: {
|
|
2378
|
+
...(input.fields ?? {}),
|
|
2379
|
+
...(lineItems ? { lineItems, rollup: computeQuantifiedRollup(lineItems) } : {})
|
|
2380
|
+
},
|
|
2381
|
+
unsetFields: input.unsetFields,
|
|
2382
|
+
allowManagedFields: true,
|
|
2383
|
+
revisionNote: input.revisionNote
|
|
2384
|
+
},
|
|
2385
|
+
actorId
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function normalizeEstimateLineItems(lineItems: EstimateLineItemInput[]): EstimateLineItem[] {
|
|
2390
|
+
const seenIds = new Set<string>();
|
|
2391
|
+
return lineItems.map((item, index) => {
|
|
2392
|
+
const id = item.id ?? randomUUID();
|
|
2393
|
+
if (seenIds.has(id)) {
|
|
2394
|
+
throw new Error(`Estimate line item id must be unique: ${id}.`);
|
|
2395
|
+
}
|
|
2396
|
+
seenIds.add(id);
|
|
2397
|
+
|
|
2398
|
+
if (item.timing === "recurring" && !item.period) {
|
|
2399
|
+
throw new Error(`Estimate line item ${index + 1} is recurring and requires period.`);
|
|
2400
|
+
}
|
|
2401
|
+
if (item.timing === "one_time" && item.period) {
|
|
2402
|
+
throw new Error(`Estimate line item ${index + 1} is one_time and must omit period.`);
|
|
2403
|
+
}
|
|
2404
|
+
if (item.amount.kind === "range" && !(item.amount.min <= item.amount.expected && item.amount.expected <= item.amount.max)) {
|
|
2405
|
+
throw new Error(`Estimate line item ${index + 1} range must satisfy min <= expected <= max.`);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
return {
|
|
2409
|
+
id,
|
|
2410
|
+
label: item.label,
|
|
2411
|
+
timing: item.timing,
|
|
2412
|
+
...(item.period ? { period: item.period } : {}),
|
|
2413
|
+
currency: item.currency,
|
|
2414
|
+
amount: item.amount
|
|
2415
|
+
};
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
async function createBenefits(
|
|
2420
|
+
runtime: DxRuntime,
|
|
2421
|
+
input: {
|
|
2422
|
+
workspaceId: string;
|
|
2423
|
+
title: string;
|
|
2424
|
+
summary?: string;
|
|
2425
|
+
benefitItems: BenefitItemInput[];
|
|
2426
|
+
requirementIds?: string[];
|
|
2427
|
+
expectationIds?: string[];
|
|
2428
|
+
fields?: Record<string, unknown>;
|
|
2429
|
+
},
|
|
2430
|
+
actorId: string
|
|
2431
|
+
) {
|
|
2432
|
+
assertNoReservedFields(input.fields, ["workspaceId", "initiativeId", "requirementIds", "expectationIds"]);
|
|
2433
|
+
assertNoTypedFields(input.fields, "create_benefits", BENEFITS_TYPED_FIELDS);
|
|
2434
|
+
assertAtLeastOneBenefitsTarget(input.requirementIds, input.expectationIds);
|
|
2435
|
+
await assertRecordsExist(runtime, "requirements", input.requirementIds ?? [], input.workspaceId);
|
|
2436
|
+
await assertRecordsExist(runtime, "expectations", input.expectationIds ?? [], input.workspaceId);
|
|
2437
|
+
|
|
2438
|
+
const benefitItems = normalizeBenefitItems(input.benefitItems);
|
|
2439
|
+
let benefits = await createRecord(
|
|
2440
|
+
runtime.db,
|
|
2441
|
+
"benefits",
|
|
2442
|
+
{
|
|
2443
|
+
workspaceId: input.workspaceId,
|
|
2444
|
+
title: input.title,
|
|
2445
|
+
summary: input.summary,
|
|
2446
|
+
allowManagedFields: true,
|
|
2447
|
+
fields: {
|
|
2448
|
+
...(input.fields ?? {}),
|
|
2449
|
+
benefitItems,
|
|
2450
|
+
rollup: computeQuantifiedRollup(benefitItems.filter(isQuantifiedBenefitItem))
|
|
2451
|
+
}
|
|
2452
|
+
},
|
|
2453
|
+
actorId
|
|
2454
|
+
);
|
|
2455
|
+
|
|
2456
|
+
benefits = await linkManyRecords(runtime, benefits, "requirements", input.requirementIds ?? [], "benefits", actorId);
|
|
2457
|
+
benefits = await linkManyRecords(runtime, benefits, "expectations", input.expectationIds ?? [], "benefits", actorId);
|
|
2458
|
+
|
|
2459
|
+
return benefits;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
async function updateBenefits(
|
|
2463
|
+
runtime: DxRuntime,
|
|
2464
|
+
input: {
|
|
2465
|
+
workspaceId: string;
|
|
2466
|
+
id: string;
|
|
2467
|
+
title?: string;
|
|
2468
|
+
summary?: string;
|
|
2469
|
+
benefitItems?: BenefitItemInput[];
|
|
2470
|
+
fields?: Record<string, unknown>;
|
|
2471
|
+
unsetFields?: string[];
|
|
2472
|
+
revisionNote?: string;
|
|
2473
|
+
},
|
|
2474
|
+
actorId: string
|
|
2475
|
+
) {
|
|
2476
|
+
assertNoTypedFields(input.fields, "update_benefits", BENEFITS_TYPED_FIELDS);
|
|
2477
|
+
assertNoTypedUnsetFields(input.unsetFields, "update_benefits", BENEFITS_TYPED_FIELDS);
|
|
2478
|
+
|
|
2479
|
+
const benefitItems = input.benefitItems ? normalizeBenefitItems(input.benefitItems) : undefined;
|
|
2480
|
+
|
|
2481
|
+
return updateRecord(
|
|
2482
|
+
runtime.db,
|
|
2483
|
+
{
|
|
2484
|
+
recordType: "benefits",
|
|
2485
|
+
workspaceId: input.workspaceId,
|
|
2486
|
+
id: input.id,
|
|
2487
|
+
title: input.title,
|
|
2488
|
+
summary: input.summary,
|
|
2489
|
+
fields: {
|
|
2490
|
+
...(input.fields ?? {}),
|
|
2491
|
+
...(benefitItems ? { benefitItems, rollup: computeQuantifiedRollup(benefitItems.filter(isQuantifiedBenefitItem)) } : {})
|
|
2492
|
+
},
|
|
2493
|
+
unsetFields: input.unsetFields,
|
|
2494
|
+
allowManagedFields: true,
|
|
2495
|
+
revisionNote: input.revisionNote
|
|
2496
|
+
},
|
|
2497
|
+
actorId
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
function normalizeBenefitItems(benefitItems: BenefitItemInput[]): BenefitItem[] {
|
|
2502
|
+
const seenIds = new Set<string>();
|
|
2503
|
+
return benefitItems.map((item, index) => {
|
|
2504
|
+
const id = item.id ?? randomUUID();
|
|
2505
|
+
if (seenIds.has(id)) {
|
|
2506
|
+
throw new Error(`Benefit item id must be unique: ${id}.`);
|
|
2507
|
+
}
|
|
2508
|
+
seenIds.add(id);
|
|
2509
|
+
|
|
2510
|
+
if (!item.amount) {
|
|
2511
|
+
if (item.timing || item.period || item.currency) {
|
|
2512
|
+
throw new Error(`Benefit item ${index + 1} is qualitative and must omit timing, period, and currency.`);
|
|
2513
|
+
}
|
|
2514
|
+
return {
|
|
2515
|
+
id,
|
|
2516
|
+
label: item.label
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
if (!item.timing) {
|
|
2521
|
+
throw new Error(`Benefit item ${index + 1} has amount and requires timing.`);
|
|
2522
|
+
}
|
|
2523
|
+
if (!item.currency) {
|
|
2524
|
+
throw new Error(`Benefit item ${index + 1} has amount and requires currency.`);
|
|
2525
|
+
}
|
|
2526
|
+
if (item.timing === "recurring" && !item.period) {
|
|
2527
|
+
throw new Error(`Benefit item ${index + 1} is recurring and requires period.`);
|
|
2528
|
+
}
|
|
2529
|
+
if (item.timing === "one_time" && item.period) {
|
|
2530
|
+
throw new Error(`Benefit item ${index + 1} is one_time and must omit period.`);
|
|
2531
|
+
}
|
|
2532
|
+
if (item.amount.kind === "range" && !(item.amount.min <= item.amount.expected && item.amount.expected <= item.amount.max)) {
|
|
2533
|
+
throw new Error(`Benefit item ${index + 1} range must satisfy min <= expected <= max.`);
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
return {
|
|
2537
|
+
id,
|
|
2538
|
+
label: item.label,
|
|
2539
|
+
timing: item.timing,
|
|
2540
|
+
...(item.period ? { period: item.period } : {}),
|
|
2541
|
+
currency: item.currency,
|
|
2542
|
+
amount: item.amount
|
|
2543
|
+
};
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function isQuantifiedBenefitItem(item: BenefitItem): item is BenefitItem & QuantifiedRollupItem {
|
|
2548
|
+
return !!item.amount && !!item.timing && !!item.currency;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
function computeQuantifiedRollup(lineItems: QuantifiedRollupItem[]): Record<string, unknown> {
|
|
2552
|
+
const currencies: Record<string, Record<string, unknown>> = {};
|
|
2553
|
+
|
|
2554
|
+
for (const item of lineItems) {
|
|
2555
|
+
const currency = (currencies[item.currency] ??= createEmptyRollupTimingGroup());
|
|
2556
|
+
const amount = normalizeRollupAmount(item.amount);
|
|
2557
|
+
|
|
2558
|
+
if (item.timing === "one_time") {
|
|
2559
|
+
currency.one_time = addRollupAmounts(currency.one_time as RollupAmount | undefined, amount);
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
const recurring = currency.recurring as Record<string, RollupAmount>;
|
|
2564
|
+
recurring[item.period ?? ""] = addRollupAmounts(recurring[item.period ?? ""], amount);
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
return { currencies };
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
function createEmptyRollupTimingGroup(): Record<string, unknown> {
|
|
2571
|
+
return {
|
|
2572
|
+
one_time: createZeroRollupAmount(),
|
|
2573
|
+
recurring: {}
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
function createZeroRollupAmount(): RollupAmount {
|
|
2578
|
+
return { min: 0, expected: 0, max: 0 };
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function normalizeRollupAmount(amount: EstimateLineItem["amount"]): RollupAmount {
|
|
2582
|
+
if (amount.kind === "single") {
|
|
2583
|
+
return { min: amount.value, expected: amount.value, max: amount.value };
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
return { min: amount.min, expected: amount.expected, max: amount.max };
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
function addRollupAmounts(
|
|
2590
|
+
left: RollupAmount | undefined,
|
|
2591
|
+
right: RollupAmount
|
|
2592
|
+
): RollupAmount {
|
|
2593
|
+
const base = left ?? createZeroRollupAmount();
|
|
2594
|
+
return {
|
|
2595
|
+
min: base.min + right.min,
|
|
2596
|
+
expected: base.expected + right.expected,
|
|
2597
|
+
max: base.max + right.max
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
async function createRequirement(
|
|
2602
|
+
runtime: DxRuntime,
|
|
2603
|
+
input: {
|
|
2604
|
+
workspaceId: string;
|
|
2605
|
+
title: string;
|
|
2606
|
+
expectationId?: string;
|
|
2607
|
+
statement: string;
|
|
2608
|
+
acceptanceCriteria?: string[];
|
|
2609
|
+
priority?: "low" | "medium" | "high";
|
|
2610
|
+
status?: "draft" | "ready" | "approved" | "superseded";
|
|
2611
|
+
fields?: Record<string, unknown>;
|
|
2612
|
+
},
|
|
2613
|
+
actorId: string
|
|
2614
|
+
) {
|
|
2615
|
+
assertNoReservedFields(input.fields, ["workspaceId", "expectationId"]);
|
|
2616
|
+
assertNoTypedFields(input.fields, "create_requirement", REQUIREMENT_TYPED_FIELDS);
|
|
2617
|
+
|
|
2618
|
+
if (input.expectationId) {
|
|
2619
|
+
const expectation = await getRecord(runtime.db, "expectations", input.expectationId, {
|
|
2620
|
+
workspaceId: input.workspaceId
|
|
2621
|
+
});
|
|
2622
|
+
if (!expectation) {
|
|
2623
|
+
throw new Error(`Expectation not found: expectations/${input.expectationId}`);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
const requirement = await createRecord(
|
|
2628
|
+
runtime.db,
|
|
2629
|
+
"requirements",
|
|
2630
|
+
{
|
|
2631
|
+
workspaceId: input.workspaceId,
|
|
2632
|
+
title: input.title,
|
|
2633
|
+
summary: input.statement,
|
|
2634
|
+
fields: {
|
|
2635
|
+
...(input.fields ?? {}),
|
|
2636
|
+
statement: input.statement,
|
|
2637
|
+
...(input.acceptanceCriteria ? { acceptanceCriteria: input.acceptanceCriteria } : {}),
|
|
2638
|
+
...(input.priority ? { priority: input.priority } : {}),
|
|
2639
|
+
status: input.status ?? "draft"
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
actorId
|
|
2643
|
+
);
|
|
2644
|
+
|
|
2645
|
+
if (!input.expectationId) {
|
|
2646
|
+
return requirement;
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
return linkRecords(
|
|
2650
|
+
runtime.db,
|
|
2651
|
+
{
|
|
2652
|
+
workspaceId: input.workspaceId,
|
|
2653
|
+
fromType: "requirements",
|
|
2654
|
+
fromId: requirement._id,
|
|
2655
|
+
toType: "expectations",
|
|
2656
|
+
toId: input.expectationId,
|
|
2657
|
+
relationship: "satisfies"
|
|
2658
|
+
},
|
|
2659
|
+
actorId
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
async function createTask(
|
|
2664
|
+
runtime: DxRuntime,
|
|
2665
|
+
input: {
|
|
2666
|
+
workspaceId: string;
|
|
2667
|
+
title: string;
|
|
2668
|
+
description: string;
|
|
2669
|
+
assignee?: string;
|
|
2670
|
+
assignor?: string;
|
|
2671
|
+
requirementId?: string;
|
|
2672
|
+
initialStatus?: "open" | "in_progress" | "blocked" | "done";
|
|
2673
|
+
fields?: Record<string, unknown>;
|
|
2674
|
+
},
|
|
2675
|
+
actorId: string
|
|
2676
|
+
) {
|
|
2677
|
+
assertNoReservedFields(input.fields, ["workspaceId", "requirementId"]);
|
|
2678
|
+
assertNoTypedFields(input.fields, "create_task", TASK_TYPED_FIELDS);
|
|
2679
|
+
|
|
2680
|
+
if (input.requirementId) {
|
|
2681
|
+
const requirement = await getRecord(runtime.db, "requirements", input.requirementId, {
|
|
2682
|
+
workspaceId: input.workspaceId
|
|
2683
|
+
});
|
|
2684
|
+
if (!requirement) {
|
|
2685
|
+
throw new Error(`Requirement not found: requirements/${input.requirementId}`);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
const now = new Date().toISOString();
|
|
2690
|
+
const initialStatus = input.initialStatus ?? "open";
|
|
2691
|
+
const initialStatusEntry: TaskEntry = {
|
|
2692
|
+
id: randomUUID(),
|
|
2693
|
+
entryType: "status_change",
|
|
2694
|
+
body: `Initial status: ${initialStatus}.`,
|
|
2695
|
+
status: initialStatus,
|
|
2696
|
+
createdAt: now,
|
|
2697
|
+
createdBy: actorId
|
|
2698
|
+
};
|
|
2699
|
+
|
|
2700
|
+
const task = await createRecord(
|
|
2701
|
+
runtime.db,
|
|
2702
|
+
"tasks",
|
|
2703
|
+
{
|
|
2704
|
+
workspaceId: input.workspaceId,
|
|
2705
|
+
title: input.title,
|
|
2706
|
+
summary: input.description,
|
|
2707
|
+
allowManagedFields: true,
|
|
2708
|
+
fields: {
|
|
2709
|
+
...(input.fields ?? {}),
|
|
2710
|
+
description: input.description,
|
|
2711
|
+
...(input.assignee !== undefined ? { assignee: input.assignee } : {}),
|
|
2712
|
+
...(input.assignor !== undefined ? { assignor: input.assignor } : {}),
|
|
2713
|
+
entries: [initialStatusEntry],
|
|
2714
|
+
currentStatus: taskEntryToCurrentStatus(initialStatusEntry)
|
|
2715
|
+
}
|
|
2716
|
+
},
|
|
2717
|
+
actorId
|
|
2718
|
+
);
|
|
2719
|
+
|
|
2720
|
+
if (!input.requirementId) {
|
|
2721
|
+
return task;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
return linkRecords(
|
|
2725
|
+
runtime.db,
|
|
2726
|
+
{
|
|
2727
|
+
workspaceId: input.workspaceId,
|
|
2728
|
+
fromType: "tasks",
|
|
2729
|
+
fromId: task._id,
|
|
2730
|
+
toType: "requirements",
|
|
2731
|
+
toId: input.requirementId,
|
|
2732
|
+
relationship: "implements"
|
|
2733
|
+
},
|
|
2734
|
+
actorId
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
async function createDecision(
|
|
2739
|
+
runtime: DxRuntime,
|
|
2740
|
+
input: {
|
|
2741
|
+
workspaceId: string;
|
|
2742
|
+
title: string;
|
|
2743
|
+
matter: string;
|
|
2744
|
+
initialEntries?: DecisionInitialEntryInput[];
|
|
2745
|
+
initialDecision?: InitialDecisionInput;
|
|
2746
|
+
fields?: Record<string, unknown>;
|
|
2747
|
+
},
|
|
2748
|
+
actorId: string
|
|
2749
|
+
) {
|
|
2750
|
+
assertNoTypedFields(input.fields, "create_decision", DECISION_TYPED_FIELDS);
|
|
2751
|
+
assertNoDecisionInputFields(input.fields);
|
|
2752
|
+
|
|
2753
|
+
const now = new Date().toISOString();
|
|
2754
|
+
const entries = buildInitialDecisionEntries(input.initialEntries, input.initialDecision, actorId, now);
|
|
2755
|
+
const latestDecisionEntry = [...entries].reverse().find((entry) => entry.entryType === "decision");
|
|
2756
|
+
|
|
2757
|
+
return createRecord(
|
|
2758
|
+
runtime.db,
|
|
2759
|
+
"decisions",
|
|
2760
|
+
{
|
|
2761
|
+
workspaceId: input.workspaceId,
|
|
2762
|
+
title: input.title,
|
|
2763
|
+
summary: latestDecisionEntry?.body ?? input.matter,
|
|
2764
|
+
allowManagedFields: true,
|
|
2765
|
+
fields: {
|
|
2766
|
+
...(input.fields ?? {}),
|
|
2767
|
+
matter: input.matter,
|
|
2768
|
+
entries,
|
|
2769
|
+
...(latestDecisionEntry ? { currentDecision: decisionEntryToCurrentDecision(latestDecisionEntry) } : {})
|
|
2770
|
+
}
|
|
2771
|
+
},
|
|
2772
|
+
actorId
|
|
2773
|
+
);
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
function buildInitialDecisionEntries(
|
|
2777
|
+
initialEntries: DecisionInitialEntryInput[] | undefined,
|
|
2778
|
+
initialDecision: InitialDecisionInput | undefined,
|
|
2779
|
+
actorId: string,
|
|
2780
|
+
createdAt: string
|
|
2781
|
+
): DecisionEntry[] {
|
|
2782
|
+
const entries = (initialEntries ?? []).map((entry) =>
|
|
2783
|
+
buildDecisionEntry(
|
|
2784
|
+
{
|
|
2785
|
+
entryType: entry.entryType,
|
|
2786
|
+
body: entry.body,
|
|
2787
|
+
decidedBy: entry.decidedBy,
|
|
2788
|
+
rationale: entry.rationale
|
|
2789
|
+
},
|
|
2790
|
+
actorId,
|
|
2791
|
+
createdAt
|
|
2792
|
+
)
|
|
2793
|
+
);
|
|
2794
|
+
|
|
2795
|
+
if (initialDecision) {
|
|
2796
|
+
entries.push(
|
|
2797
|
+
buildDecisionEntry(
|
|
2798
|
+
{
|
|
2799
|
+
entryType: "decision",
|
|
2800
|
+
body: initialDecision.body,
|
|
2801
|
+
decidedBy: initialDecision.decidedBy,
|
|
2802
|
+
rationale: initialDecision.rationale
|
|
2803
|
+
},
|
|
2804
|
+
actorId,
|
|
2805
|
+
createdAt
|
|
2806
|
+
)
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
return entries;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
function buildDecisionEntry(
|
|
2814
|
+
input: {
|
|
2815
|
+
entryType: "argument" | "decision" | "note";
|
|
2816
|
+
body: string;
|
|
2817
|
+
decidedBy?: string;
|
|
2818
|
+
rationale?: string;
|
|
2819
|
+
},
|
|
2820
|
+
actorId: string,
|
|
2821
|
+
createdAt: string
|
|
2822
|
+
): DecisionEntry {
|
|
2823
|
+
if (input.entryType !== "decision" && (input.decidedBy || input.rationale)) {
|
|
2824
|
+
throw new Error("decidedBy and rationale are only valid on decision entries.");
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
return {
|
|
2828
|
+
id: randomUUID(),
|
|
2829
|
+
entryType: input.entryType,
|
|
2830
|
+
body: input.body,
|
|
2831
|
+
createdAt,
|
|
2832
|
+
createdBy: actorId,
|
|
2833
|
+
...(input.decidedBy ? { decidedBy: input.decidedBy } : {}),
|
|
2834
|
+
...(input.rationale ? { rationale: input.rationale } : {})
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
async function createCommitment(
|
|
2839
|
+
runtime: DxRuntime,
|
|
2840
|
+
input: {
|
|
2841
|
+
workspaceId: string;
|
|
2842
|
+
title: string;
|
|
2843
|
+
summary?: string;
|
|
2844
|
+
commitmentStatement: string;
|
|
2845
|
+
requirementIds?: string[];
|
|
2846
|
+
expectationIds?: string[];
|
|
2847
|
+
reservationNotes?: string[];
|
|
2848
|
+
deferralId?: string;
|
|
2849
|
+
fields?: Record<string, unknown>;
|
|
2850
|
+
},
|
|
2851
|
+
actorId: string
|
|
2852
|
+
) {
|
|
2853
|
+
assertNoReservedFields(input.fields, ["workspaceId", "requirementIds", "expectationIds", "deferralId"]);
|
|
2854
|
+
assertNoTypedFields(input.fields, "create_commitment", COMMITMENT_TYPED_FIELDS);
|
|
2855
|
+
assertAtLeastOneCommitmentTarget(input.requirementIds, input.expectationIds);
|
|
2856
|
+
await assertRecordsExist(runtime, "requirements", input.requirementIds ?? [], input.workspaceId);
|
|
2857
|
+
await assertRecordsExist(runtime, "expectations", input.expectationIds ?? [], input.workspaceId);
|
|
2858
|
+
|
|
2859
|
+
if (input.deferralId) {
|
|
2860
|
+
const deferral = await getRecord(runtime.db, "deferrals", input.deferralId, {
|
|
2861
|
+
workspaceId: input.workspaceId
|
|
2862
|
+
});
|
|
2863
|
+
if (!deferral) {
|
|
2864
|
+
throw new Error(`Deferral not found: deferrals/${input.deferralId}`);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
const now = new Date().toISOString();
|
|
2869
|
+
let commitment = await createRecord(
|
|
2870
|
+
runtime.db,
|
|
2871
|
+
"commitments",
|
|
2872
|
+
{
|
|
2873
|
+
workspaceId: input.workspaceId,
|
|
2874
|
+
title: input.title,
|
|
2875
|
+
summary: input.summary ?? input.commitmentStatement,
|
|
2876
|
+
allowManagedFields: true,
|
|
2877
|
+
fields: {
|
|
2878
|
+
...(input.fields ?? {}),
|
|
2879
|
+
commitmentStatement: input.commitmentStatement,
|
|
2880
|
+
reservations: (input.reservationNotes ?? []).map((note) => ({
|
|
2881
|
+
id: randomUUID(),
|
|
2882
|
+
note,
|
|
2883
|
+
createdAt: now,
|
|
2884
|
+
createdBy: actorId
|
|
2885
|
+
}))
|
|
2886
|
+
}
|
|
2887
|
+
},
|
|
2888
|
+
actorId
|
|
2889
|
+
);
|
|
2890
|
+
|
|
2891
|
+
commitment = await linkManyRecords(runtime, commitment, "requirements", input.requirementIds ?? [], "commits", actorId);
|
|
2892
|
+
commitment = await linkManyRecords(runtime, commitment, "expectations", input.expectationIds ?? [], "commits", actorId);
|
|
2893
|
+
|
|
2894
|
+
if (input.deferralId) {
|
|
2895
|
+
commitment = await ensureLinkRecords(
|
|
2896
|
+
runtime,
|
|
2897
|
+
{
|
|
2898
|
+
workspaceId: input.workspaceId,
|
|
2899
|
+
fromType: "commitments",
|
|
2900
|
+
fromId: commitment._id,
|
|
2901
|
+
toType: "deferrals",
|
|
2902
|
+
toId: input.deferralId,
|
|
2903
|
+
relationship: "resolves_deferral"
|
|
2904
|
+
},
|
|
2905
|
+
actorId
|
|
2906
|
+
);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
return commitment;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
async function createDeferral(
|
|
2913
|
+
runtime: DxRuntime,
|
|
2914
|
+
input: {
|
|
2915
|
+
workspaceId: string;
|
|
2916
|
+
title: string;
|
|
2917
|
+
summary?: string;
|
|
2918
|
+
reason: string;
|
|
2919
|
+
conditions: string[];
|
|
2920
|
+
requirementIds?: string[];
|
|
2921
|
+
expectationIds?: string[];
|
|
2922
|
+
fields?: Record<string, unknown>;
|
|
2923
|
+
},
|
|
2924
|
+
actorId: string
|
|
2925
|
+
) {
|
|
2926
|
+
assertNoReservedFields(input.fields, ["workspaceId", "requirementIds", "expectationIds"]);
|
|
2927
|
+
assertNoTypedFields(input.fields, "create_deferral", DEFERRAL_TYPED_FIELDS);
|
|
2928
|
+
await assertRecordsExist(runtime, "requirements", input.requirementIds ?? [], input.workspaceId);
|
|
2929
|
+
await assertRecordsExist(runtime, "expectations", input.expectationIds ?? [], input.workspaceId);
|
|
2930
|
+
|
|
2931
|
+
const now = new Date().toISOString();
|
|
2932
|
+
let deferral = await createRecord(
|
|
2933
|
+
runtime.db,
|
|
2934
|
+
"deferrals",
|
|
2935
|
+
{
|
|
2936
|
+
workspaceId: input.workspaceId,
|
|
2937
|
+
title: input.title,
|
|
2938
|
+
summary: input.summary ?? input.reason,
|
|
2939
|
+
allowManagedFields: true,
|
|
2940
|
+
fields: {
|
|
2941
|
+
...(input.fields ?? {}),
|
|
2942
|
+
reason: input.reason,
|
|
2943
|
+
status: "open",
|
|
2944
|
+
conditions: input.conditions.map((statement) => ({
|
|
2945
|
+
id: randomUUID(),
|
|
2946
|
+
statement,
|
|
2947
|
+
state: "open",
|
|
2948
|
+
createdAt: now,
|
|
2949
|
+
createdBy: actorId,
|
|
2950
|
+
updatedAt: now,
|
|
2951
|
+
updatedBy: actorId
|
|
2952
|
+
})),
|
|
2953
|
+
conditionEvents: []
|
|
2954
|
+
}
|
|
2955
|
+
},
|
|
2956
|
+
actorId
|
|
2957
|
+
);
|
|
2958
|
+
|
|
2959
|
+
deferral = await linkManyRecords(runtime, deferral, "requirements", input.requirementIds ?? [], "defers", actorId);
|
|
2960
|
+
deferral = await linkManyRecords(runtime, deferral, "expectations", input.expectationIds ?? [], "defers", actorId);
|
|
2961
|
+
|
|
2962
|
+
return deferral;
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
async function appendDeferralEventForTool(
|
|
2966
|
+
runtime: DxRuntime,
|
|
2967
|
+
input: {
|
|
2968
|
+
workspaceId: string;
|
|
2969
|
+
deferralId: string;
|
|
2970
|
+
eventType: "condition_addressed" | "condition_reopened" | "condition_note_added" | "deferral_resolved" | "deferral_abandoned";
|
|
2971
|
+
conditionId?: string;
|
|
2972
|
+
note?: string;
|
|
2973
|
+
summary?: string;
|
|
2974
|
+
reason?: string;
|
|
2975
|
+
commitmentId?: string;
|
|
2976
|
+
},
|
|
2977
|
+
actorId: string
|
|
2978
|
+
) {
|
|
2979
|
+
const event = buildDeferralEventContent(input);
|
|
2980
|
+
|
|
2981
|
+
if (input.eventType === "deferral_resolved") {
|
|
2982
|
+
const commitment = await getRecord(runtime.db, "commitments", event.commitmentId as string, {
|
|
2983
|
+
workspaceId: input.workspaceId
|
|
2984
|
+
});
|
|
2985
|
+
if (!commitment) {
|
|
2986
|
+
throw new Error(`Commitment not found: commitments/${event.commitmentId}`);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
const deferral = await appendDeferralEvent(
|
|
2991
|
+
runtime.db,
|
|
2992
|
+
{
|
|
2993
|
+
workspaceId: input.workspaceId,
|
|
2994
|
+
deferralId: input.deferralId,
|
|
2995
|
+
eventType: input.eventType,
|
|
2996
|
+
event
|
|
2997
|
+
},
|
|
2998
|
+
actorId
|
|
2999
|
+
);
|
|
3000
|
+
|
|
3001
|
+
if (input.eventType !== "deferral_resolved") {
|
|
3002
|
+
return deferral;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
await ensureLinkRecords(
|
|
3006
|
+
runtime,
|
|
3007
|
+
{
|
|
3008
|
+
workspaceId: input.workspaceId,
|
|
3009
|
+
fromType: "commitments",
|
|
3010
|
+
fromId: event.commitmentId as string,
|
|
3011
|
+
toType: "deferrals",
|
|
3012
|
+
toId: deferral._id,
|
|
3013
|
+
relationship: "resolves_deferral"
|
|
3014
|
+
},
|
|
3015
|
+
actorId
|
|
3016
|
+
);
|
|
3017
|
+
|
|
3018
|
+
const updated = await getRecord(runtime.db, "deferrals", input.deferralId, { workspaceId: input.workspaceId });
|
|
3019
|
+
if (!updated) {
|
|
3020
|
+
throw new Error(`Deferral not found after resolution link: deferrals/${input.deferralId}`);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
return updated;
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
function buildDeferralEventContent(input: {
|
|
3027
|
+
eventType: "condition_addressed" | "condition_reopened" | "condition_note_added" | "deferral_resolved" | "deferral_abandoned";
|
|
3028
|
+
conditionId?: string;
|
|
3029
|
+
note?: string;
|
|
3030
|
+
summary?: string;
|
|
3031
|
+
reason?: string;
|
|
3032
|
+
commitmentId?: string;
|
|
3033
|
+
}): Record<string, unknown> {
|
|
3034
|
+
switch (input.eventType) {
|
|
3035
|
+
case "condition_addressed":
|
|
3036
|
+
case "condition_reopened":
|
|
3037
|
+
return compactObject({
|
|
3038
|
+
conditionId: requireDeferralEventField(input.conditionId, input.eventType, "conditionId"),
|
|
3039
|
+
summary: input.summary
|
|
3040
|
+
});
|
|
3041
|
+
case "condition_note_added":
|
|
3042
|
+
return compactObject({
|
|
3043
|
+
conditionId: requireDeferralEventField(input.conditionId, input.eventType, "conditionId"),
|
|
3044
|
+
note: requireDeferralEventField(input.note, input.eventType, "note")
|
|
3045
|
+
});
|
|
3046
|
+
case "deferral_resolved":
|
|
3047
|
+
return compactObject({
|
|
3048
|
+
commitmentId: requireDeferralEventField(input.commitmentId, input.eventType, "commitmentId"),
|
|
3049
|
+
summary: input.summary
|
|
3050
|
+
});
|
|
3051
|
+
case "deferral_abandoned":
|
|
3052
|
+
return compactObject({
|
|
3053
|
+
reason: requireDeferralEventField(input.reason, input.eventType, "reason"),
|
|
3054
|
+
summary: input.summary
|
|
3055
|
+
});
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
function requireDeferralEventField<T>(value: T | undefined, eventType: string, fieldName: string): T {
|
|
3060
|
+
if (value === undefined) {
|
|
3061
|
+
throw new Error(`${eventType} requires ${fieldName}.`);
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
return value;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
function assertAtLeastOneCommitmentTarget(requirementIds: string[] | undefined, expectationIds: string[] | undefined): void {
|
|
3068
|
+
if ((requirementIds?.length ?? 0) > 0 || (expectationIds?.length ?? 0) > 0) {
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
throw new Error("create_commitment requires at least one requirementId or expectationId.");
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
function assertAtLeastOneEstimateTarget(requirementIds: string[] | undefined, expectationIds: string[] | undefined): void {
|
|
3076
|
+
if ((requirementIds?.length ?? 0) > 0 || (expectationIds?.length ?? 0) > 0) {
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
throw new Error("create_estimate requires at least one requirementId or expectationId.");
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
function assertAtLeastOneBenefitsTarget(requirementIds: string[] | undefined, expectationIds: string[] | undefined): void {
|
|
3084
|
+
if ((requirementIds?.length ?? 0) > 0 || (expectationIds?.length ?? 0) > 0) {
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
throw new Error("create_benefits requires at least one requirementId or expectationId.");
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
async function assertRecordsExist(
|
|
3092
|
+
runtime: DxRuntime,
|
|
3093
|
+
recordType: "requirements" | "expectations",
|
|
3094
|
+
ids: string[],
|
|
3095
|
+
workspaceId: string
|
|
3096
|
+
): Promise<void> {
|
|
3097
|
+
for (const id of new Set(ids)) {
|
|
3098
|
+
const record = await getRecord(runtime.db, recordType, id, { workspaceId });
|
|
3099
|
+
if (!record) {
|
|
3100
|
+
throw new Error(`Record not found: ${recordType}/${id}`);
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
async function linkManyRecords(
|
|
3106
|
+
runtime: DxRuntime,
|
|
3107
|
+
source: Awaited<ReturnType<typeof getRecord>>,
|
|
3108
|
+
toType: "requirements" | "expectations",
|
|
3109
|
+
toIds: string[],
|
|
3110
|
+
relationship: string,
|
|
3111
|
+
actorId: string
|
|
3112
|
+
) {
|
|
3113
|
+
if (!source) {
|
|
3114
|
+
throw new Error("Source record not found.");
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
let current = source;
|
|
3118
|
+
for (const toId of new Set(toIds)) {
|
|
3119
|
+
current = await ensureLinkRecords(
|
|
3120
|
+
runtime,
|
|
3121
|
+
{
|
|
3122
|
+
workspaceId: current.workspaceId ?? "",
|
|
3123
|
+
fromType: current.recordType as CollectionName,
|
|
3124
|
+
fromId: current._id,
|
|
3125
|
+
toType,
|
|
3126
|
+
toId,
|
|
3127
|
+
relationship
|
|
3128
|
+
},
|
|
3129
|
+
actorId
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
return current;
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
async function ensureLinkRecords(
|
|
3137
|
+
runtime: DxRuntime,
|
|
3138
|
+
input: {
|
|
3139
|
+
workspaceId: string;
|
|
3140
|
+
fromType: CollectionName;
|
|
3141
|
+
fromId: string;
|
|
3142
|
+
toType: CollectionName;
|
|
3143
|
+
toId: string;
|
|
3144
|
+
relationship: string;
|
|
3145
|
+
},
|
|
3146
|
+
actorId: string
|
|
3147
|
+
) {
|
|
3148
|
+
const source = await getRecord(runtime.db, input.fromType, input.fromId, {
|
|
3149
|
+
workspaceId: input.workspaceId
|
|
3150
|
+
});
|
|
3151
|
+
if (!source) {
|
|
3152
|
+
throw new Error(`Source record not found: ${input.fromType}/${input.fromId}`);
|
|
3153
|
+
}
|
|
3154
|
+
const target = await getRecord(runtime.db, input.toType, input.toId, {
|
|
3155
|
+
workspaceId: input.workspaceId
|
|
3156
|
+
});
|
|
3157
|
+
if (!target) {
|
|
3158
|
+
throw new Error(`Target record not found: ${input.toType}/${input.toId}`);
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
if (
|
|
3162
|
+
source.links.some(
|
|
3163
|
+
(link) =>
|
|
3164
|
+
link.toType === input.toType &&
|
|
3165
|
+
link.toId === target._id &&
|
|
3166
|
+
link.relationship === input.relationship
|
|
3167
|
+
)
|
|
3168
|
+
) {
|
|
3169
|
+
return source;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
return linkRecords(runtime.db, input, actorId);
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
async function createChange(
|
|
3176
|
+
runtime: DxRuntime,
|
|
3177
|
+
input: {
|
|
3178
|
+
workspaceId: string;
|
|
3179
|
+
title: string;
|
|
3180
|
+
summary?: string;
|
|
3181
|
+
changePlan: string;
|
|
3182
|
+
executionSteps: string[];
|
|
3183
|
+
rollbackPlan: string;
|
|
3184
|
+
riskImpact: string;
|
|
3185
|
+
plannedFor?: string;
|
|
3186
|
+
requirementId?: string;
|
|
3187
|
+
fields?: Record<string, unknown>;
|
|
3188
|
+
},
|
|
3189
|
+
actorId: string
|
|
3190
|
+
) {
|
|
3191
|
+
assertNoReservedFields(input.fields, ["workspaceId", "initiativeId", "requirementId"]);
|
|
3192
|
+
assertNoTypedFields(input.fields, "create_change", CHANGE_TYPED_FIELDS);
|
|
3193
|
+
|
|
3194
|
+
if (input.requirementId) {
|
|
3195
|
+
const requirement = await getRecord(runtime.db, "requirements", input.requirementId, {
|
|
3196
|
+
workspaceId: input.workspaceId
|
|
3197
|
+
});
|
|
3198
|
+
if (!requirement) {
|
|
3199
|
+
throw new Error(`Requirement not found: requirements/${input.requirementId}`);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
let change = await createRecord(
|
|
3204
|
+
runtime.db,
|
|
3205
|
+
"changes",
|
|
3206
|
+
{
|
|
3207
|
+
workspaceId: input.workspaceId,
|
|
3208
|
+
title: input.title,
|
|
3209
|
+
summary: input.summary ?? input.changePlan,
|
|
3210
|
+
allowManagedFields: true,
|
|
3211
|
+
fields: {
|
|
3212
|
+
...(input.fields ?? {}),
|
|
3213
|
+
changePlan: input.changePlan,
|
|
3214
|
+
executionSteps: input.executionSteps,
|
|
3215
|
+
rollbackPlan: input.rollbackPlan,
|
|
3216
|
+
riskImpact: input.riskImpact,
|
|
3217
|
+
...(input.plannedFor !== undefined ? { plannedFor: input.plannedFor } : {})
|
|
3218
|
+
}
|
|
3219
|
+
},
|
|
3220
|
+
actorId
|
|
3221
|
+
);
|
|
3222
|
+
|
|
3223
|
+
if (input.requirementId) {
|
|
3224
|
+
change = await linkRecords(
|
|
3225
|
+
runtime.db,
|
|
3226
|
+
{
|
|
3227
|
+
workspaceId: input.workspaceId,
|
|
3228
|
+
fromType: "changes",
|
|
3229
|
+
fromId: change._id,
|
|
3230
|
+
toType: "requirements",
|
|
3231
|
+
toId: input.requirementId,
|
|
3232
|
+
relationship: "for_requirement"
|
|
3233
|
+
},
|
|
3234
|
+
actorId
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
return change;
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
function buildChangeEventContent(input: {
|
|
3242
|
+
eventType:
|
|
3243
|
+
| "notice_given"
|
|
3244
|
+
| "veto_recorded"
|
|
3245
|
+
| "emergency_declared"
|
|
3246
|
+
| "decision_recorded"
|
|
3247
|
+
| "result_reported"
|
|
3248
|
+
| "recovery_recorded"
|
|
3249
|
+
| "plan_revised"
|
|
3250
|
+
| "note_added";
|
|
3251
|
+
notice?: string;
|
|
3252
|
+
noticeTo?: string[];
|
|
3253
|
+
effectiveAt?: string;
|
|
3254
|
+
vetoByRole?: "Owner" | "Engineer";
|
|
3255
|
+
reason?: string;
|
|
3256
|
+
importance?: string;
|
|
3257
|
+
immediacy?: string;
|
|
3258
|
+
decision?: "proceed" | "defer" | "cancel";
|
|
3259
|
+
result?: "completed" | "failed" | "rolled_back";
|
|
3260
|
+
summary?: string;
|
|
3261
|
+
note?: string;
|
|
3262
|
+
revisedChangePlan?: string;
|
|
3263
|
+
revisedExecutionSteps?: string[];
|
|
3264
|
+
revisedRollbackPlan?: string;
|
|
3265
|
+
revisedRiskImpact?: string;
|
|
3266
|
+
revisedPlannedFor?: string;
|
|
3267
|
+
}): Record<string, unknown> {
|
|
3268
|
+
switch (input.eventType) {
|
|
3269
|
+
case "notice_given":
|
|
3270
|
+
return compactObject({
|
|
3271
|
+
notice: requireChangeEventField(input.notice, input.eventType, "notice"),
|
|
3272
|
+
noticeTo: input.noticeTo,
|
|
3273
|
+
effectiveAt: input.effectiveAt
|
|
3274
|
+
});
|
|
3275
|
+
case "veto_recorded":
|
|
3276
|
+
return compactObject({
|
|
3277
|
+
vetoByRole: requireChangeEventField(input.vetoByRole, input.eventType, "vetoByRole"),
|
|
3278
|
+
reason: input.reason,
|
|
3279
|
+
summary: input.summary
|
|
3280
|
+
});
|
|
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
|
+
case "decision_recorded":
|
|
3288
|
+
return compactObject({
|
|
3289
|
+
decision: requireChangeEventField(input.decision, input.eventType, "decision"),
|
|
3290
|
+
reason: input.reason,
|
|
3291
|
+
summary: input.summary
|
|
3292
|
+
});
|
|
3293
|
+
case "result_reported":
|
|
3294
|
+
return compactObject({
|
|
3295
|
+
result: requireChangeEventField(input.result, input.eventType, "result"),
|
|
3296
|
+
summary: input.summary
|
|
3297
|
+
});
|
|
3298
|
+
case "recovery_recorded":
|
|
3299
|
+
return compactObject({
|
|
3300
|
+
summary: requireChangeEventField(input.summary, input.eventType, "summary")
|
|
3301
|
+
});
|
|
3302
|
+
case "plan_revised": {
|
|
3303
|
+
const event = compactObject({
|
|
3304
|
+
summary: input.summary,
|
|
3305
|
+
revisedChangePlan: input.revisedChangePlan,
|
|
3306
|
+
revisedExecutionSteps: input.revisedExecutionSteps,
|
|
3307
|
+
revisedRollbackPlan: input.revisedRollbackPlan,
|
|
3308
|
+
revisedRiskImpact: input.revisedRiskImpact,
|
|
3309
|
+
revisedPlannedFor: input.revisedPlannedFor
|
|
3310
|
+
});
|
|
3311
|
+
if (Object.keys(event).length === 0) {
|
|
3312
|
+
throw new Error("plan_revised requires summary or at least one revised plan field.");
|
|
3313
|
+
}
|
|
3314
|
+
return event;
|
|
3315
|
+
}
|
|
3316
|
+
case "note_added":
|
|
3317
|
+
return compactObject({
|
|
3318
|
+
note: requireChangeEventField(input.note, input.eventType, "note")
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
function requireChangeEventField<T>(value: T | undefined, eventType: string, fieldName: string): T {
|
|
3324
|
+
if (value === undefined) {
|
|
3325
|
+
throw new Error(`${eventType} requires ${fieldName}.`);
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
return value;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
function compactObject(input: Record<string, unknown>): Record<string, unknown> {
|
|
3332
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
async function updateExpectation(
|
|
3336
|
+
runtime: DxRuntime,
|
|
3337
|
+
input: {
|
|
3338
|
+
workspaceId: string;
|
|
3339
|
+
id: string;
|
|
3340
|
+
title?: string;
|
|
3341
|
+
statement?: string;
|
|
3342
|
+
successRecognition?: string;
|
|
3343
|
+
approvalState?: "draft" | "approved" | "not_approved" | "superseded";
|
|
3344
|
+
approvedBy?: string;
|
|
3345
|
+
approvedAt?: string;
|
|
3346
|
+
source?: string;
|
|
3347
|
+
revisionNote?: string;
|
|
3348
|
+
fields?: Record<string, unknown>;
|
|
3349
|
+
unsetFields?: string[];
|
|
3350
|
+
},
|
|
3351
|
+
actorId: string
|
|
3352
|
+
) {
|
|
3353
|
+
assertNoTypedFields(input.fields, "update_expectation", EXPECTATION_TYPED_FIELDS);
|
|
3354
|
+
assertNoObsoleteExpectationFields(input.fields, "update_expectation");
|
|
3355
|
+
|
|
3356
|
+
return updateRecord(
|
|
3357
|
+
runtime.db,
|
|
3358
|
+
{
|
|
3359
|
+
recordType: "expectations",
|
|
3360
|
+
workspaceId: input.workspaceId,
|
|
3361
|
+
id: input.id,
|
|
3362
|
+
title: input.title,
|
|
3363
|
+
summary: input.statement,
|
|
3364
|
+
fields: {
|
|
3365
|
+
...(input.fields ?? {}),
|
|
3366
|
+
...(input.statement !== undefined ? { statement: input.statement } : {}),
|
|
3367
|
+
...(input.successRecognition !== undefined ? { successRecognition: input.successRecognition } : {}),
|
|
3368
|
+
...(input.approvalState !== undefined ? { approvalState: input.approvalState } : {}),
|
|
3369
|
+
...(input.approvedBy !== undefined ? { approvedBy: input.approvedBy } : {}),
|
|
3370
|
+
...(input.approvedAt !== undefined ? { approvedAt: input.approvedAt } : {}),
|
|
3371
|
+
...(input.source !== undefined ? { source: input.source } : {})
|
|
3372
|
+
},
|
|
3373
|
+
unsetFields: input.unsetFields,
|
|
3374
|
+
allowManagedFields: true,
|
|
3375
|
+
revisionNote: input.revisionNote
|
|
3376
|
+
},
|
|
3377
|
+
actorId
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
async function updateRequirement(
|
|
3382
|
+
runtime: DxRuntime,
|
|
3383
|
+
input: {
|
|
3384
|
+
workspaceId: string;
|
|
3385
|
+
id: string;
|
|
3386
|
+
title?: string;
|
|
3387
|
+
statement?: string;
|
|
3388
|
+
acceptanceCriteria?: string[];
|
|
3389
|
+
priority?: "low" | "medium" | "high";
|
|
3390
|
+
status?: "draft" | "ready" | "approved" | "superseded";
|
|
3391
|
+
revisionNote?: string;
|
|
3392
|
+
fields?: Record<string, unknown>;
|
|
3393
|
+
unsetFields?: string[];
|
|
3394
|
+
},
|
|
3395
|
+
actorId: string
|
|
3396
|
+
) {
|
|
3397
|
+
assertNoTypedFields(input.fields, "update_requirement", REQUIREMENT_TYPED_FIELDS);
|
|
3398
|
+
|
|
3399
|
+
return updateRecord(
|
|
3400
|
+
runtime.db,
|
|
3401
|
+
{
|
|
3402
|
+
recordType: "requirements",
|
|
3403
|
+
workspaceId: input.workspaceId,
|
|
3404
|
+
id: input.id,
|
|
3405
|
+
title: input.title,
|
|
3406
|
+
summary: input.statement,
|
|
3407
|
+
fields: {
|
|
3408
|
+
...(input.fields ?? {}),
|
|
3409
|
+
...(input.statement !== undefined ? { statement: input.statement } : {}),
|
|
3410
|
+
...(input.acceptanceCriteria !== undefined ? { acceptanceCriteria: input.acceptanceCriteria } : {}),
|
|
3411
|
+
...(input.priority !== undefined ? { priority: input.priority } : {}),
|
|
3412
|
+
...(input.status !== undefined ? { status: input.status } : {})
|
|
3413
|
+
},
|
|
3414
|
+
unsetFields: input.unsetFields,
|
|
3415
|
+
allowManagedFields: true,
|
|
3416
|
+
revisionNote: input.revisionNote
|
|
3417
|
+
},
|
|
3418
|
+
actorId
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
async function linkDecisionInput(
|
|
3423
|
+
runtime: DxRuntime,
|
|
3424
|
+
input: {
|
|
3425
|
+
workspaceId: string;
|
|
3426
|
+
decisionId: string;
|
|
3427
|
+
inputRecordType: DecisionInputRecordType;
|
|
3428
|
+
inputId: string;
|
|
3429
|
+
},
|
|
3430
|
+
actorId: string
|
|
3431
|
+
) {
|
|
3432
|
+
if (input.inputRecordType === "decisions" && input.inputId === input.decisionId) {
|
|
3433
|
+
throw new Error("A Decision cannot be linked to itself as a decision input.");
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
const decision = await getRecord(runtime.db, "decisions", input.decisionId, {
|
|
3437
|
+
workspaceId: input.workspaceId
|
|
3438
|
+
});
|
|
3439
|
+
if (!decision) {
|
|
3440
|
+
throw new Error(`Decision not found: decisions/${input.decisionId}`);
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
const inputRecord = await getRecord(runtime.db, input.inputRecordType, input.inputId, {
|
|
3444
|
+
workspaceId: input.workspaceId
|
|
3445
|
+
});
|
|
3446
|
+
if (!inputRecord) {
|
|
3447
|
+
throw new Error(`Decision input not found: ${input.inputRecordType}/${input.inputId}`);
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
if (
|
|
3451
|
+
decision.links.some(
|
|
3452
|
+
(link) =>
|
|
3453
|
+
link.toType === input.inputRecordType &&
|
|
3454
|
+
link.toId === inputRecord._id &&
|
|
3455
|
+
link.relationship === "informed_by"
|
|
3456
|
+
)
|
|
3457
|
+
) {
|
|
3458
|
+
return decision;
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
return linkRecords(
|
|
3462
|
+
runtime.db,
|
|
3463
|
+
{
|
|
3464
|
+
workspaceId: input.workspaceId,
|
|
3465
|
+
fromType: "decisions",
|
|
3466
|
+
fromId: decision._id,
|
|
3467
|
+
toType: input.inputRecordType,
|
|
3468
|
+
toId: inputRecord._id,
|
|
3469
|
+
relationship: "informed_by"
|
|
3470
|
+
},
|
|
3471
|
+
actorId
|
|
3472
|
+
);
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
function assertNoReservedFields(fields: Record<string, unknown> | undefined, reservedFields: string[]): void {
|
|
3476
|
+
for (const reservedField of reservedFields) {
|
|
3477
|
+
if (fields && Object.hasOwn(fields, reservedField)) {
|
|
3478
|
+
throw new Error(
|
|
3479
|
+
`${reservedField} is a relationship input on this tool. Use the top-level ${reservedField} argument instead of fields.${reservedField}.`
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function assertNoTypedFields(
|
|
3486
|
+
fields: Record<string, unknown> | undefined,
|
|
3487
|
+
toolName: string,
|
|
3488
|
+
typedFields: string[]
|
|
3489
|
+
): void {
|
|
3490
|
+
for (const typedField of typedFields) {
|
|
3491
|
+
if (fields && Object.hasOwn(fields, typedField)) {
|
|
3492
|
+
throw new Error(
|
|
3493
|
+
`${typedField} is a typed input on ${toolName}. Use the top-level ${typedField} argument instead of fields.${typedField}.`
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
function assertNoTypedUnsetFields(unsetFields: string[] | undefined, toolName: string, typedFields: string[]): void {
|
|
3500
|
+
for (const unsetField of unsetFields ?? []) {
|
|
3501
|
+
if (typedFields.includes(unsetField)) {
|
|
3502
|
+
throw new Error(`${unsetField} is managed on ${toolName}. Use the typed inputs instead of unsetFields.${unsetField}.`);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
function assertNoDecisionInputFields(fields: Record<string, unknown> | undefined): void {
|
|
3508
|
+
for (const fieldName of DECISION_INPUT_FIELDS) {
|
|
3509
|
+
if (fields && Object.hasOwn(fields, fieldName)) {
|
|
3510
|
+
throw new Error(
|
|
3511
|
+
`${fieldName} is managed on decisions. Use link_decision_input to record decision inputs.`
|
|
3512
|
+
);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
function assertNoObsoleteExpectationFields(fields: Record<string, unknown> | undefined, toolName: string): void {
|
|
3518
|
+
for (const obsoleteField of OBSOLETE_EXPECTATION_FIELDS) {
|
|
3519
|
+
if (fields && Object.hasOwn(fields, obsoleteField)) {
|
|
3520
|
+
throw new Error(
|
|
3521
|
+
`${obsoleteField} is not part of the current Expectation model on ${toolName}. Use approvalState, approvedBy, or approvedAt where approval tracking is needed.`
|
|
3522
|
+
);
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
}
|