@deksden-com/dd-flow-cli 0.9.0-beta.75 → 0.9.0-beta.79
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/dist/build-info.json +6 -6
- package/dist/cli/command-inputs.js +300 -0
- package/dist/cli/help.js +1 -1
- package/dist/cli/hook-ingress.js +82 -0
- package/dist/cli/input-preparation.js +115 -0
- package/dist/cli/run-cli.js +1015 -423
- package/dist/cli.js +6 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +7 -2
- package/dist/harness-runtime/lib/dd-codex.mjs +137 -13
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
- package/dist/harness-runtime/lib/native-hook-command.d.mts +1 -0
- package/dist/services/canon.js +11 -8
- package/dist/services/cleanup.js +49 -8
- package/dist/services/cli-operation-classifier.js +12 -24
- package/dist/services/codex-hook-delivery.js +28 -0
- package/dist/services/config.js +11 -8
- package/dist/services/controller-fanout.js +2 -2
- package/dist/services/dashboard.js +94 -31
- package/dist/services/engines.js +75 -61
- package/dist/services/eval-snapshots.js +138 -38
- package/dist/services/execution-policy.js +5 -12
- package/dist/services/hooks.js +226 -203
- package/dist/services/lanes.js +60 -52
- package/dist/services/lifecycle-command.js +13 -1
- package/dist/services/lifecycle-invocations.js +285 -94
- package/dist/services/managed-processes.js +158 -18
- package/dist/services/merge-queue.js +173 -98
- package/dist/services/migrations.js +13 -8
- package/dist/services/plan-runtime.js +19 -8
- package/dist/services/plans.js +28 -25
- package/dist/services/projects.js +11 -2
- package/dist/services/prompts.js +23 -16
- package/dist/services/protocols.js +54 -19
- package/dist/services/recovery-observation-budget.js +1 -1
- package/dist/services/run-controller-adapter.js +31 -20
- package/dist/services/run-controller-state.js +5 -0
- package/dist/services/run-controller.js +102 -56
- package/dist/services/run-fork.js +36 -22
- package/dist/services/run-recovery.js +32 -17
- package/dist/services/runs.js +110 -38
- package/dist/services/runtime-budget.js +120 -48
- package/dist/services/runtime-scope-capture.js +2 -2
- package/dist/services/runtime-scope-control.js +2 -2
- package/dist/services/runtime-scope-resume.js +34 -12
- package/dist/services/runtime-scope-worker.js +49 -16
- package/dist/services/runtime-service.js +8 -7
- package/dist/services/schema-validation.js +9 -7
- package/dist/services/sessions.js +35 -69
- package/dist/services/stage-blocker.js +17 -7
- package/dist/services/stage-context.js +42 -16
- package/dist/services/stage-lifecycle.js +82 -100
- package/dist/services/stage-pause.js +54 -24
- package/dist/services/vnext-code-review.js +82 -32
- package/dist/services/vnext-code.js +78 -45
- package/dist/services/vnext-fanout.js +13 -12
- package/dist/services/vnext-merge.js +68 -35
- package/dist/services/vnext-plan-review.js +84 -41
- package/dist/services/vnext-plan.js +91 -70
- package/dist/services/vnext-protocolize.js +116 -50
- package/dist/services/vnext-specify.js +70 -57
- package/dist/services/work-registry.js +195 -92
- package/dist/services/worktrees.js +85 -35
- package/dist/storage/database.js +104 -10
- package/dist/storage/paths.js +17 -4
- package/package.json +1 -1
package/dist/services/engines.js
CHANGED
|
@@ -12,6 +12,7 @@ import { requireProtocol } from "./protocols.js";
|
|
|
12
12
|
import { resolveCanonRootBeforeContext } from "./canon.js";
|
|
13
13
|
import { appendAudit } from "./audit.js";
|
|
14
14
|
import { findProjectByRoot } from "./projects.js";
|
|
15
|
+
import { parseCommandArgs } from "./lifecycle-command.js";
|
|
15
16
|
import { allRunEngineBindings, locateRunRoot, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
|
|
16
17
|
export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
|
|
17
18
|
const routerNativeFamilies = new Set(["engine", "version", "schema"]);
|
|
@@ -181,7 +182,8 @@ export function routeArgsThroughEngine(context, args, io, stdin, env, resolvedPr
|
|
|
181
182
|
const entrypoint = resolveManifestEntrypoint(selection.selected);
|
|
182
183
|
return spawnEngine(selection.selected, entrypoint, args, io, stdin, env, current.version);
|
|
183
184
|
}
|
|
184
|
-
export function
|
|
185
|
+
export function prepareRunEngineBind(context, input) {
|
|
186
|
+
const reason = requiredText(input.reason, "reason");
|
|
185
187
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
186
188
|
const located = locateRunRoot(context.db, projectRoot, input.runId);
|
|
187
189
|
if (!located)
|
|
@@ -191,19 +193,31 @@ export function bindRunEngine(context, input) {
|
|
|
191
193
|
packageName: input.packageName ?? getCliBuildInfo().package_name,
|
|
192
194
|
packageVersion: input.engineVersion,
|
|
193
195
|
...(input.integrityChecksum ? { integrityChecksum: input.integrityChecksum } : {})
|
|
194
|
-
});
|
|
196
|
+
}, { checkEntrypoint: false });
|
|
195
197
|
const requested = engineIdentity(manifest);
|
|
196
198
|
if (existing) {
|
|
197
199
|
if (sameEngineIdentity(existing.engine, requested))
|
|
198
|
-
return {
|
|
200
|
+
return { kind: "replay", binding: existing };
|
|
199
201
|
throw new AppError("run_engine_binding_immutable", "RUN engine binding cannot be changed", 1, {
|
|
200
202
|
run_id: located.run_id,
|
|
201
203
|
current: existing.engine,
|
|
202
204
|
requested
|
|
203
205
|
});
|
|
204
206
|
}
|
|
207
|
+
return { kind: "create", projectRoot, located, manifest, requested, reason, entrypoint: resolveManifestEntrypoint(manifest) };
|
|
208
|
+
}
|
|
209
|
+
export function bindRunEngine(context, input, prepared = prepareRunEngineBind(context, input)) {
|
|
210
|
+
if (prepared.kind === "replay")
|
|
211
|
+
return { ok: true, action: "run_engine_bind", changed: false, binding: prepared.binding };
|
|
212
|
+
const { projectRoot, located, manifest, requested, reason, entrypoint } = prepared;
|
|
213
|
+
const existing = readRunEngineBinding(located.binding_path, { allowMissing: true });
|
|
214
|
+
if (existing) {
|
|
215
|
+
if (sameEngineIdentity(existing.engine, requested))
|
|
216
|
+
return { ok: true, action: "run_engine_bind", changed: false, binding: existing };
|
|
217
|
+
throw new AppError("run_engine_binding_immutable", "RUN engine binding cannot be changed", 1, { run_id: located.run_id, current: existing.engine, requested });
|
|
218
|
+
}
|
|
205
219
|
const probeArgs = ["run", "status", located.run_id, "--project-root", projectRoot, "--json"];
|
|
206
|
-
const probe = spawnSync(process.execPath, [
|
|
220
|
+
const probe = spawnSync(process.execPath, [entrypoint, ...probeArgs], {
|
|
207
221
|
cwd: manifest.snapshot_root,
|
|
208
222
|
env: { ...process.env, ...context.env, DD_FLOW_HOME: context.ddFlowHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: manifest.snapshot_root },
|
|
209
223
|
encoding: "utf8",
|
|
@@ -224,7 +238,7 @@ export function bindRunEngine(context, input) {
|
|
|
224
238
|
project_root: projectRoot,
|
|
225
239
|
bound_at: context.now(),
|
|
226
240
|
source: "legacy_recovery",
|
|
227
|
-
reason
|
|
241
|
+
reason,
|
|
228
242
|
engine: requested,
|
|
229
243
|
probe: { status: "passed", command: `dd-flow ${probeArgs.join(" ")}` }
|
|
230
244
|
};
|
|
@@ -325,7 +339,7 @@ function runIdForArgs(args) {
|
|
|
325
339
|
return option(parsed, "run");
|
|
326
340
|
return null;
|
|
327
341
|
}
|
|
328
|
-
function requireBoundManifest(ddFlowHome, input) {
|
|
342
|
+
function requireBoundManifest(ddFlowHome, input, options = {}) {
|
|
329
343
|
const manifest = input.snapshotRoot
|
|
330
344
|
? requireManifest(path.join(input.snapshotRoot, "engine.json"))
|
|
331
345
|
: findInstalledManifest(ddFlowHome, {
|
|
@@ -338,7 +352,7 @@ function requireBoundManifest(ddFlowHome, input) {
|
|
|
338
352
|
|| manifest.package_version !== input.packageVersion
|
|
339
353
|
|| input.integrityChecksum !== undefined && manifest.integrity.checksum !== input.integrityChecksum
|
|
340
354
|
|| input.snapshotRoot !== undefined && path.resolve(manifest.snapshot_root) !== path.resolve(input.snapshotRoot)
|
|
341
|
-
||
|
|
355
|
+
|| manifestDiagnostics(manifest, options).length !== 0) {
|
|
342
356
|
throw new AppError("run_engine_binding_invalid", "RUN engine binding points to a missing or unhealthy engine snapshot", 1, {
|
|
343
357
|
package_name: input.packageName,
|
|
344
358
|
package_version: input.packageVersion,
|
|
@@ -379,7 +393,7 @@ function executionManifest(context) {
|
|
|
379
393
|
return requireManifest(path.join(String(installed.path), "engine.json"));
|
|
380
394
|
}
|
|
381
395
|
function requiredText(value, label) {
|
|
382
|
-
const normalized = value.trim();
|
|
396
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
383
397
|
if (!normalized)
|
|
384
398
|
throw new AppError("usage", `Missing required value: ${label}`, 2);
|
|
385
399
|
return normalized;
|
|
@@ -591,35 +605,11 @@ function canonCompatibilitySha(memorybankRoot) {
|
|
|
591
605
|
return null;
|
|
592
606
|
}
|
|
593
607
|
}
|
|
594
|
-
|
|
595
|
-
const positional = [];
|
|
596
|
-
const options = new Map();
|
|
597
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
598
|
-
const value = args[index];
|
|
599
|
-
if (value?.startsWith("--")) {
|
|
600
|
-
const equal = value.indexOf("=");
|
|
601
|
-
if (equal > 2) {
|
|
602
|
-
const key = value.slice(2, equal);
|
|
603
|
-
options.set(key, [...(options.get(key) ?? []), value.slice(equal + 1)]);
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
const key = value.slice(2);
|
|
607
|
-
const next = args[index + 1];
|
|
608
|
-
if (!next || next.startsWith("--"))
|
|
609
|
-
options.set(key, [...(options.get(key) ?? []), ""]);
|
|
610
|
-
else {
|
|
611
|
-
options.set(key, [...(options.get(key) ?? []), next]);
|
|
612
|
-
index += 1;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
else if (value)
|
|
616
|
-
positional.push(value);
|
|
617
|
-
}
|
|
618
|
-
return { positional, options };
|
|
619
|
-
}
|
|
608
|
+
const parseLightArgs = parseCommandArgs;
|
|
620
609
|
function option(parsed, key) {
|
|
621
610
|
const values = parsed.options.get(key);
|
|
622
|
-
|
|
611
|
+
const value = values?.[values.length - 1];
|
|
612
|
+
return value && value !== "__dd_flow_flag__" ? value : null;
|
|
623
613
|
}
|
|
624
614
|
function positional(parsed, index) {
|
|
625
615
|
return parsed.positional[index] ?? null;
|
|
@@ -651,23 +641,37 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
|
|
|
651
641
|
};
|
|
652
642
|
}
|
|
653
643
|
function copyPackageSnapshot(packageRoot, target) {
|
|
644
|
+
for (const [relative, source] of packageSnapshotFiles(packageRoot)) {
|
|
645
|
+
const destination = path.join(target, relative);
|
|
646
|
+
ensureDir(path.dirname(destination));
|
|
647
|
+
fs.copyFileSync(source, destination);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** The same source inventory drives validation and copying; no staging writes. */
|
|
651
|
+
function packageSnapshotFiles(packageRoot) {
|
|
652
|
+
const files = new Map();
|
|
653
|
+
const collect = (source, relative, skipNodeModules = false) => {
|
|
654
|
+
if (!fs.statSync(source).isDirectory()) {
|
|
655
|
+
files.set(relative, source);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
659
|
+
if (skipNodeModules && entry.name === "node_modules")
|
|
660
|
+
continue;
|
|
661
|
+
const child = path.join(source, entry.name), name = path.join(relative, entry.name);
|
|
662
|
+
if (entry.isDirectory())
|
|
663
|
+
collect(child, name, skipNodeModules);
|
|
664
|
+
else if (entry.isFile())
|
|
665
|
+
files.set(name, child);
|
|
666
|
+
}
|
|
667
|
+
};
|
|
654
668
|
for (const item of ["dist", "package.json", "README.md", "CHANGELOG.md", "tools"]) {
|
|
655
669
|
const from = path.join(packageRoot, item);
|
|
656
670
|
if (!fs.existsSync(from))
|
|
657
671
|
continue;
|
|
658
|
-
|
|
659
|
-
const stat = fs.statSync(from);
|
|
660
|
-
if (stat.isDirectory())
|
|
661
|
-
copyDir(from, to);
|
|
662
|
-
else {
|
|
663
|
-
ensureDir(path.dirname(to));
|
|
664
|
-
fs.copyFileSync(from, to);
|
|
665
|
-
}
|
|
672
|
+
collect(from, item);
|
|
666
673
|
}
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
function copyProductionDependencies(packageRoot, target) {
|
|
670
|
-
const queue = [{ sourceRoot: packageRoot, targetRoot: target, ancestors: new Set() }];
|
|
674
|
+
const queue = [{ sourceRoot: packageRoot, targetRoot: "", ancestors: new Set() }];
|
|
671
675
|
const copied = new Set();
|
|
672
676
|
while (queue.length > 0) {
|
|
673
677
|
const current = queue.shift();
|
|
@@ -682,12 +686,35 @@ function copyProductionDependencies(packageRoot, target) {
|
|
|
682
686
|
if (copied.has(copyKey))
|
|
683
687
|
continue;
|
|
684
688
|
copied.add(copyKey);
|
|
685
|
-
|
|
689
|
+
collect(sourceRoot, targetRoot, true);
|
|
686
690
|
if (current.ancestors.has(sourceRoot))
|
|
687
691
|
continue;
|
|
688
692
|
queue.push({ sourceRoot, targetRoot, ancestors: new Set([...current.ancestors, sourceRoot]) });
|
|
689
693
|
}
|
|
690
694
|
}
|
|
695
|
+
return files;
|
|
696
|
+
}
|
|
697
|
+
export function prepareForkEngine(engineVersion, integrityChecksum) {
|
|
698
|
+
const build = getCliBuildInfo();
|
|
699
|
+
if (engineVersion !== build.version)
|
|
700
|
+
throw new AppError("fork_engine_unavailable", "Fork installs the invoking CLI engine; invoke the requested version to upgrade the fork", 2, { requested: engineVersion, available: build.version });
|
|
701
|
+
const packageRoot = findPackageRoot(path.dirname(sourceFile));
|
|
702
|
+
if (!packageRoot || !fs.existsSync(path.join(packageRoot, "dist", "cli.js")))
|
|
703
|
+
throw new AppError("harness_runtime_unbuilt", "Build the CLI before preparing a fork", 1);
|
|
704
|
+
const files = packageSnapshotFiles(packageRoot);
|
|
705
|
+
if (integrityChecksum !== undefined) {
|
|
706
|
+
const checksum = crypto.createHash("sha256");
|
|
707
|
+
for (const [relative, source] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
708
|
+
if (path.basename(relative) === "engine.json")
|
|
709
|
+
continue;
|
|
710
|
+
checksum.update(relative);
|
|
711
|
+
checksum.update("\0");
|
|
712
|
+
checksum.update(fs.readFileSync(source));
|
|
713
|
+
checksum.update("\0");
|
|
714
|
+
}
|
|
715
|
+
if (checksum.digest("hex") !== integrityChecksum)
|
|
716
|
+
throw new AppError("fork_engine_unavailable", "Requested engine checksum differs from the invoking CLI artifact", 2, { requested: integrityChecksum });
|
|
717
|
+
}
|
|
691
718
|
}
|
|
692
719
|
function readProductionDependencies(packageRoot) {
|
|
693
720
|
try {
|
|
@@ -744,19 +771,6 @@ function hasAncestorFile(start, name) {
|
|
|
744
771
|
current = parent;
|
|
745
772
|
}
|
|
746
773
|
}
|
|
747
|
-
function copyDir(from, to, skipNodeModules = false) {
|
|
748
|
-
ensureDir(to);
|
|
749
|
-
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
750
|
-
if (skipNodeModules && entry.name === "node_modules")
|
|
751
|
-
continue;
|
|
752
|
-
const source = path.join(from, entry.name);
|
|
753
|
-
const target = path.join(to, entry.name);
|
|
754
|
-
if (entry.isDirectory())
|
|
755
|
-
copyDir(source, target, skipNodeModules);
|
|
756
|
-
else if (entry.isFile())
|
|
757
|
-
fs.copyFileSync(source, target);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
774
|
function acquireInstallLock(lockDir) {
|
|
761
775
|
ensureDir(path.dirname(lockDir));
|
|
762
776
|
try {
|
|
@@ -14,8 +14,9 @@ import { readVnextFlowDefinition } from "./vnext-specify.js";
|
|
|
14
14
|
import { captureRecoveryDatabase, readRecoveryResourceEvidence } from "./recovery-snapshot-database.js";
|
|
15
15
|
import { resourceHome } from "./managed-processes.js";
|
|
16
16
|
import { codexHomeSharedEntries } from "./hooks.js";
|
|
17
|
+
import { prepareOutputFile } from "../cli/input-preparation.js";
|
|
17
18
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
18
|
-
export function
|
|
19
|
+
export function prepareEvalBootstrapSnapshot(context, input) {
|
|
19
20
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
20
21
|
const output = path.resolve(input.output);
|
|
21
22
|
if (fs.existsSync(output))
|
|
@@ -24,6 +25,19 @@ export function createEvalBootstrapSnapshot(context, input) {
|
|
|
24
25
|
throw new AppError("snapshot_output_inside_project", "Bootstrap snapshot output must be outside the project tree", 1, { output });
|
|
25
26
|
if (isInside(physicalPath(context.ddFlowHome), physicalPath(output)))
|
|
26
27
|
throw new AppError("snapshot_output_inside_runtime", "Bootstrap snapshot output must be outside DD_FLOW_HOME", 1, { output });
|
|
28
|
+
prepareOutputFile(output);
|
|
29
|
+
try {
|
|
30
|
+
prepareSnapshotProjectGit(projectRoot);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (!(error instanceof AppError) || error.code !== "snapshot_project_git_invalid")
|
|
34
|
+
throw error;
|
|
35
|
+
throw new AppError(error.code, error.message, 2, { ...error.details, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
36
|
+
}
|
|
37
|
+
return { projectRoot, output };
|
|
38
|
+
}
|
|
39
|
+
export function createEvalBootstrapSnapshot(context, input) {
|
|
40
|
+
const { projectRoot, output } = prepareEvalBootstrapSnapshot(context, input);
|
|
27
41
|
fs.mkdirSync(output, { recursive: true });
|
|
28
42
|
fs.cpSync(projectRoot, path.join(output, "project"), { recursive: true, verbatimSymlinks: true, filter: snapshotPayloadAllowed });
|
|
29
43
|
const manifest = {
|
|
@@ -37,13 +51,18 @@ export function createEvalBootstrapSnapshot(context, input) {
|
|
|
37
51
|
return { ok: true, snapshot: output, ...manifest };
|
|
38
52
|
}
|
|
39
53
|
/** Restore a pre-RUN project state; `stage start --bootstrap` creates runtime state afterwards. */
|
|
40
|
-
export function
|
|
54
|
+
export function prepareEvalBootstrapRestore(input) {
|
|
41
55
|
const snapshot = path.resolve(input.snapshot);
|
|
42
56
|
const manifest = readBootstrapManifest(snapshot);
|
|
43
57
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
58
|
+
assertRestoreTargetSeparation(snapshot, [projectRoot]);
|
|
44
59
|
const sourceProject = path.join(snapshot, "project");
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
verifyGitSnapshotPayload(snapshot, { name: "project", hash: manifest.project_sha256, git: manifest.project_git });
|
|
61
|
+
return { snapshot, manifest, projectRoot, sourceProject };
|
|
62
|
+
}
|
|
63
|
+
export function restoreEvalBootstrapSnapshot(_context, input, prepared = prepareEvalBootstrapRestore(input)) {
|
|
64
|
+
const { snapshot, manifest, projectRoot, sourceProject } = prepared;
|
|
65
|
+
assertRestoreTargetSeparation(snapshot, [projectRoot]);
|
|
47
66
|
clearProjectTree(projectRoot, false);
|
|
48
67
|
restoreProjectGit(snapshot, manifest.project_git, projectRoot);
|
|
49
68
|
replaceProjectTree(projectRoot, sourceProject);
|
|
@@ -54,9 +73,7 @@ export function restoreEvalBootstrapSnapshot(_context, input) {
|
|
|
54
73
|
* retain only the selected RUN's relational and filesystem evidence.
|
|
55
74
|
*/
|
|
56
75
|
export function createEvalRunSnapshot(context, input) {
|
|
57
|
-
const output =
|
|
58
|
-
if (fs.existsSync(output))
|
|
59
|
-
throw new AppError("snapshot_exists", "Snapshot output already exists", 1, { output });
|
|
76
|
+
const { output } = prepareEvalRunSnapshotCreation(context, input);
|
|
60
77
|
const temporary = `${output}.partial-${crypto.randomUUID()}`;
|
|
61
78
|
try {
|
|
62
79
|
input.boundary?.verify();
|
|
@@ -89,7 +106,9 @@ export function createEvalRunSnapshot(context, input) {
|
|
|
89
106
|
throw error;
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
|
-
function
|
|
109
|
+
export function prepareEvalRunSnapshotCreation(context, input) {
|
|
110
|
+
if (Number(Boolean(input.candidate)) + Number(Boolean(input.incomplete)) + Number(input.stageEntry !== undefined) + Number(input.recoveryId !== undefined) !== 1)
|
|
111
|
+
throw new AppError("usage", "Use exactly one of candidate, incomplete, recoveryId or stageEntry", 2, { phase: "prepare", effect: "no_effect", recoverable: true });
|
|
93
112
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
94
113
|
// Snapshot is read-only at the control-plane boundary. An invalid workspace
|
|
95
114
|
// root must fail without registering a second project in a dedicated home.
|
|
@@ -129,6 +148,13 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
129
148
|
const workspaceRoot = String(status.run.workspace_root ?? projectRoot);
|
|
130
149
|
if (isInside(physicalPath(projectRoot), physicalPath(output)) || isInside(physicalPath(workspaceRoot), physicalPath(output)))
|
|
131
150
|
throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
|
|
151
|
+
prepareOutputFile(output);
|
|
152
|
+
prepareSnapshotProjectGit(projectRoot);
|
|
153
|
+
prepareSnapshotWorkspace(workspaceRoot, projectRoot);
|
|
154
|
+
return { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren };
|
|
155
|
+
}
|
|
156
|
+
function captureEvalRunSnapshot(context, input) {
|
|
157
|
+
const { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren } = prepareEvalRunSnapshotCreation(context, input);
|
|
132
158
|
fs.mkdirSync(output, { recursive: true, mode: 0o700 });
|
|
133
159
|
const resourceDatabase = path.join(context.ddFlowHome, "runtime.sqlite");
|
|
134
160
|
const databaseFiles = [context.db.path, resourceDatabase].flatMap(file => [file, `${file}-wal`, `${file}-shm`]);
|
|
@@ -217,9 +243,35 @@ function createdWorkSchemaForStage(stage) {
|
|
|
217
243
|
return "dd-flow/merge-result";
|
|
218
244
|
return null;
|
|
219
245
|
}
|
|
220
|
-
|
|
221
|
-
|
|
246
|
+
/** Validate sealed source data before any target cleanup or copy. */
|
|
247
|
+
export function prepareEvalRunSnapshot(snapshot) {
|
|
222
248
|
const manifest = readManifest(snapshot);
|
|
249
|
+
if (manifest.purpose === "incomplete")
|
|
250
|
+
throw new AppError("snapshot_incomplete_not_restorable", "Incomplete snapshot is forensic evidence, not a stage-entry fixture", 1, { snapshot });
|
|
251
|
+
if (manifest.purpose === "candidate")
|
|
252
|
+
throw new AppError("snapshot_candidate_not_restorable", "A terminal candidate snapshot is evidence, not a stage-entry fixture", 1, { snapshot });
|
|
253
|
+
verifySnapshotPayload(snapshot, manifest);
|
|
254
|
+
if (manifest.purpose === "stage_entry") {
|
|
255
|
+
const db = new DatabaseSync(path.join(snapshot, "runtime", "db.sqlite"), { readOnly: true });
|
|
256
|
+
try {
|
|
257
|
+
assertSnapshotRuntimeDetachAllowed(sql => db.prepare(sql).get());
|
|
258
|
+
const row = db.prepare("SELECT index_json FROM runs WHERE id = ?").get(manifest.run_id);
|
|
259
|
+
if (!row)
|
|
260
|
+
throw new AppError("snapshot_invalid", "Snapshot RUN is not registered", 1);
|
|
261
|
+
const index = JSON.parse(row.index_json);
|
|
262
|
+
if (!Array.isArray(index?.stage_runs))
|
|
263
|
+
throw new AppError("snapshot_invalid", "Snapshot RUN index is invalid", 1);
|
|
264
|
+
assertStageEntry(index.stage_runs, manifest.stage_entry);
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
db.close();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return manifest;
|
|
271
|
+
}
|
|
272
|
+
export function prepareEvalRunRestore(context, input, preparedManifest) {
|
|
273
|
+
const snapshot = path.resolve(input.snapshot);
|
|
274
|
+
const manifest = preparedManifest ?? prepareEvalRunSnapshot(snapshot);
|
|
223
275
|
if (manifest.purpose === "incomplete")
|
|
224
276
|
throw new AppError("snapshot_incomplete_not_restorable", "Incomplete snapshot is forensic evidence, not a stage-entry fixture", 1, { snapshot });
|
|
225
277
|
const recovery = manifest.purpose === "recovery";
|
|
@@ -234,24 +286,15 @@ export function restoreEvalRunSnapshot(context, input) {
|
|
|
234
286
|
if (!recovery && !manifest.stage_entry)
|
|
235
287
|
throw new AppError("snapshot_invalid", "Stage-entry snapshot has no target stage", 1, { snapshot });
|
|
236
288
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
throw new AppError("recovery_restore_target_not_empty", "Recovery restore requires fresh project and runtime targets", 1);
|
|
289
|
+
assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest);
|
|
290
|
+
if (recovery)
|
|
240
291
|
verifyRecoverySnapshot(snapshot, { runId: manifest.run_id, recoveryId: input.recoveryId, projectRoot, workspaceRoot: projectRoot }, false);
|
|
241
|
-
}
|
|
242
|
-
const existing = fs.readdirSync(context.ddFlowHome).filter((name) => !["db.sqlite", "db.sqlite-wal", "db.sqlite-shm", ...(input.executionRoutingFile !== undefined ? ["agent-profiles"] : [])].includes(name));
|
|
243
|
-
if (existing.length > 0)
|
|
244
|
-
throw new AppError("restore_home_not_empty", "Snapshot restore requires an empty DD_FLOW_HOME", 1, { dd_flow_home: context.ddFlowHome, existing });
|
|
245
|
-
if (context.db.get("SELECT 1 FROM projects LIMIT 1") || context.db.get("SELECT 1 FROM runs LIMIT 1"))
|
|
246
|
-
throw new AppError("restore_home_not_empty", "Snapshot restore cannot replace an initialized runtime", 1);
|
|
247
292
|
const sourceRuntime = path.join(snapshot, "runtime");
|
|
248
293
|
const sourceProject = path.join(snapshot, "project");
|
|
249
|
-
verifySnapshotPayload(snapshot, manifest);
|
|
250
294
|
const routingInput = input.executionRoutingFile !== undefined ? loadExecutionRoutingInput(input.executionRoutingFile) : undefined;
|
|
251
295
|
let selectedProfile;
|
|
252
296
|
if (routingInput) {
|
|
253
|
-
// Resolve selected profiles
|
|
254
|
-
// Source DB is read-only; no context or migrations may touch the snapshot.
|
|
297
|
+
// Resolve selected profiles before replacing the caller's fresh runtime.
|
|
255
298
|
const sourceDb = new DatabaseSync(path.join(sourceRuntime, "db.sqlite"), { readOnly: true });
|
|
256
299
|
try {
|
|
257
300
|
const row = sourceDb.prepare("SELECT index_json FROM runs WHERE id = ?").get(manifest.run_id);
|
|
@@ -266,11 +309,42 @@ export function restoreEvalRunSnapshot(context, input) {
|
|
|
266
309
|
resolveExecutionPolicy({ ddFlowHome: context.ddFlowHome, policy: routingInput.execution, profiles: selectedProfile.agent_profiles, stage: manifest.stage_entry, work: payload.execution_override });
|
|
267
310
|
}
|
|
268
311
|
selectedProfile.routing_input.artifact = `execution-routing.${routingInput.source_checksum}.input.json`;
|
|
312
|
+
const sourceRun = sourceDb.prepare("SELECT run_root FROM runs WHERE id = ?").get(manifest.run_id);
|
|
313
|
+
if (typeof sourceRun?.run_root !== "string" || !path.isAbsolute(sourceRun.run_root) || !isInside(manifest.dd_flow_home, sourceRun.run_root))
|
|
314
|
+
throw new AppError("snapshot_invalid", "Snapshot RUN root must be inside its runtime home", 1);
|
|
315
|
+
const existingRouting = path.join(sourceRuntime, path.relative(manifest.dd_flow_home, sourceRun.run_root), selectedProfile.routing_input.artifact);
|
|
316
|
+
if (fs.existsSync(existingRouting) && !fs.readFileSync(existingRouting).equals(routingInput.bytes))
|
|
317
|
+
throw new AppError("snapshot_checksum_mismatch", "Existing routing input does not match its checksum", 1);
|
|
269
318
|
}
|
|
270
319
|
finally {
|
|
271
320
|
sourceDb.close();
|
|
272
321
|
}
|
|
273
322
|
}
|
|
323
|
+
return { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile };
|
|
324
|
+
}
|
|
325
|
+
function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest) {
|
|
326
|
+
assertRestoreTargetSeparation(snapshot, [projectRoot, context.ddFlowHome]);
|
|
327
|
+
if (manifest.workspace.kind === "git_worktree") {
|
|
328
|
+
const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
|
|
329
|
+
assertRestoreTargetSeparation(snapshot, [workspaceRoot]);
|
|
330
|
+
if (isInside(physicalPath(projectRoot), physicalPath(workspaceRoot)) || isInside(physicalPath(workspaceRoot), physicalPath(projectRoot)) || physicalPath(workspaceRoot) === physicalPath(context.ddFlowHome))
|
|
331
|
+
throw new AppError("snapshot_path_conflict", "Feature workspace overlaps a restore target", 2);
|
|
332
|
+
if (fs.existsSync(workspaceRoot))
|
|
333
|
+
throw new AppError("snapshot_workspace_target_exists", "Snapshot feature workspace target already exists", 1, { workspace_root: workspaceRoot });
|
|
334
|
+
}
|
|
335
|
+
if (manifest.purpose === "recovery") {
|
|
336
|
+
if (fs.readdirSync(projectRoot).length || (context.db.get("SELECT COUNT(*) AS count FROM projects")?.count ?? 0) > 0)
|
|
337
|
+
throw new AppError("recovery_restore_target_not_empty", "Recovery restore requires fresh project and runtime targets", 1);
|
|
338
|
+
}
|
|
339
|
+
const existing = (fs.existsSync(context.ddFlowHome) ? fs.readdirSync(context.ddFlowHome) : []).filter((name) => !["db.sqlite", "db.sqlite-wal", "db.sqlite-shm", ...(input.executionRoutingFile !== undefined ? ["agent-profiles"] : [])].includes(name));
|
|
340
|
+
if (existing.length > 0)
|
|
341
|
+
throw new AppError("restore_home_not_empty", "Snapshot restore requires an empty DD_FLOW_HOME", 1, { dd_flow_home: context.ddFlowHome, existing });
|
|
342
|
+
if (context.db.get("SELECT 1 FROM projects LIMIT 1") || context.db.get("SELECT 1 FROM runs LIMIT 1"))
|
|
343
|
+
throw new AppError("restore_home_not_empty", "Snapshot restore cannot replace an initialized runtime", 1);
|
|
344
|
+
}
|
|
345
|
+
export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRunRestore(context, input)) {
|
|
346
|
+
const { snapshot, manifest, recovery, projectRoot, sourceRuntime, sourceProject, routingInput, selectedProfile } = prepared;
|
|
347
|
+
assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest);
|
|
274
348
|
clearProjectTree(projectRoot, false);
|
|
275
349
|
restoreProjectGit(snapshot, manifest.project_git, projectRoot);
|
|
276
350
|
replaceProjectTree(projectRoot, sourceProject);
|
|
@@ -370,12 +444,15 @@ function assertTerminalCandidate(stages) {
|
|
|
370
444
|
}
|
|
371
445
|
/** A stage-entry import is a new experiment, not a lease takeover or recovery.
|
|
372
446
|
* Keep source authority as evidence, never as executable target inventory. */
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
if (hasTable("run_recovery_guards") && context.db.get("SELECT 1 FROM run_recovery_guards WHERE status IN ('draining','sealed','resuming') LIMIT 1"))
|
|
447
|
+
function assertSnapshotRuntimeDetachAllowed(get) {
|
|
448
|
+
if (get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_recovery_guards'") && get("SELECT 1 FROM run_recovery_guards WHERE status IN ('draining','sealed','resuming') LIMIT 1"))
|
|
376
449
|
throw new AppError("snapshot_control_requires_recovery", "A fenced RUN requires explicit recovery, not a stage-entry fork", 1);
|
|
377
|
-
if (
|
|
450
|
+
if (get("SELECT 1 FROM merge_requests WHERE status IN ('dispatching','active','waiting_user','action_required','recovery_required') LIMIT 1"))
|
|
378
451
|
throw new AppError("snapshot_execution_active", "A stage-entry fork cannot discard an unresolved MERGE execution", 1);
|
|
452
|
+
}
|
|
453
|
+
function detachSnapshotRuntimeOwners(context, snapshot, manifest) {
|
|
454
|
+
const hasTable = (table) => Boolean(context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = ?", [table]));
|
|
455
|
+
assertSnapshotRuntimeDetachAllowed(sql => context.db.get(sql));
|
|
379
456
|
const detachedTables = ["lifecycle_invocations", "run_control_admissions", "run_control_stage_admissions", "run_control_operations", "run_control_workers", "run_controls", "run_controller_events", "run_controller_operations", "run_controllers", "run_recovery_bindings", "work_recovery_segments", "run_recovery_guards", "work_launches", "merge_servers", "merge_sessions", "lane_locks", "lane_waiters"].filter(hasTable);
|
|
380
457
|
const runTables = Object.fromEntries([...detachedTables, "runs", "merge_requests", "sessions", "work_sessions"].map(table => [table, context.db.all(`SELECT * FROM ${table}`)]));
|
|
381
458
|
const resourceFile = path.join(context.ddFlowHome, "runtime.sqlite");
|
|
@@ -496,15 +573,22 @@ function rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspa
|
|
|
496
573
|
function rebasePathString(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { for (const [oldPrefix, newPrefix] of [[oldHome, newHome], [oldWorkspace, newWorkspace], [oldRoot, newRoot]])
|
|
497
574
|
if (value === oldPrefix || value.startsWith(`${oldPrefix}${path.sep}`))
|
|
498
575
|
return `${newPrefix}${value.slice(oldPrefix.length)}`; return value; }
|
|
499
|
-
function
|
|
576
|
+
function prepareSnapshotWorkspace(workspaceRoot, projectRoot) {
|
|
500
577
|
if (samePath(workspaceRoot, projectRoot))
|
|
501
|
-
return
|
|
578
|
+
return null;
|
|
502
579
|
const branch = git(workspaceRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
503
580
|
if (!branch)
|
|
504
581
|
throw new AppError("snapshot_workspace_invalid", "Feature workspace must be on a named Git branch", 1, { workspace_root: workspaceRoot });
|
|
505
582
|
const baseRef = git(workspaceRoot, ["rev-parse", "HEAD"]);
|
|
506
583
|
if (!baseRef)
|
|
507
584
|
throw new AppError("snapshot_workspace_invalid", "Feature workspace has no readable HEAD", 1, { workspace_root: workspaceRoot });
|
|
585
|
+
return { branch, baseRef };
|
|
586
|
+
}
|
|
587
|
+
function snapshotWorkspace(workspaceRoot, projectRoot, output) {
|
|
588
|
+
const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot);
|
|
589
|
+
if (!prepared)
|
|
590
|
+
return { kind: "project" };
|
|
591
|
+
const { branch, baseRef } = prepared;
|
|
508
592
|
const workspace = path.join(output, "workspace");
|
|
509
593
|
const bundle = path.join(output, "workspace.bundle");
|
|
510
594
|
fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: snapshotPayloadAllowed });
|
|
@@ -520,11 +604,7 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
|
|
|
520
604
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace checksum does not match manifest", 1, { snapshot });
|
|
521
605
|
if (!fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
|
|
522
606
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace Git bundle does not match manifest", 1, { snapshot });
|
|
523
|
-
const
|
|
524
|
-
const sourceRoot = physicalPath(manifest.workspace.source_root);
|
|
525
|
-
const workspaceRoot = isInside(managedRoot, sourceRoot)
|
|
526
|
-
? path.join(ddFlowHome, path.relative(managedRoot, sourceRoot))
|
|
527
|
-
: path.join(path.dirname(projectRoot), "workspace");
|
|
607
|
+
const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome);
|
|
528
608
|
if (fs.existsSync(workspaceRoot))
|
|
529
609
|
throw new AppError("snapshot_workspace_target_exists", "Snapshot feature workspace target already exists", 1, { workspace_root: workspaceRoot });
|
|
530
610
|
runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
|
|
@@ -535,14 +615,32 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
|
|
|
535
615
|
restoreIndex(snapshot, "workspace", manifest.workspace, workspaceRoot);
|
|
536
616
|
return fs.realpathSync(workspaceRoot);
|
|
537
617
|
}
|
|
618
|
+
function restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome) {
|
|
619
|
+
if (manifest.workspace.kind === "project")
|
|
620
|
+
return projectRoot;
|
|
621
|
+
const managedRoot = physicalPath(manifest.dd_flow_home), sourceRoot = physicalPath(manifest.workspace.source_root);
|
|
622
|
+
return isInside(managedRoot, sourceRoot) ? path.join(ddFlowHome, path.relative(managedRoot, sourceRoot)) : path.join(path.dirname(projectRoot), "workspace");
|
|
623
|
+
}
|
|
624
|
+
function assertRestoreTargetSeparation(snapshot, targets) {
|
|
625
|
+
const roots = [snapshot, ...targets].map(physicalPath);
|
|
626
|
+
for (let i = 0; i < roots.length; i++)
|
|
627
|
+
for (let j = i + 1; j < roots.length; j++) {
|
|
628
|
+
if (isInside(roots[i], roots[j]) || isInside(roots[j], roots[i]))
|
|
629
|
+
throw new AppError("snapshot_path_conflict", "Snapshot source and restore targets must not overlap", 2, { source: roots[i], target: roots[j] });
|
|
630
|
+
}
|
|
631
|
+
}
|
|
538
632
|
function sourceWorkspaceRoot(manifest) {
|
|
539
633
|
return manifest.workspace.kind === "project" ? manifest.project_root : manifest.workspace.source_root;
|
|
540
634
|
}
|
|
541
|
-
function
|
|
635
|
+
function prepareSnapshotProjectGit(projectRoot) {
|
|
542
636
|
const branch = git(projectRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
543
637
|
const head = git(projectRoot, ["rev-parse", "HEAD"]);
|
|
544
638
|
if (!branch || !head)
|
|
545
639
|
throw new AppError("snapshot_project_git_invalid", "Snapshot requires a project on a named Git branch with a readable HEAD", 1, { project_root: projectRoot });
|
|
640
|
+
return { branch, head };
|
|
641
|
+
}
|
|
642
|
+
function snapshotProjectGit(projectRoot, output) {
|
|
643
|
+
const { branch, head } = prepareSnapshotProjectGit(projectRoot);
|
|
546
644
|
const bundle = path.join(output, "project.bundle");
|
|
547
645
|
runGit(projectRoot, ["bundle", "create", bundle, "--all"]);
|
|
548
646
|
return { branch, head, bundle_sha256: fileHash(bundle), ...snapshotIndex(projectRoot, output, "project") };
|
|
@@ -673,10 +771,12 @@ function verifySnapshotPayload(snapshot, manifest) {
|
|
|
673
771
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot runtime checksum does not match", 1);
|
|
674
772
|
verifySnapshotModes(snapshot, manifest);
|
|
675
773
|
const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind === "git_worktree" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
|
|
676
|
-
for (const payload of payloads)
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
774
|
+
for (const payload of payloads)
|
|
775
|
+
verifyGitSnapshotPayload(snapshot, payload);
|
|
776
|
+
}
|
|
777
|
+
function verifyGitSnapshotPayload(snapshot, payload) {
|
|
778
|
+
if (treeHash(path.join(snapshot, payload.name)) !== payload.hash || fileHash(path.join(snapshot, `${payload.name}.bundle`)) !== payload.git.bundle_sha256 || payload.git.index_sha256 && fileHash(path.join(snapshot, `${payload.name}.index.patch`)) !== payload.git.index_sha256 || payload.git.index_format === "entries@1" && (!payload.git.index_objects_sha256 || fileHash(path.join(snapshot, `${payload.name}.index.pack`)) !== payload.git.index_objects_sha256))
|
|
779
|
+
throw new AppError("snapshot_checksum_mismatch", "Snapshot payload or Git index checksum does not match", 1, { payload: payload.name });
|
|
680
780
|
}
|
|
681
781
|
function git(root, args) {
|
|
682
782
|
const result = spawnSync("git", ["-C", root, ...args], { encoding: "utf8" });
|
|
@@ -4,26 +4,19 @@ import path from "node:path";
|
|
|
4
4
|
import { AppError } from "../shared/errors.js";
|
|
5
5
|
import { harnessKey } from "./harness-config.js";
|
|
6
6
|
import { vnextStages } from "../domain/stage-catalog.js";
|
|
7
|
+
import { prepareFileBytes, prepareJsonText } from "../cli/input-preparation.js";
|
|
7
8
|
/** Read once so the frozen policy and its provenance describe the same bytes. */
|
|
8
9
|
export function loadExecutionRoutingInput(file) {
|
|
9
|
-
const source_path =
|
|
10
|
-
|
|
11
|
-
let value;
|
|
12
|
-
try {
|
|
13
|
-
bytes = fs.readFileSync(source_path);
|
|
14
|
-
value = JSON.parse(bytes.toString("utf8"));
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
throw new AppError("execution_profile_invalid", "Execution routing input must be a readable JSON file", 2, { source_path });
|
|
18
|
-
}
|
|
10
|
+
const { file: source_path, bytes } = prepareFileBytes(file, "execution routing");
|
|
11
|
+
const value = prepareJsonText(bytes.toString("utf8"), { file: source_path, label: "execution routing" }).value;
|
|
19
12
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
20
|
-
throw new AppError("execution_profile_invalid", "Execution routing input must be an object", 2, { source_path });
|
|
13
|
+
throw new AppError("execution_profile_invalid", "Execution routing input must be an object", 2, { source_path, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
21
14
|
}
|
|
22
15
|
const input = value;
|
|
23
16
|
if (input.schema_id !== "dd-flow/execution-routing@1"
|
|
24
17
|
|| Object.keys(input).some(key => key !== "schema_id" && key !== "execution")
|
|
25
18
|
|| !isExecutionRouting(input.execution)) {
|
|
26
|
-
throw new AppError("execution_profile_invalid", "Execution routing input does not match dd-flow/execution-routing@1", 2, { source_path });
|
|
19
|
+
throw new AppError("execution_profile_invalid", "Execution routing input does not match dd-flow/execution-routing@1", 2, { source_path, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
27
20
|
}
|
|
28
21
|
return { execution: input.execution, bytes, source_path, source_checksum: crypto.createHash("sha256").update(bytes).digest("hex") };
|
|
29
22
|
}
|