@deksden-com/dd-flow-cli 0.6.0 → 0.8.0-beta.135
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 +636 -0
- package/README.md +13 -1
- package/dist/build-info.json +10 -10
- package/dist/cli/help.js +108 -18
- package/dist/cli/run-cli.js +626 -49
- package/dist/domain/flow-contract.js +11 -0
- package/dist/domain/stage-catalog.js +22 -0
- package/dist/runtime/context.js +8 -2
- package/dist/schemas/code-review-decision.schema.json +26 -0
- package/dist/schemas/code-review-result.schema.json +14 -0
- package/dist/schemas/code-stage-report.schema.json +7 -2
- package/dist/schemas/code-verification.schema.json +14 -0
- package/dist/schemas/code-work-batch.schema.json +24 -0
- package/dist/schemas/code-work-result.schema.json +16 -0
- package/dist/schemas/engine-manifest.schema.json +22 -0
- package/dist/schemas/flow-contract.schema.json +6 -3
- package/dist/schemas/flow-run.schema.json +16 -122
- package/dist/schemas/mb-upgrade-migration-report.schema.json +3 -1
- package/dist/schemas/merge-stage-report-legacy-0.4.2.schema.json +24 -0
- package/dist/schemas/plan-aspect-map.schema.json +22 -0
- package/dist/schemas/plan-review-decision.schema.json +14 -0
- package/dist/schemas/plan-review-result.schema.json +42 -0
- package/dist/schemas/protocol-plan.schema.json +15 -182
- package/dist/schemas/run-engine-binding.schema.json +37 -0
- package/dist/schemas/stage-finish-input.schema.json +16 -2
- package/dist/schemas/stage-prompt.schema.json +4 -4
- package/dist/schemas/stage-report.schema.json +8 -7
- package/dist/schemas/vnext-protocol-plan.schema.json +37 -0
- package/dist/schemas/vnext-protocolize-result.schema.json +29 -0
- package/dist/schemas/vnext-specify.schema.json +45 -0
- package/dist/services/branch-context.js +1 -1
- package/dist/services/canon.js +15 -1
- package/dist/services/cleanup.js +8 -8
- package/dist/services/cli-operation-classifier.js +60 -8
- package/dist/services/code-checks.js +244 -0
- package/dist/services/compatibility-preflight.js +1 -1
- package/dist/services/config.js +7 -1
- package/dist/services/dashboard.js +14 -14
- package/dist/services/engines.js +408 -30
- package/dist/services/eval-snapshots.js +404 -0
- package/dist/services/hooks.js +775 -23
- package/dist/services/ids.js +16 -6
- package/dist/services/lanes.js +1 -5
- package/dist/services/merge-queue.js +53 -5
- package/dist/services/merge-worker.js +5 -6
- package/dist/services/migrations.js +307 -44
- package/dist/services/plan-runtime.js +5 -5
- package/dist/services/plans.js +5 -3
- package/dist/services/projects.js +4 -4
- package/dist/services/prompts.js +1 -1
- package/dist/services/protocols.js +31 -10
- package/dist/services/run-engine-bindings.js +157 -0
- package/dist/services/run-projection.js +49 -13
- package/dist/services/runs.js +525 -58
- package/dist/services/schema-validation.js +116 -2
- package/dist/services/sessions.js +51 -12
- package/dist/services/stage-blocker.js +57 -0
- package/dist/services/stage-context.js +90 -0
- package/dist/services/stage-lifecycle.js +288 -77
- package/dist/services/stage-pause.js +175 -0
- package/dist/services/stage-report-renderer.js +65 -0
- package/dist/services/status.js +8 -3
- package/dist/services/usage.js +526 -18
- package/dist/services/vnext-code-review.js +308 -0
- package/dist/services/vnext-code.js +616 -0
- package/dist/services/vnext-contracts.js +1 -0
- package/dist/services/vnext-execution-profile.js +27 -0
- package/dist/services/vnext-fanout.js +79 -0
- package/dist/services/vnext-plan-review.js +499 -0
- package/dist/services/vnext-plan.js +576 -0
- package/dist/services/vnext-protocolize.js +542 -0
- package/dist/services/vnext-specify.js +595 -0
- package/dist/services/vnext-workspace-policy.js +87 -0
- package/dist/services/work-registry.js +499 -0
- package/dist/services/worktrees.js +58 -37
- package/dist/storage/database.js +292 -42
- package/dist/storage/paths.js +47 -1
- package/package.json +12 -12
package/dist/services/engines.js
CHANGED
|
@@ -9,6 +9,10 @@ import { AppError } from "../shared/errors.js";
|
|
|
9
9
|
import { ensureDir, engineStoreRoot, engineVersionRoot, resolveProjectRoot } from "../storage/paths.js";
|
|
10
10
|
import { classifyCliOperation } from "./cli-operation-classifier.js";
|
|
11
11
|
import { requireProtocol } from "./protocols.js";
|
|
12
|
+
import { resolveCanonRootBeforeContext } from "./canon.js";
|
|
13
|
+
import { appendAudit } from "./audit.js";
|
|
14
|
+
import { findProjectByRoot } from "./projects.js";
|
|
15
|
+
import { allRunEngineBindings, findRunHome, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
|
|
12
16
|
export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
|
|
13
17
|
const routerNativeFamilies = new Set(["engine", "version", "schema"]);
|
|
14
18
|
const sourceFile = fileURLToPath(import.meta.url);
|
|
@@ -24,7 +28,7 @@ export function isRouterNativeCommand(args) {
|
|
|
24
28
|
return true;
|
|
25
29
|
if (args.includes("--help") || args.includes("-h"))
|
|
26
30
|
return true;
|
|
27
|
-
return routerNativeFamilies.has(first);
|
|
31
|
+
return routerNativeFamilies.has(first) || first === "codex" && args[1] === "hook" && args[2] === "handle";
|
|
28
32
|
}
|
|
29
33
|
export function installCurrentEngine(context, input = {}) {
|
|
30
34
|
const build = getCliBuildInfo();
|
|
@@ -63,24 +67,48 @@ export function installCurrentEngine(context, input = {}) {
|
|
|
63
67
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
64
68
|
ensureDir(tmp);
|
|
65
69
|
copyPackageSnapshot(packageRoot, tmp);
|
|
66
|
-
const
|
|
70
|
+
const stagedManifest = buildManifest(context, build.package_name, build.version, packageRoot, target, tmp);
|
|
71
|
+
const stagedChecksum = stagedManifest.integrity.checksum;
|
|
72
|
+
const activationTarget = input.force
|
|
73
|
+
&& existing
|
|
74
|
+
&& stagedChecksum
|
|
75
|
+
&& stagedChecksum !== existing.integrity.checksum
|
|
76
|
+
&& engineIsBound(context.ddFlowHome, existing)
|
|
77
|
+
? `${target}--${stagedChecksum}`
|
|
78
|
+
: target;
|
|
79
|
+
const duplicate = stagedChecksum ? findInstalledManifest(context.ddFlowHome, {
|
|
80
|
+
packageName: build.package_name,
|
|
81
|
+
packageVersion: build.version,
|
|
82
|
+
integrityChecksum: stagedChecksum
|
|
83
|
+
}) : null;
|
|
84
|
+
if (duplicate) {
|
|
85
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
action: "engine_install",
|
|
89
|
+
changed: false,
|
|
90
|
+
...(input.forRouting ? {} : { engine: manifestSummary(duplicate) }),
|
|
91
|
+
path: duplicate.snapshot_root
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const manifest = { ...stagedManifest, package_root: activationTarget, snapshot_root: activationTarget };
|
|
67
95
|
fs.writeFileSync(path.join(tmp, "engine.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
68
|
-
const
|
|
69
|
-
const stagedDiagnostics = manifestDiagnostics({ ...
|
|
96
|
+
const checkedManifest = requireManifest(path.join(tmp, "engine.json"));
|
|
97
|
+
const stagedDiagnostics = manifestDiagnostics({ ...checkedManifest, package_root: tmp, snapshot_root: tmp }, { checkEntrypoint: !input.forRouting });
|
|
70
98
|
if (stagedDiagnostics.length > 0) {
|
|
71
99
|
throw new AppError("engine_install_failed", "Staged dd-flow engine failed its integrity or dependency health check", 1, {
|
|
72
100
|
diagnostics: stagedDiagnostics,
|
|
73
101
|
path: tmp
|
|
74
102
|
});
|
|
75
103
|
}
|
|
76
|
-
activateEngineAtomically(tmp,
|
|
77
|
-
const installed = requireManifest(path.join(
|
|
104
|
+
activateEngineAtomically(tmp, activationTarget);
|
|
105
|
+
const installed = requireManifest(path.join(activationTarget, "engine.json"));
|
|
78
106
|
return {
|
|
79
107
|
ok: true,
|
|
80
108
|
action: "engine_install",
|
|
81
109
|
changed: true,
|
|
82
110
|
...(input.forRouting ? {} : { engine: manifestSummary(installed) }),
|
|
83
|
-
path:
|
|
111
|
+
path: activationTarget
|
|
84
112
|
};
|
|
85
113
|
}
|
|
86
114
|
finally {
|
|
@@ -100,15 +128,14 @@ export function engineInfo(context, input) {
|
|
|
100
128
|
return { ok: true, schema_id: "dd-flow/engine-info@1", engine: manifest };
|
|
101
129
|
}
|
|
102
130
|
export function resolveEngine(context, input = {}) {
|
|
103
|
-
|
|
104
|
-
const selection = selectEngine(context, input);
|
|
105
|
-
if (selection.status
|
|
106
|
-
return { ok: false, error: missingEngineError(selection), selection, exit_code: 1 };
|
|
131
|
+
const classification = input.classification ?? classifyCliOperation(["engine", "resolve"], input.env ?? context.env);
|
|
132
|
+
const selection = selectEngine(context, input, {}, classification);
|
|
133
|
+
if (selection.status !== "selected") {
|
|
134
|
+
return { ok: false, error: missingEngineError(selection, classification), selection, contexts: compatibilityContexts(context, { ...input, classification }), exit_code: 1 };
|
|
107
135
|
}
|
|
108
|
-
return { ok: true, schema_id: "dd-flow/engine-resolve@1", selection };
|
|
136
|
+
return { ok: true, schema_id: "dd-flow/engine-resolve@1", selection, contexts: compatibilityContexts(context, { ...input, classification }) };
|
|
109
137
|
}
|
|
110
138
|
export function doctorEngines(context, input = {}) {
|
|
111
|
-
installCurrentEngine(context);
|
|
112
139
|
const root = engineStoreRoot(context.ddFlowHome);
|
|
113
140
|
const installed = readInstalledManifests(root);
|
|
114
141
|
const checks = installed.map((manifest) => ({
|
|
@@ -116,24 +143,30 @@ export function doctorEngines(context, input = {}) {
|
|
|
116
143
|
status: manifestHealthy(manifest) ? "ok" : "corrupt",
|
|
117
144
|
diagnostics: manifestDiagnostics(manifest)
|
|
118
145
|
}));
|
|
119
|
-
const
|
|
146
|
+
const classification = input.classification ?? classifyCliOperation(["engine", "doctor"], input.env ?? context.env);
|
|
147
|
+
const selection = selectEngine(context, input, {}, classification);
|
|
120
148
|
return {
|
|
121
|
-
ok: checks.every((check) => check.status === "ok") && selection.status
|
|
149
|
+
ok: checks.every((check) => check.status === "ok") && selection.status === "selected",
|
|
122
150
|
schema_id: "dd-flow/engine-doctor@1",
|
|
123
151
|
engine_store: root,
|
|
124
152
|
checks,
|
|
125
153
|
selection,
|
|
126
|
-
|
|
154
|
+
contexts: compatibilityContexts(context, { ...input, classification }),
|
|
155
|
+
exit_code: checks.every((check) => check.status === "ok") && selection.status === "selected" ? 0 : 1
|
|
127
156
|
};
|
|
128
157
|
}
|
|
129
158
|
export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedProjectRoot) {
|
|
130
159
|
if (isEngineMode(env) || isRouterNativeCommand(args))
|
|
131
160
|
return Promise.resolve(null);
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
const
|
|
136
|
-
|
|
161
|
+
const classification = classifyCliOperation(args, env);
|
|
162
|
+
const bound = boundRunEngineForArgs(context, args, resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined);
|
|
163
|
+
if (bound) {
|
|
164
|
+
const entrypoint = resolveManifestEntrypoint(bound.manifest);
|
|
165
|
+
return spawnEngine(bound.manifest, entrypoint, args, io, stdin, env, getCliBuildInfo().version);
|
|
166
|
+
}
|
|
167
|
+
const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined, env }, { allowCurrentInProcess: true }, classification);
|
|
168
|
+
if (selection.status !== "selected") {
|
|
169
|
+
if (classification.mode === "read_only_diagnostics" || (classification.mode === "mb_upgrade" && isUpgradeDiagnostic(args))) {
|
|
137
170
|
return Promise.resolve(null);
|
|
138
171
|
}
|
|
139
172
|
throw new AppError("missing_engine", "Required dd-flow engine is not installed", 1, missingEngineDetails(selection, classification));
|
|
@@ -147,6 +180,179 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
|
|
|
147
180
|
const entrypoint = resolveManifestEntrypoint(selection.selected);
|
|
148
181
|
return spawnEngine(selection.selected, entrypoint, args, io, stdin, env, current.version);
|
|
149
182
|
}
|
|
183
|
+
export function bindRunEngine(context, input) {
|
|
184
|
+
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
185
|
+
const located = findRunHome(context.ddFlowHome, projectRoot, input.runId);
|
|
186
|
+
if (!located)
|
|
187
|
+
throw new AppError("not_found", `RUN is not registered in router storage: ${input.runId}`, 1, { run_id: input.runId, project_root: projectRoot });
|
|
188
|
+
const existing = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
189
|
+
const manifest = requireBoundManifest(context.ddFlowHome, {
|
|
190
|
+
packageName: input.packageName ?? getCliBuildInfo().package_name,
|
|
191
|
+
packageVersion: input.engineVersion,
|
|
192
|
+
...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
|
|
193
|
+
});
|
|
194
|
+
const requested = engineIdentity(manifest);
|
|
195
|
+
if (existing) {
|
|
196
|
+
if (sameEngineIdentity(existing.engine, requested))
|
|
197
|
+
return { ok: true, action: "run_engine_bind", changed: false, binding: existing };
|
|
198
|
+
throw new AppError("run_engine_binding_immutable", "RUN engine binding cannot be changed", 1, {
|
|
199
|
+
run_id: located.run_id,
|
|
200
|
+
current: existing.engine,
|
|
201
|
+
requested
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
|
|
205
|
+
const probe = spawnSync(process.execPath, [resolveManifestEntrypoint(manifest), ...probeArgs], {
|
|
206
|
+
cwd: manifest.snapshot_root,
|
|
207
|
+
env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
|
|
208
|
+
encoding: "utf8",
|
|
209
|
+
timeout: 30_000
|
|
210
|
+
});
|
|
211
|
+
if (probe.status !== 0) {
|
|
212
|
+
throw new AppError("run_engine_probe_failed", "Selected engine cannot read the RUN authority", 1, {
|
|
213
|
+
run_id: located.run_id,
|
|
214
|
+
engine: manifestSummary(manifest),
|
|
215
|
+
command: `dd-flow ${probeArgs.join(" ")}`,
|
|
216
|
+
exit_code: probe.status,
|
|
217
|
+
stderr: probe.stderr.slice(0, 2000)
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const binding = {
|
|
221
|
+
schema_id: runEngineBindingSchemaId,
|
|
222
|
+
run_id: located.run_id,
|
|
223
|
+
project_root: projectRoot,
|
|
224
|
+
bound_at: context.now(),
|
|
225
|
+
source: "legacy_recovery",
|
|
226
|
+
reason: requiredText(input.reason, "reason"),
|
|
227
|
+
engine: requested,
|
|
228
|
+
probe: { status: "passed", command: `dd-flow ${probeArgs.join(" ")}` }
|
|
229
|
+
};
|
|
230
|
+
const written = writeRunEngineBinding(located.binding_path, binding);
|
|
231
|
+
const project = context.db.writable ? findProjectByRoot(context, projectRoot) : null;
|
|
232
|
+
if (project) {
|
|
233
|
+
appendAudit(context, {
|
|
234
|
+
projectId: project.id,
|
|
235
|
+
eventType: "flow_run.engine_bound",
|
|
236
|
+
payload: { run_id: located.run_id, source: binding.source, engine: binding.engine, probe: binding.probe }
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return { ok: true, action: "run_engine_bind", changed: written.changed, binding: written.binding };
|
|
240
|
+
}
|
|
241
|
+
export function bindCurrentEngineToRun(context, input) {
|
|
242
|
+
const manifest = executionManifest(context);
|
|
243
|
+
const binding = {
|
|
244
|
+
schema_id: runEngineBindingSchemaId,
|
|
245
|
+
run_id: input.runId,
|
|
246
|
+
project_root: resolveProjectRoot(input.projectRoot),
|
|
247
|
+
bound_at: context.now(),
|
|
248
|
+
source: "run_creation",
|
|
249
|
+
reason: "RUN created by the selected execution engine.",
|
|
250
|
+
engine: engineIdentity(manifest),
|
|
251
|
+
probe: { status: "not_required", command: null }
|
|
252
|
+
};
|
|
253
|
+
return writeRunEngineBinding(path.join(input.runHome, "engine-binding.json"), binding).binding;
|
|
254
|
+
}
|
|
255
|
+
function boundRunEngineForArgs(context, args, projectRoot) {
|
|
256
|
+
const runId = runIdForArgs(args);
|
|
257
|
+
if (!runId || !projectRoot)
|
|
258
|
+
return null;
|
|
259
|
+
const located = findRunHome(context.ddFlowHome, projectRoot, runId);
|
|
260
|
+
if (!located)
|
|
261
|
+
return null;
|
|
262
|
+
const binding = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
263
|
+
if (!binding)
|
|
264
|
+
return null;
|
|
265
|
+
const manifest = requireBoundManifest(context.ddFlowHome, {
|
|
266
|
+
packageName: binding.engine.package_name,
|
|
267
|
+
packageVersion: binding.engine.package_version,
|
|
268
|
+
integrityChecksum: binding.engine.integrity_checksum,
|
|
269
|
+
snapshotRoot: binding.engine.snapshot_root
|
|
270
|
+
});
|
|
271
|
+
return { binding, manifest };
|
|
272
|
+
}
|
|
273
|
+
function runIdForArgs(args) {
|
|
274
|
+
const [family, command, ...rest] = args;
|
|
275
|
+
const parsed = parseLightArgs(rest);
|
|
276
|
+
if (family === "run" && command !== "start" && command !== "list") {
|
|
277
|
+
if (command === "flags")
|
|
278
|
+
return positional(parsed, 1);
|
|
279
|
+
return positional(parsed, 0);
|
|
280
|
+
}
|
|
281
|
+
if (family === "stage" && command !== "start")
|
|
282
|
+
return positional(parsed, 0);
|
|
283
|
+
if (family === "stage" && command === "start" && !parsed.options.has("bootstrap"))
|
|
284
|
+
return positional(parsed, 0);
|
|
285
|
+
if (family === "prompt" && command === "render")
|
|
286
|
+
return option(parsed, "run");
|
|
287
|
+
if (family === "migration" && ["plan", "report", "verify", "apply"].includes(command ?? ""))
|
|
288
|
+
return option(parsed, "run");
|
|
289
|
+
if (family === "protocol" && command === "sync-from-run")
|
|
290
|
+
return option(parsed, "run");
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function requireBoundManifest(ddFlowHome, input) {
|
|
294
|
+
const manifest = input.snapshotRoot
|
|
295
|
+
? requireManifest(path.join(input.snapshotRoot, "engine.json"))
|
|
296
|
+
: findInstalledManifest(ddFlowHome, {
|
|
297
|
+
packageName: input.packageName,
|
|
298
|
+
packageVersion: input.packageVersion,
|
|
299
|
+
...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
|
|
300
|
+
});
|
|
301
|
+
if (!manifest
|
|
302
|
+
|| manifest.package_name !== input.packageName
|
|
303
|
+
|| manifest.package_version !== input.packageVersion
|
|
304
|
+
|| input.integrityChecksum !== undefined && manifest.integrity.checksum !== input.integrityChecksum
|
|
305
|
+
|| input.snapshotRoot !== undefined && path.resolve(manifest.snapshot_root) !== path.resolve(input.snapshotRoot)
|
|
306
|
+
|| !manifestHealthy(manifest)) {
|
|
307
|
+
throw new AppError("run_engine_binding_invalid", "RUN engine binding points to a missing or unhealthy engine snapshot", 1, {
|
|
308
|
+
package_name: input.packageName,
|
|
309
|
+
package_version: input.packageVersion,
|
|
310
|
+
snapshot_root: input.snapshotRoot ?? null,
|
|
311
|
+
integrity_checksum: input.integrityChecksum ?? null
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return manifest;
|
|
315
|
+
}
|
|
316
|
+
function engineIdentity(manifest) {
|
|
317
|
+
if (!manifest.integrity.checksum) {
|
|
318
|
+
throw new AppError("engine_integrity_missing", "Cannot bind a RUN to an engine without a content checksum", 1, {
|
|
319
|
+
engine: manifestSummary(manifest)
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
package_name: manifest.package_name,
|
|
324
|
+
package_version: manifest.package_version,
|
|
325
|
+
engine_version: manifest.engine_version,
|
|
326
|
+
integrity_checksum: manifest.integrity.checksum,
|
|
327
|
+
snapshot_root: path.resolve(manifest.snapshot_root)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function sameEngineIdentity(left, right) {
|
|
331
|
+
return left.package_name === right.package_name
|
|
332
|
+
&& left.package_version === right.package_version
|
|
333
|
+
&& left.engine_version === right.engine_version
|
|
334
|
+
&& left.integrity_checksum === right.integrity_checksum
|
|
335
|
+
&& path.resolve(left.snapshot_root) === path.resolve(right.snapshot_root);
|
|
336
|
+
}
|
|
337
|
+
function executionManifest(context) {
|
|
338
|
+
const home = context.env.DD_FLOW_ENGINE_HOME;
|
|
339
|
+
if (home && home !== "in-process")
|
|
340
|
+
return requireBoundManifest(context.ddFlowHome, { packageName: getCliBuildInfo().package_name, packageVersion: getCliBuildInfo().version, snapshotRoot: home });
|
|
341
|
+
// Refresh the same-semver snapshot when this build differs. The installer
|
|
342
|
+
// deduplicates identical checksums and keeps any bound snapshot immutable.
|
|
343
|
+
const installed = installCurrentEngine(context, { force: true, forRouting: true });
|
|
344
|
+
return requireManifest(path.join(String(installed.path), "engine.json"));
|
|
345
|
+
}
|
|
346
|
+
function requiredText(value, label) {
|
|
347
|
+
const normalized = value.trim();
|
|
348
|
+
if (!normalized)
|
|
349
|
+
throw new AppError("usage", `Missing required value: ${label}`, 2);
|
|
350
|
+
return normalized;
|
|
351
|
+
}
|
|
352
|
+
function engineIsBound(ddFlowHome, manifest) {
|
|
353
|
+
const snapshotRoot = path.resolve(manifest.snapshot_root);
|
|
354
|
+
return allRunEngineBindings(ddFlowHome).some((binding) => path.resolve(binding.engine.snapshot_root) === snapshotRoot);
|
|
355
|
+
}
|
|
150
356
|
export function engineRoutingMetadata(env) {
|
|
151
357
|
if (env.DD_FLOW_ENGINE_MODE !== "1")
|
|
152
358
|
return null;
|
|
@@ -156,25 +362,54 @@ export function engineRoutingMetadata(env) {
|
|
|
156
362
|
routed_from: env.DD_FLOW_ROUTED_FROM ?? null
|
|
157
363
|
};
|
|
158
364
|
}
|
|
159
|
-
export function selectEngine(context, input, options = {}) {
|
|
160
|
-
const
|
|
365
|
+
export function selectEngine(context, input, options = {}, classification = input.classification ?? classifyCliOperation(["status"], input.env ?? process.env)) {
|
|
366
|
+
const projectCompatibility = readCompatibilityForProject(input.projectRoot);
|
|
367
|
+
const targetCanon = classification.mode === "mb_upgrade" ? resolveCanonRootBeforeContext(input.env ?? process.env) : null;
|
|
368
|
+
const compatibility = classification.mode === "mb_upgrade"
|
|
369
|
+
? readCompatibilityForCanon(targetCanon)
|
|
370
|
+
: projectCompatibility;
|
|
161
371
|
const build = getCliBuildInfo();
|
|
162
|
-
const
|
|
163
|
-
|
|
372
|
+
const targetBlockers = classification.mode === "mb_upgrade"
|
|
373
|
+
? [
|
|
374
|
+
...(targetCanon && !targetCanon.ok ? targetCanon.blockers : []),
|
|
375
|
+
...(targetCanon?.ok && !compatibility ? ["upgrade target compatibility manifest is unavailable"] : []),
|
|
376
|
+
...targetPinBlockers(input.env ?? process.env, targetCanon)
|
|
377
|
+
]
|
|
378
|
+
: [];
|
|
379
|
+
const packageName = stringValue(compatibility?.engine?.package_name) ?? (classification.mode === "mb_upgrade" ? build.package_name : build.package_name);
|
|
380
|
+
const requiredRange = stringValue(compatibility?.engine?.version_range) ?? (classification.mode === "mb_upgrade" ? null : `>=${build.version} <9999.0.0`);
|
|
164
381
|
const recommended = stringValue(compatibility?.engine?.recommended_version) ?? build.version;
|
|
165
382
|
const installHintTemplate = stringValue(compatibility?.engine?.old_engine_install_hint) ?? `npx ${packageName}@<version> engine install`;
|
|
166
383
|
const manifests = readInstalledManifests(engineStoreRoot(context.ddFlowHome))
|
|
167
384
|
.filter((manifest) => manifest.package_name === packageName)
|
|
168
|
-
.filter((manifest) => satisfiesRange(manifest.package_version, requiredRange))
|
|
385
|
+
.filter((manifest) => requiredRange !== null && satisfiesRange(manifest.package_version, requiredRange))
|
|
169
386
|
.filter((manifest) => options.allowCurrentInProcess
|
|
170
387
|
&& manifest.package_name === build.package_name
|
|
171
388
|
&& manifest.package_version === build.version
|
|
172
389
|
? true
|
|
173
390
|
: manifestHealthy(manifest))
|
|
174
|
-
.sort((a, b) =>
|
|
175
|
-
|
|
391
|
+
.sort((a, b) => {
|
|
392
|
+
const versionOrder = compareSemverStrings(b.package_version, a.package_version);
|
|
393
|
+
if (versionOrder !== 0)
|
|
394
|
+
return versionOrder;
|
|
395
|
+
const installedOrder = Date.parse(b.installed_at) - Date.parse(a.installed_at);
|
|
396
|
+
if (Number.isFinite(installedOrder) && installedOrder !== 0)
|
|
397
|
+
return installedOrder;
|
|
398
|
+
return b.snapshot_root.localeCompare(a.snapshot_root);
|
|
399
|
+
});
|
|
400
|
+
const selected = manifests[0] ?? (options.allowCurrentInProcess &&
|
|
401
|
+
targetBlockers.length === 0 &&
|
|
402
|
+
requiredRange !== null &&
|
|
403
|
+
packageName === build.package_name &&
|
|
404
|
+
satisfiesRange(build.version, requiredRange)
|
|
405
|
+
? currentBuildManifest(context, build)
|
|
406
|
+
: null);
|
|
407
|
+
const diagnostics = [
|
|
408
|
+
...targetBlockers,
|
|
409
|
+
...(selected ? [] : [`No installed engine satisfies ${packageName} ${requiredRange ?? "the pinned target range"}`])
|
|
410
|
+
];
|
|
176
411
|
return {
|
|
177
|
-
status: selected ? "selected" : "missing",
|
|
412
|
+
status: targetBlockers.length > 0 ? "unsupported" : selected ? "selected" : "missing",
|
|
178
413
|
package_name: packageName,
|
|
179
414
|
required_range: requiredRange,
|
|
180
415
|
recommended_version: recommended,
|
|
@@ -182,9 +417,38 @@ export function selectEngine(context, input, options = {}) {
|
|
|
182
417
|
install_hint: selected ? null : installHintTemplate.replace("<version>", recommended),
|
|
183
418
|
project_root: input.projectRoot ? path.resolve(input.projectRoot) : null,
|
|
184
419
|
memory_bank_version: compatibility?.memory_bank_version ?? null,
|
|
185
|
-
diagnostics
|
|
420
|
+
diagnostics
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function currentBuildManifest(context, build) {
|
|
424
|
+
const packageRoot = findPackageRoot(path.dirname(sourceFile)) ?? process.cwd();
|
|
425
|
+
return buildManifest(context, build.package_name, build.version, packageRoot, packageRoot, packageRoot);
|
|
426
|
+
}
|
|
427
|
+
export function compatibilityContexts(context, input = {}) {
|
|
428
|
+
const env = input.env ?? process.env;
|
|
429
|
+
const classification = input.classification ?? classifyCliOperation(["status"], env);
|
|
430
|
+
const currentSelection = selectEngine(context, { projectRoot: input.projectRoot, env }, { allowCurrentInProcess: true }, { mode: "normal_write", operation: "project.current", family: "project", command: "current", action: null });
|
|
431
|
+
const targetCanon = classification.mode === "mb_upgrade" ? resolveCanonRootBeforeContext(env) : null;
|
|
432
|
+
const targetSelection = classification.mode === "mb_upgrade"
|
|
433
|
+
? selectEngine(context, { projectRoot: input.projectRoot, env }, {}, classification)
|
|
434
|
+
: null;
|
|
435
|
+
return {
|
|
436
|
+
project_current: compatibilityContextSummary("project_current", currentSelection, null),
|
|
437
|
+
upgrade_target: classification.mode === "mb_upgrade"
|
|
438
|
+
? compatibilityContextSummary("upgrade_target", targetSelection ?? currentSelection, targetCanon)
|
|
439
|
+
: null,
|
|
440
|
+
effective_execution: {
|
|
441
|
+
operation: classification.operation,
|
|
442
|
+
mode: classification.mode,
|
|
443
|
+
package_name: (targetSelection ?? currentSelection).package_name,
|
|
444
|
+
engine_version: (targetSelection ?? currentSelection).selected?.package_version ?? null,
|
|
445
|
+
reason: classification.mode === "mb_upgrade" ? "explicit_upgrade_allowlist_target_canon" : "project_current"
|
|
446
|
+
}
|
|
186
447
|
};
|
|
187
448
|
}
|
|
449
|
+
export function resolveExplicitProjectRoot(args) {
|
|
450
|
+
return option(parseLightArgs(args), "project-root") ?? option(parseLightArgs(args), "root");
|
|
451
|
+
}
|
|
188
452
|
export function resolveOperationProjectRoot(context, args) {
|
|
189
453
|
const [family, command, ...rest] = args;
|
|
190
454
|
const parsed = parseLightArgs(rest);
|
|
@@ -234,6 +498,68 @@ function readCompatibilityForProject(projectRoot) {
|
|
|
234
498
|
}
|
|
235
499
|
return null;
|
|
236
500
|
}
|
|
501
|
+
function isUpgradeDiagnostic(args) {
|
|
502
|
+
const [family, command] = args;
|
|
503
|
+
return family === "status" || family === "engine" && command === "resolve";
|
|
504
|
+
}
|
|
505
|
+
function targetPinBlockers(env, resolution) {
|
|
506
|
+
if (!resolution?.ok || !resolution.canon)
|
|
507
|
+
return [];
|
|
508
|
+
const blockers = [];
|
|
509
|
+
const expectedVersion = env.DD_FLOW_MB_UPGRADE_TARGET_VERSION;
|
|
510
|
+
const expectedCommit = env.DD_FLOW_MB_UPGRADE_TARGET_COMMIT;
|
|
511
|
+
if (expectedVersion && expectedVersion !== resolution.canon.version)
|
|
512
|
+
blockers.push("target canon version pin does not match resolved canon");
|
|
513
|
+
if (expectedCommit && expectedCommit !== resolution.canon.commit)
|
|
514
|
+
blockers.push("target canon commit pin does not match resolved canon");
|
|
515
|
+
const expectedSha = env.DD_FLOW_MB_UPGRADE_COMPATIBILITY_SHA256;
|
|
516
|
+
if (expectedSha && expectedSha !== canonCompatibilitySha(resolution.canon.memorybank_root))
|
|
517
|
+
blockers.push("target compatibility SHA pin does not match resolved canon");
|
|
518
|
+
return blockers;
|
|
519
|
+
}
|
|
520
|
+
function readCompatibilityForCanon(resolution) {
|
|
521
|
+
if (!resolution?.ok || !resolution.canon)
|
|
522
|
+
return null;
|
|
523
|
+
return readCompatibilityFile(path.join(resolution.canon.memorybank_root, "dd-flow", "compatibility.json"));
|
|
524
|
+
}
|
|
525
|
+
function readCompatibilityFile(file) {
|
|
526
|
+
try {
|
|
527
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function compatibilityContextSummary(name, selection, canon) {
|
|
534
|
+
return {
|
|
535
|
+
name,
|
|
536
|
+
canon: canon?.canon ? {
|
|
537
|
+
root: canon.canon.root,
|
|
538
|
+
version: canon.canon.version,
|
|
539
|
+
commit: canon.canon.commit,
|
|
540
|
+
flow_contract: canon.canon.flow_contract,
|
|
541
|
+
compatibility_sha256: canonCompatibilitySha(canon.canon.memorybank_root)
|
|
542
|
+
} : {
|
|
543
|
+
source: "project_current"
|
|
544
|
+
},
|
|
545
|
+
memory_bank_version: selection.memory_bank_version,
|
|
546
|
+
package_name: selection.package_name,
|
|
547
|
+
required_engine_range: selection.required_range,
|
|
548
|
+
recommended_engine_version: selection.recommended_version,
|
|
549
|
+
resolution: selection.status,
|
|
550
|
+
engine_version: selection.selected?.package_version ?? null,
|
|
551
|
+
diagnostics: selection.diagnostics
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function canonCompatibilitySha(memorybankRoot) {
|
|
555
|
+
const file = path.join(memorybankRoot, "dd-flow", "compatibility.json");
|
|
556
|
+
try {
|
|
557
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
237
563
|
function parseLightArgs(args) {
|
|
238
564
|
const positional = [];
|
|
239
565
|
const options = new Map();
|
|
@@ -285,6 +611,7 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
|
|
|
285
611
|
version: ["dd-flow/version-report@1"],
|
|
286
612
|
status: ["dd-flow/status-report@1"]
|
|
287
613
|
},
|
|
614
|
+
schema_registry: buildSchemaRegistry(checksumRoot),
|
|
288
615
|
integrity: {
|
|
289
616
|
source: "package_snapshot",
|
|
290
617
|
checksum: engineSnapshotChecksum(checksumRoot),
|
|
@@ -430,6 +757,11 @@ function readInstalledManifests(root) {
|
|
|
430
757
|
}
|
|
431
758
|
return manifests;
|
|
432
759
|
}
|
|
760
|
+
function findInstalledManifest(ddFlowHome, input) {
|
|
761
|
+
return readInstalledManifests(engineStoreRoot(ddFlowHome)).find((manifest) => (manifest.package_name === input.packageName
|
|
762
|
+
&& manifest.package_version === input.packageVersion
|
|
763
|
+
&& (input.integrityChecksum === undefined || manifest.integrity.checksum === input.integrityChecksum))) ?? null;
|
|
764
|
+
}
|
|
433
765
|
function requireManifest(file) {
|
|
434
766
|
const manifest = readManifest(file);
|
|
435
767
|
if (!manifest)
|
|
@@ -479,6 +811,24 @@ function manifestDiagnostics(manifest, options = {}) {
|
|
|
479
811
|
diagnostics.push("snapshot_unreadable");
|
|
480
812
|
}
|
|
481
813
|
}
|
|
814
|
+
if (manifest.schema_registry) {
|
|
815
|
+
for (const entry of manifest.schema_registry.entries) {
|
|
816
|
+
const schemaPath = safeResolveSnapshotPath(manifest, entry.path);
|
|
817
|
+
if (!schemaPath || !fs.existsSync(schemaPath))
|
|
818
|
+
diagnostics.push(`schema_registry_missing:${entry.name}`);
|
|
819
|
+
else if (fileChecksum(schemaPath) !== entry.checksum)
|
|
820
|
+
diagnostics.push(`schema_registry_checksum_mismatch:${entry.name}`);
|
|
821
|
+
else {
|
|
822
|
+
try {
|
|
823
|
+
if (JSON.parse(fs.readFileSync(schemaPath, "utf8")).$id !== entry.id)
|
|
824
|
+
diagnostics.push(`schema_registry_id_mismatch:${entry.name}`);
|
|
825
|
+
}
|
|
826
|
+
catch {
|
|
827
|
+
diagnostics.push(`schema_registry_unreadable:${entry.name}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
482
832
|
if (options.checkEntrypoint !== false && entrypoint && fs.existsSync(entrypoint) && !diagnostics.includes("entrypoint_outside_snapshot")) {
|
|
483
833
|
const health = spawnSync(process.execPath, [entrypoint, "--version", "--json"], {
|
|
484
834
|
cwd: snapshotRoot,
|
|
@@ -503,6 +853,7 @@ function manifestSummary(manifest) {
|
|
|
503
853
|
package_name: manifest.package_name,
|
|
504
854
|
package_version: manifest.package_version,
|
|
505
855
|
engine_version: manifest.engine_version,
|
|
856
|
+
...(manifest.schema_registry ? { schema_registry: manifest.schema_registry } : {}),
|
|
506
857
|
install_source: manifest.install_source,
|
|
507
858
|
entrypoint: manifest.entrypoint,
|
|
508
859
|
snapshot_root: manifest.snapshot_root,
|
|
@@ -530,6 +881,33 @@ function safeResolveManifestEntrypoint(manifest) {
|
|
|
530
881
|
return null;
|
|
531
882
|
return candidate;
|
|
532
883
|
}
|
|
884
|
+
function safeResolveSnapshotPath(manifest, relativePath) {
|
|
885
|
+
if (path.isAbsolute(relativePath))
|
|
886
|
+
return null;
|
|
887
|
+
const snapshotRoot = path.resolve(manifest.snapshot_root || manifest.package_root);
|
|
888
|
+
const candidate = path.resolve(snapshotRoot, relativePath);
|
|
889
|
+
return isInside(snapshotRoot, candidate) ? candidate : null;
|
|
890
|
+
}
|
|
891
|
+
function buildSchemaRegistry(snapshotRoot) {
|
|
892
|
+
const schemaRoot = path.join(snapshotRoot, "dist", "schemas");
|
|
893
|
+
const entries = !fs.existsSync(schemaRoot) ? [] : fs.readdirSync(schemaRoot, { withFileTypes: true })
|
|
894
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".schema.json") && !entry.name.includes("-legacy-"))
|
|
895
|
+
.map((entry) => {
|
|
896
|
+
const file = path.join(schemaRoot, entry.name);
|
|
897
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
898
|
+
return {
|
|
899
|
+
name: entry.name.slice(0, -".schema.json".length),
|
|
900
|
+
id: typeof parsed.$id === "string" ? parsed.$id : entry.name,
|
|
901
|
+
path: path.join("dist", "schemas", entry.name),
|
|
902
|
+
checksum: fileChecksum(file)
|
|
903
|
+
};
|
|
904
|
+
})
|
|
905
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
906
|
+
return { schema_id: "dd-flow/engine-schema-registry@1", entries };
|
|
907
|
+
}
|
|
908
|
+
function fileChecksum(file) {
|
|
909
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
910
|
+
}
|
|
533
911
|
function isInside(root, candidate) {
|
|
534
912
|
const relative = path.relative(root, candidate);
|
|
535
913
|
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|