@deksden-com/dd-flow-cli 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +25 -9
- package/dist/build-info.json +5 -5
- package/dist/cli/help.js +45 -29
- package/dist/cli/run-cli.js +229 -49
- package/dist/domain/entity-ids.js +4 -4
- package/dist/domain/flow-contract.js +502 -36
- package/dist/domain/validation.js +34 -0
- package/dist/protocol/local-files.js +8 -6
- package/dist/schemas/code-stage-report.schema.json +197 -2
- package/dist/schemas/flow-contract.schema.json +126 -0
- package/dist/schemas/flow-run-index-v3.schema.json +203 -0
- package/dist/schemas/flow-run-index.schema.json +22 -2
- package/dist/schemas/flow-run.schema.json +36 -0
- package/dist/schemas/merge-stage-report.schema.json +213 -2
- package/dist/schemas/plan-stage-report.schema.json +156 -2
- package/dist/schemas/release-impact.schema.json +16 -0
- package/dist/services/audit.js +3 -3
- package/dist/services/branch-context.js +17 -5
- package/dist/services/canon.js +0 -1
- package/dist/services/cleanup.js +6 -6
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/compatibility-preflight.js +3 -75
- package/dist/services/dashboard.js +48 -11
- package/dist/services/engines.js +123 -13
- package/dist/services/hooks.js +6 -6
- package/dist/services/ids.js +40 -49
- package/dist/services/merge-queue.js +33 -26
- package/dist/services/merge-worker.js +2 -1
- package/dist/services/migrations.js +64 -0
- package/dist/services/plans.js +23 -16
- package/dist/services/projects.js +2 -2
- package/dist/services/prompts.js +322 -0
- package/dist/services/protocols.js +77 -46
- package/dist/services/run-projection.js +80 -0
- package/dist/services/runs.js +360 -22
- package/dist/services/schema-validation.js +35 -12
- package/dist/services/sessions.js +81 -3
- package/dist/services/status.js +32 -1
- package/dist/services/usage.js +233 -0
- package/dist/services/worktrees.js +24 -19
- package/dist/storage/database.js +223 -9
- package/package.json +1 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { Ajv } from "ajv/dist/ajv.js";
|
|
3
6
|
import { parse as parseYaml } from "yaml";
|
|
4
7
|
import { AppError } from "../shared/errors.js";
|
|
5
8
|
import { parseJsonObject } from "../shared/json.js";
|
|
9
|
+
let validateBundledFlowContract;
|
|
6
10
|
export const defaultFlowContract = {
|
|
7
11
|
id: "dd-flow-canonical-2026-05",
|
|
8
12
|
version: 1,
|
|
13
|
+
capabilities: [],
|
|
9
14
|
stages: {
|
|
10
15
|
registered: { terminal: false },
|
|
11
16
|
prime: { terminal: false },
|
|
@@ -59,7 +64,39 @@ export function loadProjectFlowContract(projectRoot) {
|
|
|
59
64
|
if (!contractPath) {
|
|
60
65
|
return defaultFlowContract;
|
|
61
66
|
}
|
|
62
|
-
|
|
67
|
+
try {
|
|
68
|
+
const value = readFlowContractObject(contractPath);
|
|
69
|
+
validateFlowContractStructure(value);
|
|
70
|
+
return normalizeFlowContract(value);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof AppError) {
|
|
74
|
+
throw new AppError(error.code, `${contractPath}: ${error.message}`, error.exitCode, { ...error.details, file: contractPath });
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function validateFlowContractStructure(value) {
|
|
80
|
+
const validate = validateBundledFlowContract ??= new Ajv({ allErrors: true, strict: false, validateFormats: false }).compile(JSON.parse(fs.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "schemas", "flow-contract.schema.json"), "utf8")));
|
|
81
|
+
if (validate(value))
|
|
82
|
+
return;
|
|
83
|
+
const errors = (validate.errors ?? []).map((error) => ({
|
|
84
|
+
path: ajvErrorPath(error),
|
|
85
|
+
message: error.message ?? "schema validation failed",
|
|
86
|
+
keyword: error.keyword
|
|
87
|
+
}));
|
|
88
|
+
const first = errors[0] ?? { path: "/", message: "schema validation failed", keyword: "schema" };
|
|
89
|
+
throw new AppError("validation", `${first.path} ${first.message}`, 2, { path: first.path, errors });
|
|
90
|
+
}
|
|
91
|
+
function ajvErrorPath(error) {
|
|
92
|
+
const property = error.keyword === "required"
|
|
93
|
+
? error.params.missingProperty
|
|
94
|
+
: error.keyword === "additionalProperties"
|
|
95
|
+
? error.params.additionalProperty
|
|
96
|
+
: error.keyword === "uniqueItems"
|
|
97
|
+
? Math.max(Number(error.params.i), Number(error.params.j))
|
|
98
|
+
: undefined;
|
|
99
|
+
return property ? childPath(error.instancePath, String(property)) : error.instancePath || "/";
|
|
63
100
|
}
|
|
64
101
|
export function flowContractForState(state) {
|
|
65
102
|
if (!state.flow_contract) {
|
|
@@ -68,7 +105,7 @@ export function flowContractForState(state) {
|
|
|
68
105
|
if (typeof state.flow_contract !== "object" || Array.isArray(state.flow_contract)) {
|
|
69
106
|
return defaultFlowContract;
|
|
70
107
|
}
|
|
71
|
-
return normalizeFlowContract(state.flow_contract
|
|
108
|
+
return normalizeFlowContract(state.flow_contract);
|
|
72
109
|
}
|
|
73
110
|
export function normalizeStage(value, contract) {
|
|
74
111
|
return contract.legacy_aliases[value] ?? value;
|
|
@@ -95,16 +132,269 @@ export function canTransition(from, to, contract = defaultFlowContract) {
|
|
|
95
132
|
}
|
|
96
133
|
return contract.transitions[normalizedFrom]?.includes(normalizedTo) ?? false;
|
|
97
134
|
}
|
|
98
|
-
function
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
135
|
+
export function resolveFlowFlags(contract, input) {
|
|
136
|
+
const catalog = contract.flow_flags;
|
|
137
|
+
const now = input.now ?? new Date().toISOString();
|
|
138
|
+
if (!catalog) {
|
|
139
|
+
const values = {};
|
|
140
|
+
return {
|
|
141
|
+
contract: { id: contract.id, version: contract.version, capabilities: contract.capabilities },
|
|
142
|
+
flow_kind: input.flowKind,
|
|
143
|
+
preset: { requested: input.preset ?? null, applied: "none", source_ref: `${contract.id}#flow_flags.none` },
|
|
144
|
+
snapshot_revision: input.snapshotRevision ?? 1,
|
|
145
|
+
resolution_status: "valid",
|
|
146
|
+
resolved_at: now,
|
|
147
|
+
values,
|
|
148
|
+
compatibility_projection: { task_profile_revision: null, legacy_flow_profile_read: false },
|
|
149
|
+
snapshot_checksum: checksumForFlowFlagValues(values),
|
|
150
|
+
floors_applied: []
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const flow = catalog.flows[input.flowKind] ?? catalog.flows.custom;
|
|
154
|
+
if (!flow) {
|
|
155
|
+
throw new AppError("validation", `Flow kind has no flow-flag contract: ${input.flowKind}`, 2, {
|
|
156
|
+
flow_kind: input.flowKind,
|
|
157
|
+
supported: Object.keys(catalog.flows)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const defaultPreset = flow.default_preset ?? flow.defaults?.preset;
|
|
161
|
+
if (!defaultPreset || !catalog.presets[defaultPreset]) {
|
|
162
|
+
throw new AppError("validation", `Flow kind has no valid default preset: ${input.flowKind}`, 2);
|
|
163
|
+
}
|
|
164
|
+
const appliedPreset = input.preset ?? defaultPreset;
|
|
165
|
+
const presetValues = catalog.presets[appliedPreset];
|
|
166
|
+
if (!presetValues) {
|
|
167
|
+
throw new AppError("validation", `Unknown flow preset: ${appliedPreset}`, 2, {
|
|
168
|
+
preset: appliedPreset,
|
|
169
|
+
allowed: Object.keys(catalog.presets)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const supported = new Set(flow.supported);
|
|
173
|
+
const values = {};
|
|
174
|
+
const floorsApplied = [];
|
|
175
|
+
applyFlagMap(values, catalog, supported, catalog.presets[defaultPreset] ?? {}, "flow_default", `${contract.id}#flow_flags.flows.${input.flowKind}`, "flow default preset", now);
|
|
176
|
+
if (input.preset) {
|
|
177
|
+
applyFlagMap(values, catalog, supported, presetValues, "preset", `${contract.id}#flow_flags.presets.${appliedPreset}`, `preset ${appliedPreset}`, now);
|
|
178
|
+
}
|
|
179
|
+
const taskProfile = recordValue(input.taskProfile);
|
|
180
|
+
const profileFlags = taskProfileFlagMap(taskProfile);
|
|
181
|
+
const profileSource = taskProfile?.flow_flags || taskProfile?.flags ? "task_profile" : taskProfile?.flow_profile ? "compatibility_alias" : "task_profile";
|
|
182
|
+
applyFlagMap(values, catalog, supported, profileFlags, profileSource, "task_profile", "task profile override", now, true);
|
|
183
|
+
applyFlagMap(values, catalog, supported, input.protocolOverrides ?? {}, "protocol_override", "protocol_override", "protocol override", now, true, input.snapshotRevision ?? 1);
|
|
184
|
+
applyFlagMap(values, catalog, supported, input.runOverrides ?? {}, "run_override", "run_override", "RUN override", now, true, input.snapshotRevision ?? 1);
|
|
185
|
+
for (const key of supported) {
|
|
186
|
+
if (!values[key]) {
|
|
187
|
+
throw new AppError("validation", `Preset resolution is incomplete for flag: ${key}`, 2, {
|
|
188
|
+
flow_kind: input.flowKind,
|
|
189
|
+
preset: appliedPreset
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const facts = taskFacts(taskProfile);
|
|
194
|
+
for (const [floorIndex, floor] of (catalog.mandatory_floors ?? []).entries()) {
|
|
195
|
+
if (!floorMatches(floor.when, facts))
|
|
196
|
+
continue;
|
|
197
|
+
for (const [key, floorValue] of Object.entries(floor.values)) {
|
|
198
|
+
if (!supported.has(key))
|
|
199
|
+
continue;
|
|
200
|
+
const definition = catalog.definitions[key];
|
|
201
|
+
if (!definition)
|
|
202
|
+
continue;
|
|
203
|
+
validateFlagValue(key, definition, floorValue);
|
|
204
|
+
const current = values[key];
|
|
205
|
+
if (!current || flagStrength(key, current.value, definition) < flagStrength(key, floorValue, definition)) {
|
|
206
|
+
if (current && ["task_profile", "compatibility_alias", "protocol_override", "run_override"].includes(current.source.kind)) {
|
|
207
|
+
throw new AppError("validation", `Override is below mandatory floor for ${key}`, 2, {
|
|
208
|
+
flag: key,
|
|
209
|
+
requested: current.value,
|
|
210
|
+
floor: floorValue,
|
|
211
|
+
floor_index: floorIndex,
|
|
212
|
+
reason: floor.reason
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
values[key] = valueRecord(key, definition, floorValue, "escalation", `mandatory_floors[${floorIndex}]`, floor.reason, now, input.snapshotRevision ?? 1);
|
|
216
|
+
floorsApplied.push(`${key}=floor:${floorIndex}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const taskProfileRevision = numberValue(taskProfile?.revision);
|
|
221
|
+
return {
|
|
222
|
+
contract: { id: contract.id, version: contract.version, capabilities: contract.capabilities },
|
|
223
|
+
flow_kind: input.flowKind,
|
|
224
|
+
preset: {
|
|
225
|
+
requested: input.preset ?? null,
|
|
226
|
+
applied: appliedPreset,
|
|
227
|
+
source_ref: `${contract.id}#flow_flags.presets.${appliedPreset}`
|
|
228
|
+
},
|
|
229
|
+
snapshot_revision: input.snapshotRevision ?? 1,
|
|
230
|
+
resolution_status: "valid",
|
|
231
|
+
resolved_at: now,
|
|
232
|
+
values,
|
|
233
|
+
compatibility_projection: {
|
|
234
|
+
task_profile_revision: taskProfileRevision,
|
|
235
|
+
legacy_flow_profile_read: Boolean(taskProfile?.flow_profile)
|
|
236
|
+
},
|
|
237
|
+
snapshot_checksum: checksumForFlowFlagValues(values),
|
|
238
|
+
floors_applied: floorsApplied
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function applyFlagMap(target, catalog, supported, sourceMap, sourceKind, sourceRef, rationale, now, rejectUnknown = false, revision = null) {
|
|
242
|
+
for (const [key, value] of Object.entries(sourceMap)) {
|
|
243
|
+
if (!catalog.definitions[key]) {
|
|
244
|
+
if (rejectUnknown)
|
|
245
|
+
throw new AppError("validation", `Unknown flow flag: ${key}`, 2, { flag: key, source: sourceRef });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (!supported.has(key)) {
|
|
249
|
+
if (rejectUnknown)
|
|
250
|
+
throw new AppError("validation", `Flow flag is unsupported for this flow: ${key}`, 2, { flag: key });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const definition = catalog.definitions[key];
|
|
254
|
+
validateFlagValue(key, definition, value);
|
|
255
|
+
target[key] = valueRecord(key, definition, value, sourceKind, sourceRef, rationale, now, revision);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function valueRecord(key, definition, value, sourceKind, sourceRef, rationale, now, revision) {
|
|
259
|
+
return {
|
|
260
|
+
value: value,
|
|
261
|
+
value_type: definition.type,
|
|
262
|
+
owner: definition.owner,
|
|
263
|
+
source: { kind: sourceKind, ref: sourceRef, revision },
|
|
264
|
+
rationale: `${key}: ${rationale}`,
|
|
265
|
+
resolved_at: now
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function validateFlagValue(key, definition, value, errorPath) {
|
|
269
|
+
const validType = definition.type === "boolean"
|
|
270
|
+
? typeof value === "boolean"
|
|
271
|
+
: definition.type === "number"
|
|
272
|
+
? typeof value === "number" && Number.isFinite(value)
|
|
273
|
+
: definition.type === "string"
|
|
274
|
+
? typeof value === "string"
|
|
275
|
+
: typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null;
|
|
276
|
+
if (!validType || (definition.values && !definition.values.some((candidate) => candidate === value))) {
|
|
277
|
+
throw new AppError("validation", `Invalid value for flow flag: ${key}`, 2, {
|
|
278
|
+
...(errorPath ? { path: errorPath } : {}),
|
|
279
|
+
flag: key,
|
|
280
|
+
value,
|
|
281
|
+
expected: definition.values ?? definition.type
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function taskProfileFlagMap(profile) {
|
|
286
|
+
if (!profile)
|
|
287
|
+
return {};
|
|
288
|
+
const flags = recordValue(profile.flow_flags) ?? recordValue(profile.flags);
|
|
289
|
+
const result = { ...(flags ?? {}) };
|
|
290
|
+
const route = recordValue(profile.route);
|
|
291
|
+
if (route?.planning !== undefined)
|
|
292
|
+
result["planning.mode"] = route.planning;
|
|
293
|
+
const verification = recordValue(profile.verification);
|
|
294
|
+
if (verification?.depth !== undefined)
|
|
295
|
+
result["verification.depth"] = verification.depth;
|
|
296
|
+
if (verification?.mode !== undefined)
|
|
297
|
+
result["verification.depth"] = verification.mode;
|
|
298
|
+
const evidence = recordValue(profile.evidence);
|
|
299
|
+
if (evidence?.level !== undefined)
|
|
300
|
+
result["evidence.level"] = evidence.level;
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
function taskFacts(profile) {
|
|
304
|
+
const impact = recordValue(profile?.impact);
|
|
305
|
+
return {
|
|
306
|
+
...(profile ?? {}),
|
|
307
|
+
risk: profile?.risk ?? impact?.risk,
|
|
308
|
+
contract: profile?.contract ?? impact?.contract,
|
|
309
|
+
operations: profile?.operations ?? impact?.operations
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function floorMatches(when, facts) {
|
|
313
|
+
for (const [key, expected] of Object.entries(when)) {
|
|
314
|
+
if (key === "contract_not") {
|
|
315
|
+
if (expected.includes(facts.contract))
|
|
316
|
+
return false;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const actual = facts[key];
|
|
320
|
+
const expectedValues = Array.isArray(expected) ? expected : [expected];
|
|
321
|
+
if (!expectedValues.includes(actual))
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
function flagStrength(key, value, definition) {
|
|
327
|
+
if (typeof value === "boolean")
|
|
328
|
+
return value ? 1 : 0;
|
|
329
|
+
const ranks = {
|
|
330
|
+
"planning.mode": ["no_plan", "compact_plan", "full_plan"],
|
|
331
|
+
"plan.review.mode": ["self", "grouped", "focused", "mixed"],
|
|
332
|
+
"subagents.route": ["self_check", "grouped_subagent", "focused_subagent", "mixed"],
|
|
333
|
+
"subagents.grouping": ["off", "allowlisted"],
|
|
334
|
+
"subagents.pool_fallback": ["one", "runtime"],
|
|
335
|
+
"observability.detail": ["compact", "normal", "full"],
|
|
336
|
+
"knowledge.extract": ["skip", "conditional", "required"],
|
|
337
|
+
"knowledge.promote": ["skip", "conditional", "required"],
|
|
338
|
+
"workspace.bootstrap.mode": ["not_required", "revalidate", "required"],
|
|
339
|
+
"merge.ceremony": ["compact", "normal", "full"],
|
|
340
|
+
"merge.report_detail": ["compact", "normal", "full"],
|
|
341
|
+
"verification.depth": ["minimal", "standard", "full"],
|
|
342
|
+
"evidence.level": ["final_report", "protocol_record", "proof_bundle", "verification_passport", "rollout_evidence"]
|
|
343
|
+
};
|
|
344
|
+
return ranks[key]?.indexOf(String(value)) ?? definition.values?.indexOf(value) ?? 0;
|
|
345
|
+
}
|
|
346
|
+
export function isFlowFlagDowngrade(key, candidate, baseline) {
|
|
347
|
+
return flagStrength(key, candidate, { type: "string", owner: "flow_contract" })
|
|
348
|
+
< flagStrength(key, baseline, { type: "string", owner: "flow_contract" });
|
|
349
|
+
}
|
|
350
|
+
export function checksumForFlowFlagValues(values) {
|
|
351
|
+
return crypto.createHash("sha256").update(JSON.stringify(sortRecord(values))).digest("hex");
|
|
352
|
+
}
|
|
353
|
+
function sortRecord(value) {
|
|
354
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, value[key]]));
|
|
355
|
+
}
|
|
356
|
+
function recordValue(value) {
|
|
357
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
358
|
+
}
|
|
359
|
+
function numberValue(value) {
|
|
360
|
+
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
361
|
+
}
|
|
362
|
+
export function normalizeFlowContract(value) {
|
|
363
|
+
const id = requireString(value.id, "/id");
|
|
364
|
+
const version = requireNumber(value.version, "/version");
|
|
365
|
+
if (version > 3)
|
|
366
|
+
fail("/version", `unsupported flow-contract version: ${version}`);
|
|
367
|
+
if (version === 3 && !/^dd-flow-canonical-[0-9]{4}-[0-9]{2}$/.test(id)) {
|
|
368
|
+
fail("/id", "must identify a canonical version 3 flow contract");
|
|
369
|
+
}
|
|
370
|
+
if (value.flow_flags !== undefined && version !== 3)
|
|
371
|
+
fail("/version", "must be 3 when flow_flags is present");
|
|
372
|
+
const capabilities = normalizeStringArray(value.capabilities ?? [], "/capabilities");
|
|
373
|
+
const stages = normalizeStages(value.stages, "/stages");
|
|
374
|
+
const legacyAliases = normalizeStringMap(value.legacy_aliases ?? {}, "/legacy_aliases");
|
|
375
|
+
const transitions = normalizeTransitions(value.transitions, "/transitions", stages, legacyAliases);
|
|
376
|
+
const readiness = normalizeReadiness(value.readiness, "/readiness", stages, legacyAliases);
|
|
377
|
+
const mergeQueue = normalizeMergeQueue(value.merge_queue, "/merge_queue", stages, legacyAliases);
|
|
378
|
+
const defaults = normalizeDefaults(value.defaults, "/defaults");
|
|
379
|
+
const flowFlags = value.flow_flags === undefined ? undefined : normalizeFlowFlags(value.flow_flags, "/flow_flags");
|
|
380
|
+
const entrypoints = recordValue(value.entrypoints);
|
|
381
|
+
const autoPolicy = recordValue(value.auto_policy);
|
|
382
|
+
const routeAliases = value.route_aliases === undefined ? undefined : normalizeStringMap(value.route_aliases, "/route_aliases");
|
|
383
|
+
return {
|
|
384
|
+
id,
|
|
385
|
+
version,
|
|
386
|
+
capabilities,
|
|
387
|
+
stages,
|
|
388
|
+
transitions,
|
|
389
|
+
readiness,
|
|
390
|
+
merge_queue: mergeQueue,
|
|
391
|
+
defaults,
|
|
392
|
+
legacy_aliases: legacyAliases,
|
|
393
|
+
...(entrypoints ? { entrypoints } : {}),
|
|
394
|
+
...(autoPolicy ? { auto_policy: autoPolicy } : {}),
|
|
395
|
+
...(routeAliases ? { route_aliases: routeAliases } : {}),
|
|
396
|
+
...(flowFlags ? { flow_flags: flowFlags } : {})
|
|
397
|
+
};
|
|
108
398
|
}
|
|
109
399
|
function findProjectFlowContractPath(projectRoot) {
|
|
110
400
|
const basePath = path.join(projectRoot, ".memory-bank", "dd-flow", "flow-contract");
|
|
@@ -129,35 +419,39 @@ function readFlowContractObject(file) {
|
|
|
129
419
|
}
|
|
130
420
|
function normalizeStages(value, label) {
|
|
131
421
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
132
|
-
|
|
422
|
+
fail(label, "must be an object");
|
|
133
423
|
}
|
|
134
424
|
const stages = {};
|
|
135
425
|
for (const [stage, config] of Object.entries(value)) {
|
|
426
|
+
const stagePath = childPath(label, stage);
|
|
136
427
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
137
|
-
|
|
428
|
+
fail(stagePath, "must be an object");
|
|
138
429
|
}
|
|
139
430
|
const terminal = config.terminal;
|
|
140
|
-
|
|
431
|
+
if (typeof terminal !== "boolean")
|
|
432
|
+
fail(childPath(stagePath, "terminal"), "must be a boolean");
|
|
433
|
+
stages[stage] = { terminal };
|
|
141
434
|
}
|
|
142
435
|
if (!stages.registered || !stages.closed || !stages.cancelled) {
|
|
143
|
-
|
|
436
|
+
fail(label, "must include registered, closed, and cancelled stages");
|
|
144
437
|
}
|
|
145
438
|
return stages;
|
|
146
439
|
}
|
|
147
440
|
function normalizeTransitions(value, label, stages, aliases) {
|
|
148
441
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
149
|
-
|
|
442
|
+
fail(label, "must be an object");
|
|
150
443
|
}
|
|
151
444
|
const transitions = {};
|
|
152
445
|
for (const [from, tos] of Object.entries(value)) {
|
|
446
|
+
const transitionPath = childPath(label, from);
|
|
153
447
|
const normalizedFrom = aliases[from] ?? from;
|
|
154
|
-
requireKnownStage(normalizedFrom, stages,
|
|
448
|
+
requireKnownStage(normalizedFrom, stages, transitionPath);
|
|
155
449
|
if (!Array.isArray(tos) || tos.some((to) => typeof to !== "string")) {
|
|
156
|
-
|
|
450
|
+
fail(transitionPath, "must be a string array");
|
|
157
451
|
}
|
|
158
|
-
transitions[normalizedFrom] = tos.map((to) => {
|
|
452
|
+
transitions[normalizedFrom] = tos.map((to, index) => {
|
|
159
453
|
const normalizedTo = aliases[to] ?? to;
|
|
160
|
-
requireKnownStage(normalizedTo, stages,
|
|
454
|
+
requireKnownStage(normalizedTo, stages, childPath(transitionPath, index));
|
|
161
455
|
return normalizedTo;
|
|
162
456
|
});
|
|
163
457
|
}
|
|
@@ -165,59 +459,225 @@ function normalizeTransitions(value, label, stages, aliases) {
|
|
|
165
459
|
}
|
|
166
460
|
function normalizeReadiness(value, label, stages, aliases) {
|
|
167
461
|
const object = requireObject(value, label);
|
|
168
|
-
const command = requireString(object.command,
|
|
462
|
+
const command = requireString(object.command, childPath(label, "command"));
|
|
169
463
|
if (command !== "ready-for-merge") {
|
|
170
|
-
|
|
464
|
+
fail(childPath(label, "command"), "must be ready-for-merge");
|
|
171
465
|
}
|
|
172
466
|
if (!Array.isArray(object.allowed_from) || object.allowed_from.some((stage) => typeof stage !== "string")) {
|
|
173
|
-
|
|
467
|
+
fail(childPath(label, "allowed_from"), "must be a string array");
|
|
174
468
|
}
|
|
175
|
-
const allowedFrom = object.allowed_from.map((stage) => normalizeKnownStage(stage, stages, aliases,
|
|
176
|
-
const
|
|
469
|
+
const allowedFrom = object.allowed_from.map((stage, index) => normalizeKnownStage(stage, stages, aliases, childPath(childPath(label, "allowed_from"), index)));
|
|
470
|
+
const targetStagePath = childPath(label, "target_stage");
|
|
471
|
+
const targetStage = normalizeKnownStage(requireString(object.target_stage, targetStagePath), stages, aliases, targetStagePath);
|
|
177
472
|
return { command, allowed_from: allowedFrom, target_stage: targetStage };
|
|
178
473
|
}
|
|
179
474
|
function normalizeMergeQueue(value, label, stages, aliases) {
|
|
180
475
|
const object = requireObject(value, label);
|
|
181
|
-
const
|
|
476
|
+
const completePath = childPath(label, "complete");
|
|
477
|
+
const complete = requireObject(object.complete, completePath);
|
|
478
|
+
const targetStagePath = childPath(completePath, "target_stage");
|
|
182
479
|
return {
|
|
183
480
|
complete: {
|
|
184
|
-
target_stage: normalizeKnownStage(requireString(complete.target_stage,
|
|
185
|
-
next_action: requireString(complete.next_action,
|
|
481
|
+
target_stage: normalizeKnownStage(requireString(complete.target_stage, targetStagePath), stages, aliases, targetStagePath),
|
|
482
|
+
next_action: requireString(complete.next_action, childPath(completePath, "next_action"))
|
|
186
483
|
}
|
|
187
484
|
};
|
|
188
485
|
}
|
|
189
486
|
function normalizeDefaults(value, label) {
|
|
190
487
|
const object = requireObject(value, label);
|
|
191
|
-
return { registered_next_action: requireString(object.registered_next_action,
|
|
488
|
+
return { registered_next_action: requireString(object.registered_next_action, childPath(label, "registered_next_action")) };
|
|
192
489
|
}
|
|
193
490
|
function normalizeStringMap(value, label) {
|
|
194
491
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
195
|
-
|
|
492
|
+
fail(label, "must be an object");
|
|
196
493
|
}
|
|
197
494
|
const output = {};
|
|
198
495
|
for (const [key, mapValue] of Object.entries(value)) {
|
|
199
496
|
if (typeof mapValue !== "string" || mapValue.length === 0) {
|
|
200
|
-
|
|
497
|
+
fail(childPath(label, key), "must be a non-empty string");
|
|
201
498
|
}
|
|
202
499
|
output[key] = mapValue;
|
|
203
500
|
}
|
|
204
501
|
return output;
|
|
205
502
|
}
|
|
503
|
+
function normalizeStringArray(value, label) {
|
|
504
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
505
|
+
fail(label, "must be a string array");
|
|
506
|
+
}
|
|
507
|
+
return [...value];
|
|
508
|
+
}
|
|
509
|
+
function normalizeFlowFlags(value, label) {
|
|
510
|
+
const object = requireObject(value, label);
|
|
511
|
+
const definitionsPath = childPath(label, "definitions");
|
|
512
|
+
const definitionsObject = requireObject(object.definitions, definitionsPath);
|
|
513
|
+
const definitions = {};
|
|
514
|
+
for (const [key, rawDefinition] of Object.entries(definitionsObject)) {
|
|
515
|
+
const definitionPath = childPath(definitionsPath, key);
|
|
516
|
+
const definition = requireObject(rawDefinition, definitionPath);
|
|
517
|
+
const typePath = childPath(definitionPath, "type");
|
|
518
|
+
const type = requireString(definition.type, typePath);
|
|
519
|
+
if (!["boolean", "enum", "number", "string"].includes(type)) {
|
|
520
|
+
fail(typePath, "is unsupported");
|
|
521
|
+
}
|
|
522
|
+
const rawValues = definition.values;
|
|
523
|
+
if (rawValues !== undefined && (!Array.isArray(rawValues) || rawValues.some((item) => !isFlagScalar(item)))) {
|
|
524
|
+
fail(childPath(definitionPath, "values"), "must contain scalar values");
|
|
525
|
+
}
|
|
526
|
+
definitions[key] = {
|
|
527
|
+
type,
|
|
528
|
+
...(rawValues ? { values: rawValues } : {}),
|
|
529
|
+
owner: requireString(definition.owner, childPath(definitionPath, "owner")),
|
|
530
|
+
...(typeof definition.description === "string" ? { description: definition.description } : {})
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
const presetsPath = childPath(label, "presets");
|
|
534
|
+
const presetsObject = requireObject(object.presets, presetsPath);
|
|
535
|
+
const presets = {};
|
|
536
|
+
for (const [name, rawPreset] of Object.entries(presetsObject)) {
|
|
537
|
+
const presetPath = childPath(presetsPath, name);
|
|
538
|
+
const preset = normalizeFlagMap(rawPreset, presetPath);
|
|
539
|
+
for (const [key, item] of Object.entries(preset)) {
|
|
540
|
+
const definition = definitions[key];
|
|
541
|
+
if (!definition)
|
|
542
|
+
fail(childPath(presetPath, key), `references unknown flow flag: ${key}`);
|
|
543
|
+
validateFlagValue(key, definition, item, childPath(presetPath, key));
|
|
544
|
+
}
|
|
545
|
+
presets[name] = preset;
|
|
546
|
+
}
|
|
547
|
+
const flowsPath = childPath(label, "flows");
|
|
548
|
+
const flowsObject = requireObject(object.flows, flowsPath);
|
|
549
|
+
const flows = {};
|
|
550
|
+
for (const [name, rawFlow] of Object.entries(flowsObject)) {
|
|
551
|
+
const flowPath = childPath(flowsPath, name);
|
|
552
|
+
const flow = requireObject(rawFlow, flowPath);
|
|
553
|
+
const flowDefaults = recordValue(flow.defaults);
|
|
554
|
+
const defaultPresetPath = childPath(flowPath, "default_preset");
|
|
555
|
+
const defaultPreset = requireString(flow.default_preset, defaultPresetPath);
|
|
556
|
+
const supportedPath = childPath(flowPath, "supported");
|
|
557
|
+
const supported = normalizeStringArray(flow.supported, supportedPath);
|
|
558
|
+
const duplicateIndex = supported.findIndex((key, index) => supported.indexOf(key) !== index);
|
|
559
|
+
if (duplicateIndex >= 0)
|
|
560
|
+
fail(childPath(supportedPath, duplicateIndex), `duplicates flow flag: ${supported[duplicateIndex]}`);
|
|
561
|
+
supported.forEach((key, index) => {
|
|
562
|
+
if (!definitions[key])
|
|
563
|
+
fail(childPath(supportedPath, index), `references unknown flow flag: ${key}`);
|
|
564
|
+
});
|
|
565
|
+
if (!presets[defaultPreset])
|
|
566
|
+
fail(defaultPresetPath, `references unknown preset: ${defaultPreset}`);
|
|
567
|
+
const legacyDefault = flowDefaults?.preset;
|
|
568
|
+
if (typeof legacyDefault === "string" && legacyDefault !== defaultPreset) {
|
|
569
|
+
fail(childPath(childPath(flowPath, "defaults"), "preset"), "must match default_preset");
|
|
570
|
+
}
|
|
571
|
+
for (const [presetName, preset] of Object.entries(presets)) {
|
|
572
|
+
for (const key of supported) {
|
|
573
|
+
if (!(key in preset))
|
|
574
|
+
fail(childPath(childPath(presetsPath, presetName), key), `is required by flow: ${name}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
flows[name] = {
|
|
578
|
+
default_preset: defaultPreset,
|
|
579
|
+
supported,
|
|
580
|
+
...(typeof flowDefaults?.preset === "string" ? { defaults: { preset: flowDefaults.preset } } : {})
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
const floorsValue = object.mandatory_floors ?? [];
|
|
584
|
+
const floorsPath = childPath(label, "mandatory_floors");
|
|
585
|
+
if (!Array.isArray(floorsValue))
|
|
586
|
+
fail(floorsPath, "must be an array");
|
|
587
|
+
const mandatory_floors = floorsValue.map((rawFloor, index) => {
|
|
588
|
+
const floorPath = childPath(floorsPath, index);
|
|
589
|
+
const floor = requireObject(rawFloor, floorPath);
|
|
590
|
+
const valuesPath = childPath(floorPath, "values");
|
|
591
|
+
const values = normalizeFlagMap(floor.values, valuesPath);
|
|
592
|
+
for (const [key, item] of Object.entries(values)) {
|
|
593
|
+
const definition = definitions[key];
|
|
594
|
+
if (!definition)
|
|
595
|
+
fail(childPath(valuesPath, key), `references unknown flow flag: ${key}`);
|
|
596
|
+
validateFlagValue(key, definition, item, childPath(valuesPath, key));
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
when: requireObject(floor.when, childPath(floorPath, "when")),
|
|
600
|
+
values,
|
|
601
|
+
reason: requireString(floor.reason, childPath(floorPath, "reason"))
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
const precedencePath = childPath(label, "source_precedence");
|
|
605
|
+
const precedence = normalizeSourcePrecedence(object.source_precedence, precedencePath);
|
|
606
|
+
const reductionMatrix = recordValue(object.reduction_matrix);
|
|
607
|
+
const consumerMatrix = recordValue(object.consumer_matrix);
|
|
608
|
+
if (reductionMatrix) {
|
|
609
|
+
for (const preset of Object.keys(reductionMatrix)) {
|
|
610
|
+
if (!presets[preset])
|
|
611
|
+
fail(childPath(childPath(label, "reduction_matrix"), preset), `references unknown preset: ${preset}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (consumerMatrix) {
|
|
615
|
+
for (const key of Object.keys(consumerMatrix)) {
|
|
616
|
+
if (!definitions[key])
|
|
617
|
+
fail(childPath(childPath(label, "consumer_matrix"), key), `references unknown flow flag: ${key}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
source_precedence: precedence,
|
|
622
|
+
definitions,
|
|
623
|
+
presets,
|
|
624
|
+
flows,
|
|
625
|
+
mandatory_floors,
|
|
626
|
+
...(reductionMatrix ? { reduction_matrix: reductionMatrix } : {}),
|
|
627
|
+
...(consumerMatrix ? { consumer_matrix: Object.fromEntries(Object.entries(consumerMatrix).map(([key, items]) => [key, normalizeStringArray(items, childPath(childPath(label, "consumer_matrix"), key))])) } : {})
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function normalizeSourcePrecedence(value, label) {
|
|
631
|
+
const supported = ["flow_default", "preset", "task_profile", "protocol_override", "run_override", "escalation", "compatibility_alias"];
|
|
632
|
+
const required = ["flow_default", "preset", "task_profile", "protocol_override", "run_override"];
|
|
633
|
+
const items = normalizeStringArray(value ?? required, label);
|
|
634
|
+
const result = [];
|
|
635
|
+
items.forEach((item, index) => {
|
|
636
|
+
if (!supported.includes(item))
|
|
637
|
+
fail(childPath(label, index), `contains unsupported source kind: ${item}`);
|
|
638
|
+
if (items.indexOf(item) !== index)
|
|
639
|
+
fail(childPath(label, index), `duplicates source kind: ${item}`);
|
|
640
|
+
result.push(item);
|
|
641
|
+
});
|
|
642
|
+
let previous = -1;
|
|
643
|
+
for (const source of required) {
|
|
644
|
+
const index = result.indexOf(source);
|
|
645
|
+
if (index < 0)
|
|
646
|
+
fail(label, `must include source kind: ${source}`);
|
|
647
|
+
if (index < previous)
|
|
648
|
+
fail(childPath(label, index), `must preserve mandatory source order at: ${source}`);
|
|
649
|
+
previous = index;
|
|
650
|
+
}
|
|
651
|
+
return result;
|
|
652
|
+
}
|
|
653
|
+
function normalizeFlagMap(value, label) {
|
|
654
|
+
const object = requireObject(value, label);
|
|
655
|
+
const result = {};
|
|
656
|
+
for (const [key, item] of Object.entries(object)) {
|
|
657
|
+
if (!isFlagScalar(item))
|
|
658
|
+
fail(childPath(label, key), "must be a scalar");
|
|
659
|
+
result[key] = item;
|
|
660
|
+
}
|
|
661
|
+
return result;
|
|
662
|
+
}
|
|
663
|
+
function isFlagScalar(value) {
|
|
664
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
665
|
+
}
|
|
206
666
|
function requireObject(value, label) {
|
|
207
667
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
208
|
-
|
|
668
|
+
fail(label, "must be an object");
|
|
209
669
|
}
|
|
210
670
|
return value;
|
|
211
671
|
}
|
|
212
672
|
function requireString(value, label) {
|
|
213
673
|
if (typeof value !== "string" || value.length === 0) {
|
|
214
|
-
|
|
674
|
+
fail(label, "must be a non-empty string");
|
|
215
675
|
}
|
|
216
676
|
return value;
|
|
217
677
|
}
|
|
218
678
|
function requireNumber(value, label) {
|
|
219
679
|
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
220
|
-
|
|
680
|
+
fail(label, "must be a positive integer");
|
|
221
681
|
}
|
|
222
682
|
return value;
|
|
223
683
|
}
|
|
@@ -228,6 +688,12 @@ function normalizeKnownStage(value, stages, aliases, label) {
|
|
|
228
688
|
}
|
|
229
689
|
function requireKnownStage(value, stages, label) {
|
|
230
690
|
if (!Object.prototype.hasOwnProperty.call(stages, value)) {
|
|
231
|
-
|
|
691
|
+
fail(label, `references unknown stage: ${value}`);
|
|
232
692
|
}
|
|
233
693
|
}
|
|
694
|
+
function childPath(parent, segment) {
|
|
695
|
+
return `${parent}/${String(segment).replaceAll("~", "~0").replaceAll("/", "~1")}`;
|
|
696
|
+
}
|
|
697
|
+
function fail(errorPath, message) {
|
|
698
|
+
throw new AppError("validation", `${errorPath} ${message}`, 2, { path: errorPath });
|
|
699
|
+
}
|