@deksden-com/dd-flow-cli 0.9.0-beta.145 → 0.9.0-beta.149
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 +71 -0
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +6 -2
- package/dist/cli/command-metadata.js +18 -2
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/memory-flow-admission.js +4 -2
- package/dist/cli/memory-init.js +5 -2
- package/dist/cli/run-cli.js +16 -15
- package/dist/cli.js +6 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +306 -34
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +70 -28
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +3 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +11 -3
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +1 -1
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/code-checks.js +23 -15
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +13 -6
- package/dist/services/controller-memory-worker.js +3 -2
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +22 -3
- package/dist/services/descriptor-stage-coordinator.js +37 -4
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/eval-snapshots.js +85 -29
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/flow-catalog.js +6 -1
- package/dist/services/frozen-engine.js +37 -2
- package/dist/services/hybrid-contracts.js +2 -0
- package/dist/services/installed-flow-source.js +25 -3
- package/dist/services/lifecycle-invocations.js +147 -34
- package/dist/services/memory-init-contexts.js +16 -0
- package/dist/services/memory-init-controller-start.js +12 -4
- package/dist/services/memory-init-lint-capability.js +5 -5
- package/dist/services/memory-init-native-capacity.js +4 -4
- package/dist/services/memory-init-preflight.js +10 -16
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +4 -3
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-verification-checks.js +15 -4
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/provider-capacity.js +43 -38
- package/dist/services/release-cache.js +8 -0
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +2 -1
- package/dist/services/run-controller-recovery.js +2 -1
- package/dist/services/run-controller.js +85 -15
- package/dist/services/run-fork.js +1 -1
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/stage-handlers.js +16 -3
- package/dist/services/stage-pause.js +4 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +31 -1
- package/dist/services/work-registry.js +37 -6
- package/dist/services/workspace-bootstrap.js +2 -1
- package/package.json +6 -3
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
3
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
4
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
|
+
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
6
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
7
|
+
import { readDescriptorRootWork } from "./descriptor-root-work.js";
|
|
8
|
+
import { diagnosticSnapshotPaths, readDiagnosticSnapshot, readDiagnosticSnapshotCustody } from "./diagnostic-snapshot.js";
|
|
9
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
10
|
+
import { validateDiagnosticStageResult } from "./diagnostic-stage-results.js";
|
|
11
|
+
import { diagnosticWorkBatch } from "./diagnostic-work-batch.js";
|
|
12
|
+
import { addWorkBatch, prepareWorkBatch, readWorkBatchCustody } from "./work-registry.js";
|
|
13
|
+
import { stageGraphScope } from "./stage-work-graph.js";
|
|
14
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
15
|
+
import { appendAudit } from "./audit.js";
|
|
16
|
+
const event = "diagnostic.work_registration";
|
|
17
|
+
function refuse(reason) { throw new AppError("diagnostic_work_registration_invalid", "Diagnostic graph lacks its assigned stage authority", 2, { reason, effect: "no_effect" }); }
|
|
18
|
+
/** SQL-only reader. An explicitly registered empty graph is distinct from no graph. */
|
|
19
|
+
export function readRegisteredDiagnosticGraph(context, scope) {
|
|
20
|
+
const graph = stageGraphScope(context, scope.projectId, scope.runId, scope.stage);
|
|
21
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? AND json_extract(payload_json,'$.attempt')=? LIMIT 2", [scope.projectId, event, scope.runId, scope.stage, graph.attempt]);
|
|
22
|
+
if (!rows.length)
|
|
23
|
+
return null;
|
|
24
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 2 * 1024 * 1024)
|
|
25
|
+
refuse("ambiguous_registration");
|
|
26
|
+
const receipt = JSON.parse(rows[0].payload_json);
|
|
27
|
+
if (receipt.schema_id !== "dd-flow/diagnostic-work-registration@1" || receipt.run_id !== scope.runId
|
|
28
|
+
|| receipt.stage !== scope.stage || receipt.attempt !== graph.attempt || !receipt.work_ids
|
|
29
|
+
|| receipt.input.run_id !== scope.runId || receipt.input.stage !== scope.stage
|
|
30
|
+
|| receipt.input.input_sha256 !== receipt.input_sha256)
|
|
31
|
+
refuse("foreign_registration");
|
|
32
|
+
const batch = diagnosticWorkBatch(receipt.input);
|
|
33
|
+
if (receipt.batch_sha256 !== hybridDigest(batch.works) || batch.works.length !== Object.keys(receipt.work_ids).length
|
|
34
|
+
|| new Set(Object.values(receipt.work_ids)).size !== batch.works.length)
|
|
35
|
+
refuse("changed_batch");
|
|
36
|
+
for (const item of batch.works) {
|
|
37
|
+
const id = receipt.work_ids[item.key];
|
|
38
|
+
if (!id)
|
|
39
|
+
refuse("missing_work");
|
|
40
|
+
const custody = readWorkBatchCustody(context, id);
|
|
41
|
+
if (custody.item_key !== item.key || custody.batch_sha256 !== receipt.batch_sha256
|
|
42
|
+
|| hybridDigest(custody.work_ids) !== hybridDigest(receipt.work_ids)
|
|
43
|
+
|| hybridDigest(custody.scope) !== hybridDigest({ ...graph, stage: scope.stage,
|
|
44
|
+
parent_work_id: receipt.parent_work_id, request_key: "diagnostic-graph" }))
|
|
45
|
+
refuse("changed_work_custody");
|
|
46
|
+
}
|
|
47
|
+
return receipt;
|
|
48
|
+
}
|
|
49
|
+
/** The registered handler calls this only after generic Stage entry. Applicability
|
|
50
|
+
* and candidates come from the accepted predecessor, not a caller-owned batch. */
|
|
51
|
+
export function registerDiagnosticStageGraph(context, input) {
|
|
52
|
+
const bundle = readFlowRunExecutionBundle(context, input), project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
53
|
+
if (!bundle || !isDiagnosticFlow(bundle.manifest.flow_id) || !["audit", "analyze", "review"].includes(input.stage))
|
|
54
|
+
refuse("diagnostic_stage_required");
|
|
55
|
+
const flowId = bundle.manifest.flow_id, frozen = bundle.execution.snapshot().scope;
|
|
56
|
+
const scope = { projectId: project.id, runId: input.runId, stage: input.stage };
|
|
57
|
+
const selection = readDescriptorStageInput(context, scope, bundle), root = readDescriptorRootWork(context, scope, bundle);
|
|
58
|
+
const snapshotSelection = { projectId: project.id, projectRoot: frozen.projectRoot, sourceRoot: frozen.projectRoot, requestId: frozen.requestId, flowId };
|
|
59
|
+
const snapshot = readDiagnosticSnapshot(context, snapshotSelection, input.runId);
|
|
60
|
+
if (!snapshot || snapshot.root !== frozen.workspace)
|
|
61
|
+
refuse("snapshot_required");
|
|
62
|
+
const previousStage = input.stage === "review" ? flowId === "mb-audit" ? "audit" : "analyze" : "preflight";
|
|
63
|
+
if (selection.input.predecessor?.stage !== previousStage)
|
|
64
|
+
refuse("predecessor_required");
|
|
65
|
+
const previousScope = { ...scope, stage: previousStage };
|
|
66
|
+
const accepted = readAcceptedStageData(context, previousScope), previousInput = readDescriptorStageInput(context, previousScope, bundle, true, true);
|
|
67
|
+
const previous = validateDiagnosticStageResult(accepted.bytes, bundle.descriptor.stageBlob(previousStage, "result_schema"), {
|
|
68
|
+
flowId, runId: input.runId, stage: previousStage, inputSha256: previousInput.inputSha256,
|
|
69
|
+
snapshotRoot: diagnosticSnapshotPaths(context, snapshot, input.runId).root, snapshotSha256: snapshot.source_sha256,
|
|
70
|
+
predecessors: previousInput.input.predecessor ? [{ stage: previousInput.input.predecessor.stage,
|
|
71
|
+
path: previousInput.input.predecessor.result.path, sha256: previousInput.input.predecessor.result.sha256 }] : []
|
|
72
|
+
});
|
|
73
|
+
const workInput = { flow_id: flowId, run_id: input.runId, stage: input.stage,
|
|
74
|
+
input_sha256: selection.inputSha256, snapshot_sha256: snapshot.source_sha256,
|
|
75
|
+
instruction: bundle.descriptor.stageBlob(input.stage, "instruction").toString("utf8"),
|
|
76
|
+
coverage: previous.details.coverage, candidates: input.stage === "review" ? previous.details.findings : [],
|
|
77
|
+
support: bundle.manifest.inventory.filter(ref => ref.path.startsWith("support/")).map(ref => ({ path: `execution-bundle/${ref.path}`, sha256: ref.sha256 })) };
|
|
78
|
+
const batch = diagnosticWorkBatch(workInput), batchInput = { parentWorkId: root.workId, stage: input.stage, requestKey: "diagnostic-graph", value: batch };
|
|
79
|
+
const prepared = batch.works.length ? prepareWorkBatch(context, batchInput) : [];
|
|
80
|
+
return context.db.writeTransaction(() => {
|
|
81
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
82
|
+
if (hybridDigest(readDiagnosticSnapshotCustody(context, snapshotSelection, input.runId)) !== hybridDigest(snapshot)
|
|
83
|
+
|| hybridDigest(readDescriptorStageInput(context, scope, bundle)) !== hybridDigest(selection)
|
|
84
|
+
|| hybridDigest(readAcceptedStageData(context, previousScope)) !== hybridDigest(accepted)
|
|
85
|
+
|| readDescriptorRootWork(context, scope, bundle).workId !== root.workId)
|
|
86
|
+
refuse("stage_authority_changed");
|
|
87
|
+
const expected = { schema_id: "dd-flow/diagnostic-work-registration@1", run_id: input.runId, stage: input.stage,
|
|
88
|
+
attempt: selection.attempt, parent_work_id: root.workId, bundle_sha256: bundle.sha256,
|
|
89
|
+
input_sha256: selection.inputSha256, input: workInput, batch_sha256: hybridDigest(batch.works) };
|
|
90
|
+
const prior = readRegisteredDiagnosticGraph(context, scope);
|
|
91
|
+
if (prior) {
|
|
92
|
+
if (hybridDigest({ ...prior, work_ids: undefined }) !== hybridDigest(expected))
|
|
93
|
+
refuse("registration_changed");
|
|
94
|
+
return prior;
|
|
95
|
+
}
|
|
96
|
+
const workIds = batch.works.length ? addWorkBatch(context, { ...batchInput, prepared }).work_ids : {};
|
|
97
|
+
const receipt = { ...expected, work_ids: workIds };
|
|
98
|
+
appendAudit(context, { projectId: project.id, eventType: event, payload: receipt });
|
|
99
|
+
return readRegisteredDiagnosticGraph(context, scope);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
2
|
+
import { verifyDiagnosticSources, verifyDiagnosticCritic } from "./diagnostic-evidence.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
const refuse = (reason) => {
|
|
6
|
+
throw new AppError("diagnostic_work_result_invalid", "Diagnostic Work result does not match its assigned task", 2, { reason, effect: "no_effect", ...(reason === "invalid_schema" ? { phase: "prepare", recoverable: true } : {}) });
|
|
7
|
+
};
|
|
8
|
+
/** Accepts semantic data only after the caller has established completed Work,
|
|
9
|
+
* immutable batch custody and physical Session independence. */
|
|
10
|
+
export function validateDiagnosticWorkResult(bytes, schemaBytes, snapshotRoot, input, assignment) {
|
|
11
|
+
if (!bytes.length || bytes.length > 1024 * 1024 || schemaBytes.length > 1024 * 1024)
|
|
12
|
+
refuse("result_exceeds_bound");
|
|
13
|
+
const validate = (() => {
|
|
14
|
+
try {
|
|
15
|
+
const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(schemaBytes));
|
|
16
|
+
if (schema.$id !== "dd-flow/diagnostic-work-result@1")
|
|
17
|
+
refuse("frozen_schema_invalid");
|
|
18
|
+
return new Ajv2020({ strict: false, validateFormats: false }).compile(schema);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return refuse("frozen_schema_invalid");
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
let result;
|
|
25
|
+
try {
|
|
26
|
+
result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return refuse("invalid_schema");
|
|
30
|
+
}
|
|
31
|
+
if (!validate(result))
|
|
32
|
+
refuse("invalid_schema");
|
|
33
|
+
if (result.flow_id !== input.flow_id || result.run_id !== input.run_id || result.stage !== input.stage
|
|
34
|
+
|| result.work_id !== assignment.workId || result.aspect_id !== assignment.aspectId)
|
|
35
|
+
refuse("foreign_assignment");
|
|
36
|
+
verifyDiagnosticSources(snapshotRoot, result.sources);
|
|
37
|
+
if (assignment.role === "critic") {
|
|
38
|
+
if (input.stage !== "review" || result.findings.length)
|
|
39
|
+
refuse("critic_cannot_replace_candidates");
|
|
40
|
+
verifyDiagnosticCritic(snapshotRoot, input.candidates, result.decisions);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (input.stage === "review" || result.decisions.length
|
|
44
|
+
|| !input.coverage.some(value => value.id === assignment.aspectId && value.applicability === "applicable"))
|
|
45
|
+
refuse("foreign_aspect");
|
|
46
|
+
const ids = new Set();
|
|
47
|
+
for (const finding of result.findings) {
|
|
48
|
+
if (ids.has(finding.id) || finding.aspect_id !== assignment.aspectId || !finding.evidence.length)
|
|
49
|
+
refuse("foreign_or_duplicate_finding");
|
|
50
|
+
ids.add(finding.id);
|
|
51
|
+
verifyDiagnosticSources(snapshotRoot, finding.evidence);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
/** Work-local IDs are not globally unique. Keep their origin when assigning
|
|
57
|
+
* stable report IDs, so one author's FIND-1 cannot overwrite another's. */
|
|
58
|
+
export function collectDiagnosticCandidates(results) {
|
|
59
|
+
const findings = [], origins = [];
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
for (const [workIndex, item] of results.entries()) {
|
|
62
|
+
if (seen.has(item.workId) || item.result.work_id !== item.workId || item.result.stage === "review")
|
|
63
|
+
refuse("duplicate_or_foreign_work");
|
|
64
|
+
seen.add(item.workId);
|
|
65
|
+
for (const [findingIndex, finding] of item.result.findings.entries()) {
|
|
66
|
+
const id = `FIND-${String(workIndex + 1).padStart(3, "0")}-${String(findingIndex + 1).padStart(3, "0")}`;
|
|
67
|
+
findings.push({ ...structuredClone(finding), id });
|
|
68
|
+
origins.push({ id, work_id: item.workId, original_id: finding.id });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { findings, origins };
|
|
72
|
+
}
|
|
73
|
+
/** Assign CLI-owned global IDs only to an exact, ordered copy of admitted Work
|
|
74
|
+
* findings. Any other proposal still goes through the ordinary strict guards. */
|
|
75
|
+
export function sealDiagnosticCandidateIds(bytes, results) {
|
|
76
|
+
if (!bytes.length || bytes.length > 1024 * 1024)
|
|
77
|
+
return bytes;
|
|
78
|
+
let value;
|
|
79
|
+
try {
|
|
80
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return bytes;
|
|
84
|
+
}
|
|
85
|
+
if (!value || !Array.isArray(value.details?.findings)
|
|
86
|
+
|| hybridDigest(value.details.findings) !== hybridDigest(results.flatMap(item => item.result.findings)))
|
|
87
|
+
return bytes;
|
|
88
|
+
const canonical = collectDiagnosticCandidates(results).findings;
|
|
89
|
+
if (hybridDigest(value.details.findings) === hybridDigest(canonical))
|
|
90
|
+
return bytes;
|
|
91
|
+
value.details.findings = canonical;
|
|
92
|
+
return Buffer.from(JSON.stringify(value));
|
|
93
|
+
}
|
|
@@ -23,12 +23,13 @@ import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-f
|
|
|
23
23
|
import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
24
24
|
import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
|
|
25
25
|
import { readSetupStatus } from "./setup.js";
|
|
26
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
26
27
|
import { isObservationOperation, nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
|
|
27
28
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
28
29
|
export function prepareEvalBootstrapSnapshot(context, input) {
|
|
29
30
|
let source;
|
|
30
31
|
if (input.flow !== undefined && input.flow !== "sdlc") {
|
|
31
|
-
if (input.flow !== "mb-init" && input.flow !== "mb-upgrade")
|
|
32
|
+
if (input.flow !== "mb-init" && input.flow !== "mb-upgrade" && !isDiagnosticFlow(input.flow))
|
|
32
33
|
throw new AppError("snapshot_flow_unsupported", "Bootstrap snapshot flow is not registered", 2, { phase: "prepare", effect: "no_effect" });
|
|
33
34
|
const home = context.env.DD_FLOW_CONFIG_HOME ?? context.ddFlowHome;
|
|
34
35
|
const selected = readSetupStatus(home);
|
|
@@ -70,7 +71,7 @@ export function createEvalBootstrapSnapshot(context, input) {
|
|
|
70
71
|
const manifest = {
|
|
71
72
|
schema_id: source ? "dd-eval/bootstrap-snapshot@2" : "dd-eval/bootstrap-snapshot@1",
|
|
72
73
|
stage: source?.resolveRegisteredFlow().descriptor.stages[0]?.id ?? "specify",
|
|
73
|
-
...(source ? { flow: { id:
|
|
74
|
+
...(source ? { flow: { id: source.resolveRegisteredFlow().descriptor.id, source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
|
|
74
75
|
project_root: projectRoot,
|
|
75
76
|
project_sha256: treeHash(path.join(temporary, "project")),
|
|
76
77
|
project_git: snapshotProjectGit(projectRoot, temporary)
|
|
@@ -222,7 +223,7 @@ export function prepareEvalRunSnapshotCreation(context, input) {
|
|
|
222
223
|
throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
|
|
223
224
|
prepareOutputFile(output);
|
|
224
225
|
prepareSnapshotProjectGit(projectRoot, recovery && status.run.flow_kind === "mb-init");
|
|
225
|
-
prepareSnapshotWorkspace(workspaceRoot, projectRoot);
|
|
226
|
+
prepareSnapshotWorkspace(workspaceRoot, projectRoot, isDiagnosticFlow(status.run.flow_kind));
|
|
226
227
|
return { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren };
|
|
227
228
|
}
|
|
228
229
|
function captureEvalRunSnapshot(context, input) {
|
|
@@ -240,7 +241,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
240
241
|
}
|
|
241
242
|
}
|
|
242
243
|
else
|
|
243
|
-
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions], grokProviderHomes(context.ddFlowHome), input.observationCuts);
|
|
244
|
+
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions, ...(isDiagnosticFlow(status.run.flow_kind) ? [workspaceRoot] : [])], grokProviderHomes(context.ddFlowHome), input.observationCuts);
|
|
244
245
|
for (const [file, cut] of input.journalCuts ?? []) {
|
|
245
246
|
const target = path.join(output, "runtime", path.relative(context.ddFlowHome, file));
|
|
246
247
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -280,7 +281,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
280
281
|
filter: projectPayloadFilter(projectRoot)
|
|
281
282
|
});
|
|
282
283
|
const projectGit = snapshotProjectGit(projectRoot, output, recovery && status.run.flow_kind === "mb-init");
|
|
283
|
-
const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
|
|
284
|
+
const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output, isDiagnosticFlow(status.run.flow_kind));
|
|
284
285
|
// Accepted Work results are captured in SQLite. Their file representations
|
|
285
286
|
// are retained evidence, not a second authority or a checkpoint gate.
|
|
286
287
|
if (recovery) {
|
|
@@ -293,7 +294,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
293
294
|
}
|
|
294
295
|
}
|
|
295
296
|
const manifest = {
|
|
296
|
-
schema_id: projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
|
|
297
|
+
schema_id: workspace.kind === "git_repository" ? "dd-flow/eval-run-snapshot@7" : projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
|
|
297
298
|
run_id: input.runId,
|
|
298
299
|
project_id: project.id,
|
|
299
300
|
project_root: projectRoot,
|
|
@@ -310,7 +311,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
310
311
|
...(input.observationCuts ? { observation_cuts: [...input.observationCuts].map(([file, receipt]) => ({ path: path.relative(context.ddFlowHome, file), receipt })) } : {}),
|
|
311
312
|
project_git: projectGit,
|
|
312
313
|
workspace,
|
|
313
|
-
payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind
|
|
314
|
+
payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind !== "project" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
|
|
314
315
|
source_status: status
|
|
315
316
|
};
|
|
316
317
|
writeJson(path.join(output, "snapshot.json"), manifest);
|
|
@@ -436,7 +437,7 @@ export function prepareSnapshotOperationalDecision(input) {
|
|
|
436
437
|
}
|
|
437
438
|
function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest) {
|
|
438
439
|
assertRestoreTargetSeparation(snapshot, [projectRoot, context.ddFlowHome]);
|
|
439
|
-
if (manifest.workspace.kind
|
|
440
|
+
if (manifest.workspace.kind !== "project") {
|
|
440
441
|
const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
|
|
441
442
|
assertRestoreTargetSeparation(snapshot, [workspaceRoot]);
|
|
442
443
|
if (isInside(physicalPath(projectRoot), physicalPath(workspaceRoot)) || isInside(physicalPath(workspaceRoot), physicalPath(projectRoot)) || physicalPath(workspaceRoot) === physicalPath(context.ddFlowHome))
|
|
@@ -760,27 +761,29 @@ function rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspa
|
|
|
760
761
|
function rebasePathString(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { for (const [oldPrefix, newPrefix] of [[oldHome, newHome], [oldWorkspace, newWorkspace], [oldRoot, newRoot]])
|
|
761
762
|
if (value === oldPrefix || value.startsWith(`${oldPrefix}${path.sep}`))
|
|
762
763
|
return `${newPrefix}${value.slice(oldPrefix.length)}`; return value; }
|
|
763
|
-
function prepareSnapshotWorkspace(workspaceRoot, projectRoot) {
|
|
764
|
+
function prepareSnapshotWorkspace(workspaceRoot, projectRoot, allowUnborn = false) {
|
|
764
765
|
if (samePath(workspaceRoot, projectRoot))
|
|
765
766
|
return null;
|
|
766
767
|
const branch = git(workspaceRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
767
768
|
if (!branch)
|
|
768
769
|
throw new AppError("snapshot_workspace_invalid", "Feature workspace must be on a named Git branch", 1, { workspace_root: workspaceRoot });
|
|
769
770
|
const baseRef = git(workspaceRoot, ["rev-parse", "HEAD"]);
|
|
770
|
-
if (!baseRef)
|
|
771
|
+
if (!baseRef && !(allowUnborn && git(workspaceRoot, ['for-each-ref', '--format=%(refname)']) === ''))
|
|
771
772
|
throw new AppError("snapshot_workspace_invalid", "Feature workspace has no readable HEAD", 1, { workspace_root: workspaceRoot });
|
|
772
773
|
return { branch, baseRef };
|
|
773
774
|
}
|
|
774
|
-
function snapshotWorkspace(workspaceRoot, projectRoot, output) {
|
|
775
|
-
const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot);
|
|
775
|
+
function snapshotWorkspace(workspaceRoot, projectRoot, output, independent = false) {
|
|
776
|
+
const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot, independent);
|
|
776
777
|
if (!prepared)
|
|
777
778
|
return { kind: "project" };
|
|
778
779
|
const { branch, baseRef } = prepared;
|
|
779
780
|
const workspace = path.join(output, "workspace");
|
|
780
781
|
const bundle = path.join(output, "workspace.bundle");
|
|
781
782
|
fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: projectPayloadFilter(workspaceRoot) });
|
|
782
|
-
|
|
783
|
-
|
|
783
|
+
if (baseRef === null)
|
|
784
|
+
return { kind: 'git_repository', source_root: workspaceRoot, branch, base_ref: null, bundle_sha256: null, state: 'unborn', sha256: treeHash(workspace), ...snapshotIndex(workspaceRoot, output, 'workspace') };
|
|
785
|
+
runGit(independent ? workspaceRoot : projectRoot, ["bundle", "create", bundle, branch]);
|
|
786
|
+
return { kind: independent ? "git_repository" : "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle), ...snapshotIndex(workspaceRoot, output, "workspace") };
|
|
784
787
|
}
|
|
785
788
|
function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
|
|
786
789
|
if (manifest.workspace.kind === "project")
|
|
@@ -789,15 +792,28 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
|
|
|
789
792
|
const bundle = path.join(snapshot, "workspace.bundle");
|
|
790
793
|
if (treeHash(source) !== manifest.workspace.sha256)
|
|
791
794
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace checksum does not match manifest", 1, { snapshot });
|
|
792
|
-
if (!fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
|
|
795
|
+
if (manifest.workspace.state === 'unborn' ? fs.existsSync(bundle) : !fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
|
|
793
796
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace Git bundle does not match manifest", 1, { snapshot });
|
|
794
797
|
const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome);
|
|
795
798
|
if (fs.existsSync(workspaceRoot))
|
|
796
799
|
throw new AppError("snapshot_workspace_target_exists", "Snapshot feature workspace target already exists", 1, { workspace_root: workspaceRoot });
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
800
|
+
if (manifest.workspace.kind === 'git_repository') {
|
|
801
|
+
fs.mkdirSync(path.dirname(workspaceRoot), { recursive: true });
|
|
802
|
+
if (manifest.workspace.state === 'unborn') {
|
|
803
|
+
fs.mkdirSync(workspaceRoot);
|
|
804
|
+
runGit(workspaceRoot, ['-c', 'init.templateDir=', 'init', '--initial-branch', manifest.workspace.branch]);
|
|
805
|
+
}
|
|
806
|
+
else
|
|
807
|
+
runGit(projectRoot, ['clone', '--no-checkout', '--branch', manifest.workspace.branch, bundle, workspaceRoot]);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
|
|
811
|
+
if (git(projectRoot, ["rev-parse", `refs/heads/${manifest.workspace.branch}`]) !== manifest.workspace.base_ref)
|
|
812
|
+
throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen feature base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
|
|
813
|
+
runGit(projectRoot, ["worktree", "add", workspaceRoot, manifest.workspace.branch]);
|
|
814
|
+
}
|
|
815
|
+
if (git(workspaceRoot, ["rev-parse", "HEAD"]) !== manifest.workspace.base_ref)
|
|
816
|
+
throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen workspace base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
|
|
801
817
|
replaceProjectTree(workspaceRoot, source);
|
|
802
818
|
restoreIndex(snapshot, "workspace", manifest.workspace, workspaceRoot);
|
|
803
819
|
return fs.realpathSync(workspaceRoot);
|
|
@@ -847,7 +863,7 @@ function restoreProjectGit(snapshot, source, projectRoot) {
|
|
|
847
863
|
restoreIndex(snapshot, "project", source, projectRoot);
|
|
848
864
|
}
|
|
849
865
|
const mergeStateFiles = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "ORIG_HEAD", "AUTO_MERGE"];
|
|
850
|
-
function indexEntries(root) {
|
|
866
|
+
function indexEntries(root, includeExclude = true) {
|
|
851
867
|
const entries = Buffer.from(gitBytes(root, ["ls-files", "--stage", "-z"])
|
|
852
868
|
.toString("utf8").split("\0").filter(entry => entry && localEnvironmentAllowed(entry.slice(entry.indexOf("\t") + 1)))
|
|
853
869
|
.map(entry => `${entry}\0`).join(""));
|
|
@@ -860,7 +876,22 @@ function indexEntries(root) {
|
|
|
860
876
|
if (fs.existsSync(resolved))
|
|
861
877
|
merge[name] = fs.readFileSync(resolved).toString("base64");
|
|
862
878
|
}
|
|
863
|
-
|
|
879
|
+
if (!includeExclude)
|
|
880
|
+
return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge }));
|
|
881
|
+
const excludePath = git(root, ["rev-parse", "--git-path", "info/exclude"]);
|
|
882
|
+
if (!excludePath)
|
|
883
|
+
throw new AppError("snapshot_index_invalid", "Could not locate Git local excludes", 1);
|
|
884
|
+
const excludeFile = path.resolve(root, excludePath);
|
|
885
|
+
let exclude = null;
|
|
886
|
+
if (fs.existsSync(excludeFile)) {
|
|
887
|
+
const stat = fs.lstatSync(excludeFile);
|
|
888
|
+
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
889
|
+
throw new AppError("snapshot_index_invalid", "Git local excludes must be a bounded regular file", 1);
|
|
890
|
+
exclude = fs.readFileSync(excludeFile).toString("base64");
|
|
891
|
+
}
|
|
892
|
+
// Local excludes affect review-visible inputs, but are not part of a Git
|
|
893
|
+
// bundle/index. Keep them in the existing hashed, non-executable Git payload.
|
|
894
|
+
return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge, exclude }));
|
|
864
895
|
}
|
|
865
896
|
function snapshotIndex(root, output, name) {
|
|
866
897
|
const bytes = indexEntries(root);
|
|
@@ -911,6 +942,8 @@ function restoreIndex(snapshot, name, metadata, root) {
|
|
|
911
942
|
if (!metadata.index_objects_sha256 || fileHash(pack) !== metadata.index_objects_sha256)
|
|
912
943
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot Git index object checksum does not match", 1);
|
|
913
944
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
945
|
+
if (value.exclude !== undefined && value.exclude !== null && (typeof value.exclude !== "string" || Buffer.byteLength(value.exclude) > 1400000))
|
|
946
|
+
throw new AppError("snapshot_index_invalid", "Snapshot Git local excludes are invalid", 1);
|
|
914
947
|
gitBytes(root, ["unpack-objects"], fs.readFileSync(pack));
|
|
915
948
|
runGit(root, ["read-tree", "--empty"]);
|
|
916
949
|
gitBytes(root, ["update-index", "-z", "--index-info"], Buffer.from(value.entries, "base64"));
|
|
@@ -923,18 +956,36 @@ function restoreIndex(snapshot, name, metadata, root) {
|
|
|
923
956
|
else
|
|
924
957
|
fs.rmSync(path.resolve(root, target), { force: true });
|
|
925
958
|
}
|
|
959
|
+
if (value.exclude !== undefined) {
|
|
960
|
+
const target = git(root, ["rev-parse", "--git-path", "info/exclude"]);
|
|
961
|
+
if (!target)
|
|
962
|
+
throw new AppError("snapshot_index_invalid", "Could not locate restored Git local excludes", 1);
|
|
963
|
+
const destination = path.resolve(root, target);
|
|
964
|
+
if (fs.existsSync(destination) && !fs.lstatSync(destination).isFile())
|
|
965
|
+
throw new AppError("snapshot_index_invalid", "Restored Git local excludes are not a regular file", 1);
|
|
966
|
+
if (value.exclude === null)
|
|
967
|
+
fs.rmSync(destination, { force: true });
|
|
968
|
+
else {
|
|
969
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
970
|
+
fs.writeFileSync(destination, Buffer.from(value.exclude, "base64"));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
926
973
|
return;
|
|
927
974
|
}
|
|
928
975
|
if (fs.statSync(file).size)
|
|
929
976
|
runGit(root, ["apply", "--cached", "--binary", file]);
|
|
930
977
|
}
|
|
978
|
+
function capturedIndexEntries(snapshot, name, root) {
|
|
979
|
+
const captured = JSON.parse(fs.readFileSync(path.join(snapshot, `${name}.index.patch`), "utf8"));
|
|
980
|
+
return indexEntries(root, Object.hasOwn(captured, "exclude"));
|
|
981
|
+
}
|
|
931
982
|
export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true) {
|
|
932
983
|
const manifest = readManifest(snapshot);
|
|
933
984
|
if (manifest.purpose !== "recovery" || manifest.recovery_id !== input.recoveryId || manifest.run_id !== input.runId)
|
|
934
985
|
throw new AppError("recovery_capture_invalid", "Snapshot does not identify this recovery", 1);
|
|
935
986
|
const entries = [
|
|
936
987
|
{ name: "project", root: input.projectRoot, hash: manifest.project_sha256, git: manifest.project_git, head: manifest.project_git.head },
|
|
937
|
-
...(manifest.workspace.kind
|
|
988
|
+
...(manifest.workspace.kind !== "project" ? [{ name: "workspace", root: input.workspaceRoot, hash: manifest.workspace.sha256, git: manifest.workspace, head: manifest.workspace.base_ref }] : [])
|
|
938
989
|
];
|
|
939
990
|
verifySnapshotPayload(snapshot, manifest);
|
|
940
991
|
for (const entry of entries) {
|
|
@@ -944,7 +995,7 @@ export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true
|
|
|
944
995
|
throw new AppError("recovery_workspace_drift", "Executable file modes changed after recovery capture", 1, { payload: entry.name });
|
|
945
996
|
if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "--verify", "HEAD"]) !== entry.head
|
|
946
997
|
|| entry.head === null && git(entry.root, ["for-each-ref", "--format=%(refname)"]) !== ""
|
|
947
|
-
|| git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ?
|
|
998
|
+
|| git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? capturedIndexEntries(snapshot, entry.name, entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
|
|
948
999
|
throw new AppError("recovery_workspace_drift", "Working files or Git state changed after recovery capture", 1, { payload: entry.name });
|
|
949
1000
|
}
|
|
950
1001
|
}
|
|
@@ -964,7 +1015,7 @@ function verifySnapshotPayload(snapshot, manifest) {
|
|
|
964
1015
|
if (treeHash(path.join(snapshot, "runtime")) !== manifest.runtime_sha256)
|
|
965
1016
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot runtime checksum does not match", 1);
|
|
966
1017
|
verifySnapshotModes(snapshot, manifest);
|
|
967
|
-
const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind
|
|
1018
|
+
const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind !== "project" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
|
|
968
1019
|
for (const payload of payloads)
|
|
969
1020
|
verifyGitSnapshotPayload(snapshot, payload);
|
|
970
1021
|
if (manifest.schema_id === "dd-flow/eval-run-snapshot@6") {
|
|
@@ -1433,7 +1484,7 @@ function treeModesHash(root, filtered = false) {
|
|
|
1433
1484
|
function verifySnapshotModes(snapshot, manifest) {
|
|
1434
1485
|
if (!manifest.payload_modes)
|
|
1435
1486
|
return; // Legacy snapshots did not attest modes.
|
|
1436
|
-
for (const name of ["project", "runtime", ...(manifest.workspace.kind
|
|
1487
|
+
for (const name of ["project", "runtime", ...(manifest.workspace.kind !== "project" ? ["workspace"] : [])]) {
|
|
1437
1488
|
if (treeModesHash(path.join(snapshot, name)) !== manifest.payload_modes[name])
|
|
1438
1489
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot executable modes do not match manifest", 1, { payload: name });
|
|
1439
1490
|
}
|
|
@@ -1465,7 +1516,7 @@ function readManifest(snapshot) {
|
|
|
1465
1516
|
if (!fs.existsSync(file))
|
|
1466
1517
|
throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
|
|
1467
1518
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1468
|
-
if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6"].includes(String(value.schema_id)) || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
|
|
1519
|
+
if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6", "dd-flow/eval-run-snapshot@7"].includes(String(value.schema_id)) || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
|
|
1469
1520
|
throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
|
|
1470
1521
|
if (value.runtime_payload_policy !== undefined && (value.runtime_payload_policy !== "flow-evidence@1" || value.purpose === "recovery"))
|
|
1471
1522
|
throw new AppError("snapshot_invalid", "Snapshot runtime payload policy is invalid for its purpose", 1, { snapshot });
|
|
@@ -1480,8 +1531,13 @@ function readManifest(snapshot) {
|
|
|
1480
1531
|
|| spawnSync("git", ["check-ref-format", "--branch", value.project_git.branch], { encoding: "utf8" }).status !== 0
|
|
1481
1532
|
: value.project_git.state !== undefined || !value.project_git.head || !value.project_git.bundle_sha256))
|
|
1482
1533
|
throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
|
|
1483
|
-
if (
|
|
1484
|
-
|
|
1534
|
+
if (!['project', 'git_worktree', 'git_repository'].includes(value.workspace.kind)
|
|
1535
|
+
|| (value.schema_id === 'dd-flow/eval-run-snapshot@7') !== (value.workspace.kind === 'git_repository')
|
|
1536
|
+
|| value.workspace.kind !== 'project' && (!value.workspace.branch || (value.workspace.state === 'unborn'
|
|
1537
|
+
? value.workspace.kind !== 'git_repository' || value.workspace.base_ref !== null || value.workspace.bundle_sha256 !== null
|
|
1538
|
+
|| value.workspace.index_format !== 'entries@1' || !value.workspace.index_sha256 || !value.workspace.index_objects_sha256
|
|
1539
|
+
: value.workspace.state !== undefined || !value.workspace.base_ref || !value.workspace.bundle_sha256)))
|
|
1540
|
+
throw new AppError("snapshot_invalid", "Snapshot workspace manifest is incomplete or incompatible", 1, { snapshot });
|
|
1485
1541
|
return value;
|
|
1486
1542
|
}
|
|
1487
1543
|
function readBootstrapManifest(snapshot) {
|
|
@@ -1490,7 +1546,7 @@ function readBootstrapManifest(snapshot) {
|
|
|
1490
1546
|
throw new AppError("snapshot_missing", "Bootstrap snapshot manifest is missing", 1, { snapshot });
|
|
1491
1547
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1492
1548
|
const validBoundary = value.schema_id === "dd-eval/bootstrap-snapshot@1" ? value.stage === "specify" && value.flow === undefined
|
|
1493
|
-
: value.schema_id === "dd-eval/bootstrap-snapshot@2" && (value.flow?.id === "mb-init" || value.flow?.id === "mb-upgrade") && value.stage === "preflight"
|
|
1549
|
+
: value.schema_id === "dd-eval/bootstrap-snapshot@2" && (value.flow?.id === "mb-init" || value.flow?.id === "mb-upgrade" || isDiagnosticFlow(value.flow?.id)) && value.stage === "preflight"
|
|
1494
1550
|
&& Object.keys(value.flow).sort().join() === "descriptor_sha256,id,source_sha256"
|
|
1495
1551
|
&& /^[a-f0-9]{64}$/.test(value.flow.source_sha256) && /^[a-f0-9]{64}$/.test(value.flow.descriptor_sha256);
|
|
1496
1552
|
if (!validBoundary || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
|
|
@@ -9,6 +9,7 @@ import { FrozenGenericExecution } from "./generic-execution.js";
|
|
|
9
9
|
import { hybridDigest, validateHybridContract, validateHybridShape } from "./hybrid-contracts.js";
|
|
10
10
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
11
11
|
import { acquireInstallationLock } from "./installation-lock.js";
|
|
12
|
+
import { projectPacketLocators } from "./stage-context.js";
|
|
12
13
|
const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
13
14
|
const limit = 1024 * 1024;
|
|
14
15
|
function refuse(reason, issues) {
|
|
@@ -54,7 +55,8 @@ export class RetainedExecutionBundle {
|
|
|
54
55
|
#blobs;
|
|
55
56
|
#admission;
|
|
56
57
|
#engine;
|
|
57
|
-
|
|
58
|
+
#engineRelocation;
|
|
59
|
+
constructor(bytes, manifest, blobs, admission, descriptor, execution, engineRelocation) {
|
|
58
60
|
frozen(manifest);
|
|
59
61
|
this.manifest = manifest;
|
|
60
62
|
this.sha256 = hash(bytes);
|
|
@@ -63,9 +65,10 @@ export class RetainedExecutionBundle {
|
|
|
63
65
|
this.#admission = admission;
|
|
64
66
|
this.descriptor = descriptor;
|
|
65
67
|
this.execution = execution;
|
|
68
|
+
this.#engineRelocation = engineRelocation && structuredClone(engineRelocation);
|
|
66
69
|
Object.freeze(this);
|
|
67
70
|
}
|
|
68
|
-
static admit(input, expectedSha256, sourceBlobs, selected) {
|
|
71
|
+
static admit(input, expectedSha256, sourceBlobs, selected, engineRelocation) {
|
|
69
72
|
if (input.byteLength > limit || !/^[a-f0-9]{64}$/.test(expectedSha256))
|
|
70
73
|
refuse("invalid_bundle_identity");
|
|
71
74
|
const bytes = Buffer.from(input);
|
|
@@ -113,12 +116,12 @@ export class RetainedExecutionBundle {
|
|
|
113
116
|
const snapshot = { schema_id: "dd-flow/frozen-execution@1", scope: admission.executionScope,
|
|
114
117
|
settings: json(blobs.get(manifest.execution_settings.path)), profiles: profiles.profiles };
|
|
115
118
|
const execution = FrozenGenericExecution.restore(snapshot, hybridDigest(snapshot), descriptor, admission.executionScope, admission.receipts);
|
|
116
|
-
const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution);
|
|
119
|
+
const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution, engineRelocation);
|
|
117
120
|
admitted.engine();
|
|
118
121
|
return admitted;
|
|
119
122
|
}
|
|
120
123
|
/** Read only the retained inventory. Mutable canon/profile sources are never consulted. */
|
|
121
|
-
static read(root, expectedSha256, admission) {
|
|
124
|
+
static read(root, expectedSha256, admission, engineRelocation) {
|
|
122
125
|
try {
|
|
123
126
|
directory(root);
|
|
124
127
|
const bytes = readBoundedReleaseFile(path.join(root, "manifest.json"), limit);
|
|
@@ -130,7 +133,7 @@ export class RetainedExecutionBundle {
|
|
|
130
133
|
const manifest = value, blobs = new Map();
|
|
131
134
|
for (const ref of manifest.inventory)
|
|
132
135
|
blobs.set(ref.path, readBoundedReleaseFile(fileIn(root, ref.path), limit));
|
|
133
|
-
return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission);
|
|
136
|
+
return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission, engineRelocation);
|
|
134
137
|
}
|
|
135
138
|
catch (error) {
|
|
136
139
|
if (error instanceof AppError && ["execution_bundle_invalid", "frozen_engine_invalid"].includes(error.code))
|
|
@@ -153,8 +156,17 @@ export class RetainedExecutionBundle {
|
|
|
153
156
|
}
|
|
154
157
|
#admitEngine() {
|
|
155
158
|
const binding = json(this.blob(this.manifest.engine_binding.path));
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
159
|
+
const retainedManifest = this.blob(this.manifest.engine_manifest.path), relocation = this.#engineRelocation;
|
|
160
|
+
if (relocation) {
|
|
161
|
+
const root = JSON.parse(projectPacketLocators(JSON.stringify({ root: binding.engine.snapshot_root }), relocation.relocations)).root;
|
|
162
|
+
if (!relocation.relocations.length || root === binding.engine.snapshot_root || root !== relocation.binding.engine.snapshot_root
|
|
163
|
+
|| relocation.binding.run_id !== this.manifest.run_id)
|
|
164
|
+
refuse("foreign_engine_relocation");
|
|
165
|
+
}
|
|
166
|
+
const engine = relocation
|
|
167
|
+
? AdmittedFrozenEngine.admitRelocation(binding, retainedManifest, this.manifest.engine_manifest.sha256, relocation.binding)
|
|
168
|
+
: AdmittedFrozenEngine.admit(binding, this.manifest.engine_manifest.sha256);
|
|
169
|
+
if (!relocation && !engine.manifestBytes().equals(retainedManifest))
|
|
158
170
|
refuse("engine_manifest_bytes_changed");
|
|
159
171
|
const manifest = engine.manifest(), contracts = new Set(Object.values(manifest.supports_contracts).flat());
|
|
160
172
|
for (const id of ["dd-flow/flow-descriptor@1", "dd-flow/execution-settings@1", "dd-flow/execution-bundle@1"]) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { vnextStages } from "../domain/stage-catalog.js";
|
|
2
2
|
import { sdlcHandlerIdentities, memoryInitHandlerIdentities, memoryUpgradeHandlerIdentities } from "../domain/stage-handler-identities.js";
|
|
3
3
|
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { diagnosticFlowStages } from "../domain/diagnostic-flows.js";
|
|
4
5
|
/** Engine-owned catalog, not descriptor discovery or permission to execute a RUN. */
|
|
5
6
|
export function listBundledFlows() {
|
|
6
7
|
return [
|
|
@@ -14,7 +15,11 @@ export function listBundledFlows() {
|
|
|
14
15
|
stages: stages.map((handler, index) => ({ id: handler.stage, directory: `${String(index + 1).padStart(2, "0")}-${handler.stage}`, entry_handler: handler.entryId, finish_handler: handler.finishId })),
|
|
15
16
|
edges: stages.slice(0, -1).flatMap((handler, index) => [{ from: handler.stage, to: stages[index + 1].stage, on: "accepted" },
|
|
16
17
|
...(handler.stage === "verify" ? [{ from: handler.stage, to: "review", on: "needs_repair" }] : [])]) };
|
|
17
|
-
})
|
|
18
|
+
}),
|
|
19
|
+
...Object.entries(diagnosticFlowStages).map(([id, stages]) => ({ id, handler_status: "registered", entry_stage: "preflight", terminal_stages: ["report"],
|
|
20
|
+
stages: stages.map((stage, index) => ({ id: stage, directory: `${String(index + 1).padStart(2, "0")}-${stage}`,
|
|
21
|
+
entry_handler: "diagnostic-stage-entry", finish_handler: "diagnostic-stage-finish" })),
|
|
22
|
+
edges: stages.slice(0, -1).map((from, index) => ({ from, to: stages[index + 1], on: "accepted" })) }))
|
|
18
23
|
];
|
|
19
24
|
}
|
|
20
25
|
export function showBundledFlow(id) {
|