@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
|
@@ -5,14 +5,24 @@ import path from "node:path";
|
|
|
5
5
|
import { parse } from "shell-quote";
|
|
6
6
|
import { AppError } from "../shared/errors.js";
|
|
7
7
|
import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
|
|
8
|
+
import { isDeepStrictEqual } from "node:util";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
8
10
|
function object(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
9
11
|
function boundedString(value, bytes) { return typeof value === "string" && Buffer.byteLength(value) <= bytes; }
|
|
10
12
|
const validatedBrowserProofs = new WeakSet();
|
|
13
|
+
const unsupportedObservationContracts = new WeakSet();
|
|
14
|
+
const validatedNativeProofs = new WeakSet();
|
|
15
|
+
const validatedChildren = new WeakMap();
|
|
16
|
+
const validatedReports = new WeakMap();
|
|
17
|
+
const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
18
|
+
const absoluteFile = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") && path.isAbsolute(value);
|
|
19
|
+
export function candidateAbsencesValid(value) { return Array.isArray(value) && value.length <= 64 && value.every(item => object(item) && absoluteFile(item.file) && item.path_state === "missing" && object(item.declaration) && absoluteFile(item.declaration.file) && hash(item.declaration.sha256)); }
|
|
11
20
|
function relativeProofPath(value) { return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= 4096 && !path.isAbsolute(value) && !value.includes("\\") && value.split("/").every(part => part !== "" && part !== "." && part !== ".."); }
|
|
12
21
|
function proofArtifact(value) { return object(value) && relativeProofPath(value.relative_path) && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256); }
|
|
13
22
|
function browserProofShape(proof) {
|
|
14
23
|
const attempt = proof.attempt, step = proof.step;
|
|
15
|
-
return proof.
|
|
24
|
+
return (proof.observation_contract === undefined || proof.observation_contract === "dd-tasks/browser-observation@2" && object(proof.target) && [proof.target.page_id, proof.target.context_id].every(value => boundedString(value, 256) && Boolean(value)) && Number.isSafeInteger(proof.window_epoch) && Number(proof.window_epoch) > 0)
|
|
25
|
+
&& proof.native_code === "PLAYWRIGHT_ACTION_INACTIVITY" && object(attempt) && boundedString(attempt.invocation_id, 4096) && boundedString(attempt.test_id, 4096) && Boolean(attempt.test_id)
|
|
16
26
|
&& [attempt.retry, attempt.repeat_each_index, attempt.worker_index].every(value => Number.isSafeInteger(value) && value >= 0)
|
|
17
27
|
&& object(step) && boundedString(step.id, 4096) && Boolean(step.id) && ["pw:api", "expect"].includes(String(step.category)) && step.file === proof.file && Number.isSafeInteger(step.line) && step.line > 0
|
|
18
28
|
&& proofArtifact(proof.worker_snapshot) && proofArtifact(proof.native_result);
|
|
@@ -29,6 +39,21 @@ export function runnerFailureValid(value) {
|
|
|
29
39
|
return false;
|
|
30
40
|
if (value.repair_evidence !== undefined) {
|
|
31
41
|
const proof = value.repair_evidence;
|
|
42
|
+
if (object(proof) && proof.rule === "candidate_test_runtime_error")
|
|
43
|
+
return ["TypeError", "ReferenceError"].includes(String(proof.exception_type))
|
|
44
|
+
&& absoluteFile(proof.file) && hash(proof.file_sha256) && ["test", "setup", "collection"].includes(String(proof.phase))
|
|
45
|
+
&& [proof.line, proof.column].every(value => Number.isSafeInteger(value) && Number(value) > 0) && proofArtifact(proof.native_artifact);
|
|
46
|
+
if (object(proof) && proof.rule === "candidate_migration_sql")
|
|
47
|
+
return proof.native_code === "42601" && absoluteFile(proof.file) && hash(proof.file_sha256)
|
|
48
|
+
&& boundedString(proof.operation_id, 4096) && Boolean(proof.operation_id) && proofArtifact(proof.native_artifact) && object(proof.world)
|
|
49
|
+
&& boundedString(proof.world.invocation_id, 4096) && typeof proof.world.world_id === "string" && /^[a-f0-9]{32}$/.test(proof.world.world_id)
|
|
50
|
+
&& boundedString(proof.world.role, 128) && Boolean(proof.world.role) && hash(proof.world.migrations_sha256) && proof.world.phase === "migrating" && proof.world.setup_completed === false;
|
|
51
|
+
if (object(proof) && proof.rule === "candidate_tool_diagnostic")
|
|
52
|
+
return absoluteFile(proof.file) && proofArtifact(proof.native_artifact)
|
|
53
|
+
&& object(proof.tool) && { biome: "2.5.6", typescript: "5.9.3", docs: "1", "value-absence": "1" }[String(proof.tool.name)] === proof.tool.version
|
|
54
|
+
&& object(proof.step) && boundedString(proof.step.id, 256) && Boolean(proof.step.id) && Number.isSafeInteger(proof.step.ordinal) && proof.step.ordinal >= 0 && hash(proof.step.command_sha256)
|
|
55
|
+
&& boundedString(proof.diagnostic_code, 256) && (proof.tool.name === "biome" ? ["format", "assist/source/organizeImports"].includes(String(proof.diagnostic_code)) || /^lint\/[\w/-]+$/.test(String(proof.diagnostic_code)) : proof.tool.name === "typescript" ? /^TS\d+$/.test(String(proof.diagnostic_code)) : proof.tool.name === "docs" ? ["required_path_missing", "docs_contract"].includes(String(proof.diagnostic_code)) : proof.diagnostic_code === "value_absence")
|
|
56
|
+
&& (proof.path_state === "present" && hash(proof.file_sha256) || proof.path_state === "missing" && proof.tool.name === "docs" && proof.diagnostic_code === "required_path_missing" && proof.file_sha256 === null && object(proof.declaration) && absoluteFile(proof.declaration.file) && hash(proof.declaration.sha256));
|
|
32
57
|
if (!object(proof) || !boundedString(proof.rule, 128) || !boundedString(proof.native_code, 128) || !boundedString(proof.file, 4096) || !object(proof.world))
|
|
33
58
|
return false;
|
|
34
59
|
const world = proof.world;
|
|
@@ -45,23 +70,132 @@ function candidateFile(root, file) {
|
|
|
45
70
|
if (!root || !path.isAbsolute(file))
|
|
46
71
|
return false;
|
|
47
72
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
73
|
+
const realRoot = fs.realpathSync(root), relative = path.relative(realRoot, fs.realpathSync(file));
|
|
74
|
+
if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
75
|
+
return false;
|
|
76
|
+
let current = file;
|
|
77
|
+
while (fs.realpathSync(current) !== realRoot) {
|
|
78
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
79
|
+
return false;
|
|
80
|
+
const parent = path.dirname(current);
|
|
81
|
+
if (parent === current)
|
|
82
|
+
return false;
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
return fs.statSync(file).isFile();
|
|
50
86
|
}
|
|
51
87
|
catch {
|
|
52
88
|
return false;
|
|
53
89
|
}
|
|
54
90
|
}
|
|
55
91
|
export function candidateFailureFiles(evidence, root) {
|
|
56
|
-
return [...new Set((evidence?.failures ?? []).flatMap(failure =>
|
|
92
|
+
return [...new Set((evidence?.failures ?? []).flatMap(failure => {
|
|
93
|
+
const child = validatedChildren.get(failure);
|
|
94
|
+
if (child)
|
|
95
|
+
return candidateFailureFiles({ ...child.evidence, failures: [child.failure] }, root);
|
|
96
|
+
const proof = failure.repair_evidence;
|
|
97
|
+
const checksum = proof && "file_sha256" in proof ? proof.file_sha256 : undefined;
|
|
98
|
+
try {
|
|
99
|
+
return failure.file && candidateFile(root, failure.file) && (checksum === undefined || checksum === crypto.createHash("sha256").update(fs.readFileSync(failure.file)).digest("hex")) ? [failure.file] : [];
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}))];
|
|
105
|
+
}
|
|
106
|
+
function absentCandidate(root, file) {
|
|
107
|
+
if (!root || !absoluteFile(file))
|
|
108
|
+
return false;
|
|
109
|
+
try {
|
|
110
|
+
let current = path.resolve(file);
|
|
111
|
+
try {
|
|
112
|
+
fs.lstatSync(current);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error.code !== "ENOENT")
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
for (;;) {
|
|
120
|
+
try {
|
|
121
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
122
|
+
return false;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code !== "ENOENT")
|
|
127
|
+
return false;
|
|
128
|
+
const parent = path.dirname(current);
|
|
129
|
+
if (parent === current)
|
|
130
|
+
return false;
|
|
131
|
+
current = parent;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const realRoot = fs.realpathSync(root), relative = path.relative(realRoot, fs.realpathSync(current));
|
|
135
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
136
|
+
return false;
|
|
137
|
+
while (fs.realpathSync(current) !== realRoot) {
|
|
138
|
+
if (fs.lstatSync(current).isSymbolicLink())
|
|
139
|
+
return false;
|
|
140
|
+
current = path.dirname(current);
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
57
147
|
}
|
|
58
|
-
function
|
|
59
|
-
|
|
148
|
+
export function candidateFailureAbsences(evidence, root) {
|
|
149
|
+
return (evidence?.failures ?? []).flatMap(failure => {
|
|
150
|
+
const child = validatedChildren.get(failure);
|
|
151
|
+
if (child)
|
|
152
|
+
return candidateFailureAbsences({ ...child.evidence, failures: [child.failure] }, root);
|
|
153
|
+
try {
|
|
154
|
+
const proof = failure.repair_evidence;
|
|
155
|
+
if (proof?.rule !== "candidate_tool_diagnostic" || proof.path_state !== "missing" || !proof.declaration || !validatedNativeProofs.has(proof)
|
|
156
|
+
|| !absentCandidate(root, proof.file) || !candidateFile(root, proof.declaration.file) || crypto.createHash("sha256").update(fs.readFileSync(proof.declaration.file)).digest("hex") !== proof.declaration.sha256)
|
|
157
|
+
return [];
|
|
158
|
+
return [{ file: proof.file, declaration: proof.declaration, path_state: "missing" }];
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function candidateFailure(failure, evidence, root, capturedFiles, capturedAbsences) {
|
|
166
|
+
const child = validatedChildren.get(failure);
|
|
167
|
+
if (child)
|
|
168
|
+
return candidateFailure(child.failure, child.evidence, root, capturedFiles, capturedAbsences);
|
|
169
|
+
const owned = (file) => {
|
|
170
|
+
if (capturedFiles !== undefined)
|
|
171
|
+
return capturedFiles.includes(file);
|
|
172
|
+
if (!candidateFile(root, file))
|
|
173
|
+
return false;
|
|
174
|
+
const proof = failure.repair_evidence;
|
|
175
|
+
try {
|
|
176
|
+
return !proof || !("file_sha256" in proof) || proof.file_sha256 === crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
60
182
|
if (failure.origin === "tooling" || failure.origin === "unknown")
|
|
61
183
|
return false;
|
|
62
184
|
if (failure.category === "assertion")
|
|
63
185
|
return failure.origin === undefined || failure.origin === "candidate" && failure.file !== undefined && owned(failure.file);
|
|
64
186
|
const proof = failure.repair_evidence;
|
|
187
|
+
if (proof?.rule === "candidate_test_runtime_error")
|
|
188
|
+
return validatedNativeProofs.has(proof) && failure.category === "unclassified" && failure.origin === "candidate"
|
|
189
|
+
&& failure.phase === proof.phase && failure.native_code === proof.exception_type && failure.file === proof.file && owned(proof.file);
|
|
190
|
+
if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic") {
|
|
191
|
+
if (!validatedNativeProofs.has(proof) || failure.category !== "unclassified" || failure.origin !== "candidate" || failure.file !== proof.file || !["setup", "collection", "test"].includes(failure.phase ?? ""))
|
|
192
|
+
return false;
|
|
193
|
+
if (proof.rule === "candidate_migration_sql")
|
|
194
|
+
return failure.phase === "setup" && failure.native_code === "42601" && proof.world.invocation_id === evidence.binding.invocation_id && owned(proof.file);
|
|
195
|
+
if (failure.phase !== "collection" || failure.native_code !== proof.diagnostic_code)
|
|
196
|
+
return false;
|
|
197
|
+
return proof.path_state === "missing" ? Boolean(proof.declaration && (capturedAbsences ?? (root ? candidateFailureAbsences(evidence, root) : [])).some(item => item.file === proof.file && isDeepStrictEqual(item.declaration, proof.declaration))) : owned(proof.file);
|
|
198
|
+
}
|
|
65
199
|
if (proof?.rule === "candidate_browser_action_inactivity")
|
|
66
200
|
return validatedBrowserProofs.has(proof) && failure.category === "unclassified" && failure.phase === "test" && failure.origin === "candidate" && failure.native_code === proof.native_code && failure.file === proof.file && failure.test_id === proof.attempt.test_id && proof.attempt.invocation_id === evidence.binding.invocation_id && owned(proof.file);
|
|
67
201
|
return failure.category === "unclassified" && failure.origin === "candidate" && ["test", "setup"].includes(failure.phase ?? "") && proof?.rule === "candidate_sql_integrity" && ["23502", "23503", "23505", "23514"].includes(proof.native_code) && failure.native_code === proof.native_code && failure.file === proof.file && proof.world.invocation_id === evidence.binding.invocation_id && owned(proof.file);
|
|
@@ -107,7 +241,7 @@ export function readRunnerProgress(file, invocationId, lastSequence = 0, now = D
|
|
|
107
241
|
if (!value || typeof value !== "object" || Array.isArray(value) || value.schema_id !== "dd-flow/check-runner-progress@1" || value.invocation_id !== invocationId
|
|
108
242
|
|| !Number.isSafeInteger(value.sequence) || value.sequence <= 0 || typeof value.at !== "string" || !Number.isFinite(Date.parse(value.at))
|
|
109
243
|
|| !["startup", "execution"].includes(value.phase)
|
|
110
|
-
|| !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_completed", "run_completed"].includes(value.event))
|
|
244
|
+
|| !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_started", "step_completed", "run_completed"].includes(value.event))
|
|
111
245
|
throw new Error("Malformed official progress record");
|
|
112
246
|
if (value.sequence <= lastSequence || Date.parse(value.at) > now)
|
|
113
247
|
continue;
|
|
@@ -164,6 +298,9 @@ function readProofArtifact(root, ref, limit = 256 * 1024) {
|
|
|
164
298
|
function matches(actual, expected) { return object(actual) && Object.entries(expected).every(([key, value]) => actual[key] === value); }
|
|
165
299
|
function validateBrowserArtifacts(root, proof, binding) {
|
|
166
300
|
const snapshot = readProofArtifact(root, proof.worker_snapshot), native = readProofArtifact(root, proof.native_result);
|
|
301
|
+
if (proof.observation_contract !== undefined && (proof.observation_contract !== "dd-tasks/browser-observation@2" || snapshot.observation_contract !== proof.observation_contract || native.observation_contract !== proof.observation_contract
|
|
302
|
+
|| !isDeepStrictEqual(snapshot.target, proof.target) || !isDeepStrictEqual(native.target, proof.target) || snapshot.window_epoch !== proof.window_epoch || native.window_epoch !== proof.window_epoch))
|
|
303
|
+
return false;
|
|
167
304
|
if (!Array.isArray(native.errors) || native.errors.length < 1 || native.errors.length > 64 || native.errors.some(error => !boundedString(error, 4096) || !/^(?:Error: )?(?:[A-Za-z][\w.]*: )?Test timeout of [1-9]\d*ms exceeded\.(?:\nCall log:[\s\S]*)?$/.test(stripVTControlCharacters(String(error)))))
|
|
168
305
|
return false;
|
|
169
306
|
if (!matches(snapshot.binding, binding) || !matches(native.binding, binding) || !matches(snapshot.attempt, proof.attempt) || !matches(native.attempt, proof.attempt)
|
|
@@ -175,7 +312,13 @@ function validateBrowserArtifacts(root, proof, binding) {
|
|
|
175
312
|
return Number.isSafeInteger(quiet) && quiet > 0 && [last, deadline, observed, health, ended].every(Number.isFinite) && Math.abs(deadline - last - quiet) <= 2 && observed >= last && observed <= deadline && health <= observed && health >= last && deadline - health <= Math.min(1000, quiet / 4) && ended >= deadline && ended <= Date.now() && ended - deadline <= 5000;
|
|
176
313
|
}
|
|
177
314
|
export function runnerProofArtifacts(evidence) {
|
|
178
|
-
return [...new Set((evidence?.failures ?? []).flatMap(failure => {
|
|
315
|
+
return [...new Set([...(evidence?.failures ?? []).flatMap(failure => {
|
|
316
|
+
const child = validatedChildren.get(failure);
|
|
317
|
+
if (child && failure.child_evidence)
|
|
318
|
+
return [failure.child_evidence.report.relative_path, ...runnerProofArtifacts(child.evidence).map(file => path.posix.join(path.posix.dirname(failure.child_evidence.report.relative_path), file))];
|
|
319
|
+
const proof = failure.repair_evidence;
|
|
320
|
+
return [...(failure.diagnostic_artifact ? [failure.diagnostic_artifact.relative_path] : []), ...(proof?.rule === "candidate_browser_action_inactivity" ? [proof.worker_snapshot.relative_path, proof.native_result.relative_path] : proof && "native_artifact" in proof ? [proof.native_artifact.relative_path] : [])];
|
|
321
|
+
}), ...(evidence ? validatedReports.get(evidence) ?? [] : []).flatMap(child => [child.report.relative_path, ...runnerProofArtifacts(child.evidence).map(file => path.posix.join(path.posix.dirname(child.report.relative_path), file))])])];
|
|
179
322
|
}
|
|
180
323
|
export function runnerContractsValid(value) {
|
|
181
324
|
if (value === undefined)
|
|
@@ -186,13 +329,15 @@ export function runnerContractsValid(value) {
|
|
|
186
329
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
187
330
|
return false;
|
|
188
331
|
const contract = raw;
|
|
189
|
-
return alias.startsWith("@check/") &&
|
|
332
|
+
return alias.startsWith("@check/") && ["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(String(contract.schema_id)) && typeof contract.path === "string" && contract.path.length > 0 && contract.path.split("/")[0] !== "engine-outcome.json" && !contract.path.includes("\\") && !path.isAbsolute(contract.path) && !contract.path.split("/").some(part => part === ".." || part === "" || part === ".");
|
|
190
333
|
});
|
|
191
334
|
}
|
|
192
|
-
export function readRunnerEvidence(root, contract, binding) {
|
|
335
|
+
export function readRunnerEvidence(root, contract, binding, depth = 0, budget = { reports: 0 }) {
|
|
193
336
|
try {
|
|
337
|
+
if (depth > 4 || ++budget.reports > 64)
|
|
338
|
+
return null;
|
|
194
339
|
let current = root;
|
|
195
|
-
if (!fs.lstatSync(current).isDirectory())
|
|
340
|
+
if (!fs.lstatSync(current).isDirectory() || fs.lstatSync(current).isSymbolicLink())
|
|
196
341
|
return null;
|
|
197
342
|
for (const part of contract.path.split(/[\\/]/)) {
|
|
198
343
|
current = path.join(current, part);
|
|
@@ -200,34 +345,141 @@ export function readRunnerEvidence(root, contract, binding) {
|
|
|
200
345
|
return null;
|
|
201
346
|
}
|
|
202
347
|
const evidence = JSON.parse(readBoundedProofFile(current, 1024 * 1024).toString("utf8"));
|
|
348
|
+
if (!["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(contract.schema_id))
|
|
349
|
+
return null;
|
|
350
|
+
const partial = evidence.failure_projection;
|
|
351
|
+
if (partial !== undefined && (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || !object(partial) || partial.state !== "partial" || !["count_bound", "byte_bound", "interrupt"].includes(partial.reason)
|
|
352
|
+
|| ![partial.represented, partial.omitted].every(value => Number.isSafeInteger(value) && value >= 0)))
|
|
353
|
+
return null;
|
|
203
354
|
if (evidence.schema_id !== contract.schema_id || !evidence.binding || Object.entries(binding).some(([key, value]) => evidence.binding[key] !== value)
|
|
204
355
|
|| typeof evidence.runner?.name !== "string" || !evidence.runner.name || typeof evidence.runner.version !== "string" || !evidence.runner.version || typeof evidence.completed !== "boolean" || !evidence.counts
|
|
205
356
|
|| ![evidence.counts.passed, evidence.counts.failed, evidence.counts.skipped].every(n => Number.isSafeInteger(n) && n >= 0)
|
|
206
357
|
|| !Array.isArray(evidence.failures) || evidence.failures.length > 64 || evidence.failures.some(f => !runnerFailureValid(f))
|
|
207
|
-
|| evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
|
|
358
|
+
|| !partial && evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
|
|
208
359
|
return null;
|
|
360
|
+
const children = new Map();
|
|
361
|
+
const consumed = new Map();
|
|
362
|
+
if (evidence.steps !== undefined) {
|
|
363
|
+
if (!Array.isArray(evidence.steps) || evidence.steps.length > 64)
|
|
364
|
+
return null;
|
|
365
|
+
const ids = new Set(), invocations = new Set();
|
|
366
|
+
let stopped = false;
|
|
367
|
+
for (const [ordinal, step] of evidence.steps.entries()) {
|
|
368
|
+
if (!step || !boundedString(step.id, 256) || !step.id || ids.has(step.id) || step.ordinal !== ordinal || !hash(step.command_sha256) || !["passed", "failed", "unreached"].includes(step.state))
|
|
369
|
+
return null;
|
|
370
|
+
ids.add(step.id);
|
|
371
|
+
if (step.state === "unreached") {
|
|
372
|
+
if (!stopped || step.binding !== undefined || step.report !== undefined || step.exit_code !== undefined)
|
|
373
|
+
return null;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (stopped)
|
|
377
|
+
return null;
|
|
378
|
+
if (step.state === "failed")
|
|
379
|
+
stopped = true;
|
|
380
|
+
if (!step.binding || !proofArtifact(step.report) || !Number.isSafeInteger(step.exit_code) || step.state === "passed" && step.exit_code !== 0 || step.state === "failed" && step.exit_code === 0 || invocations.has(step.binding.invocation_id) || step.binding.invocation_id === binding.invocation_id)
|
|
381
|
+
return null;
|
|
382
|
+
if (step.binding.invocation_id !== `${binding.invocation_id}:step:${step.id}` || step.binding.check_id !== `${binding.check_id}/${step.id}` || step.binding.verification_epoch !== binding.verification_epoch || step.binding.input_hash !== crypto.createHash("sha256").update(JSON.stringify([binding.input_hash, step.command_sha256])).digest("hex"))
|
|
383
|
+
return null;
|
|
384
|
+
if (crypto.createHash("sha256").update(step.binding.command).digest("hex") !== step.command_sha256)
|
|
385
|
+
return null;
|
|
386
|
+
invocations.add(step.binding.invocation_id);
|
|
387
|
+
readProofArtifact(root, step.report, 1024 * 1024);
|
|
388
|
+
const child = readRunnerEvidence(path.join(root, path.dirname(step.report.relative_path)), { schema_id: contract.schema_id, path: path.basename(step.report.relative_path) }, step.binding, depth + 1, budget);
|
|
389
|
+
if (!child || step.state === "passed" && (child.counts.failed !== 0 || child.failures.length !== 0) || step.state === "failed" && !child.failures.length && child.completed && !child.observation_unknown)
|
|
390
|
+
return null;
|
|
391
|
+
if (!child.completed || child.observation_unknown)
|
|
392
|
+
stopped = true;
|
|
393
|
+
children.set(step.id, child);
|
|
394
|
+
}
|
|
395
|
+
for (const key of ["passed", "failed", "skipped"])
|
|
396
|
+
if (evidence.counts[key] !== [...children.values()].reduce((total, child) => total + child.counts[key], 0))
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
209
399
|
for (const failure of evidence.failures) {
|
|
400
|
+
if (failure.child_evidence) {
|
|
401
|
+
const ref = failure.child_evidence, step = evidence.steps?.find(item => item.id === ref.step_id), child = children.get(ref.step_id);
|
|
402
|
+
if (!step || !child || !isDeepStrictEqual(ref.binding, step.binding) || !isDeepStrictEqual(ref.report, step.report))
|
|
403
|
+
return null;
|
|
404
|
+
const original = { ...failure };
|
|
405
|
+
delete original.child_evidence;
|
|
406
|
+
const used = consumed.get(child) ?? new Set();
|
|
407
|
+
consumed.set(child, used);
|
|
408
|
+
const index = child.failures.findIndex((item, index) => { const native = { ...item }; delete native.child_evidence; return !used.has(index) && isDeepStrictEqual(native, original); });
|
|
409
|
+
if (index < 0)
|
|
410
|
+
return null;
|
|
411
|
+
used.add(index);
|
|
412
|
+
const matched = child.failures[index];
|
|
413
|
+
if (!matched.child_evidence && matched.repair_evidence?.rule === "candidate_tool_diagnostic" && !isDeepStrictEqual(matched.repair_evidence.step, { id: step.id, ordinal: step.ordinal, command_sha256: step.command_sha256 }))
|
|
414
|
+
return null;
|
|
415
|
+
validatedChildren.set(failure, { failure: matched, evidence: child });
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
210
418
|
if (failure.diagnostic_artifact && !matches(readProofArtifact(root, failure.diagnostic_artifact, 2 * 1024 * 1024).binding, binding))
|
|
211
419
|
return null;
|
|
212
420
|
const proof = failure.repair_evidence;
|
|
421
|
+
if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic" || proof?.rule === "candidate_test_runtime_error") {
|
|
422
|
+
const native = readProofArtifact(root, proof.native_artifact, 2 * 1024 * 1024), facts = Object.fromEntries(Object.entries(proof).filter(([key]) => key !== "native_artifact"));
|
|
423
|
+
if (!isDeepStrictEqual(native.binding, binding) || Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)))
|
|
424
|
+
return null;
|
|
425
|
+
if (proof.rule === "candidate_test_runtime_error") {
|
|
426
|
+
if (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || native.executing_file !== proof.file || native.exception_name !== proof.exception_type
|
|
427
|
+
|| !isDeepStrictEqual(native.top_frame, { file: proof.file, line: proof.line, column: proof.column }) || !boundedString(native.stack, 16384) || !String(native.stack).startsWith(`${proof.exception_type}:`))
|
|
428
|
+
return null;
|
|
429
|
+
const firstFrame = String(native.stack).split("\n").find(line => /^\s*at\s/.test(line));
|
|
430
|
+
const frame = firstFrame?.match(/^\s*at\s+(?:.*?\()?((?:file:\/\/)?\/[^\n]+):(\d+):(\d+)\)?$/);
|
|
431
|
+
if (!frame || (frame[1].startsWith("file://") ? fileURLToPath(frame[1]) : frame[1]) !== proof.file || Number(frame[2]) !== proof.line || Number(frame[3]) !== proof.column)
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
validatedNativeProofs.add(proof);
|
|
435
|
+
}
|
|
213
436
|
if (proof?.rule === "candidate_browser_action_inactivity") {
|
|
214
|
-
if (
|
|
437
|
+
if (proof.observation_contract === undefined && evidence.schema_id === "dd-flow/check-runner-evidence@1") {
|
|
438
|
+
// Legacy diagnostics remain readable; they do not prove the new window/target guarantee.
|
|
439
|
+
if (!validateBrowserArtifacts(root, proof, binding))
|
|
440
|
+
return null;
|
|
441
|
+
evidence.completed = false;
|
|
442
|
+
unsupportedObservationContracts.add(evidence);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (proof.observation_contract !== "dd-tasks/browser-observation@2" || !validateBrowserArtifacts(root, proof, binding))
|
|
215
446
|
return null;
|
|
216
447
|
validatedBrowserProofs.add(proof);
|
|
217
448
|
}
|
|
218
449
|
}
|
|
450
|
+
const represented = [...consumed.values()].reduce((total, used) => total + used.size, 0), childFailures = [...children.values()].reduce((total, child) => total + child.failures.length, 0);
|
|
451
|
+
if (partial) {
|
|
452
|
+
if (children.size ? partial.represented !== represented || partial.omitted !== childFailures - represented : partial.represented !== evidence.failures.filter(failure => failure.code !== "report_truncated").length)
|
|
453
|
+
return null;
|
|
454
|
+
evidence.completed = false;
|
|
455
|
+
}
|
|
456
|
+
else if (represented !== childFailures)
|
|
457
|
+
return null;
|
|
458
|
+
// Valid but incomplete native observation is diagnostic evidence, not a
|
|
459
|
+
// malformed schema. Preserve the primary failure and references through
|
|
460
|
+
// every composite, including a parent that incorrectly claims completion.
|
|
461
|
+
if ([...children.values()].some(child => !child.completed || child.observation_unknown)) {
|
|
462
|
+
evidence.completed = false;
|
|
463
|
+
if ([...children.values()].some(child => unsupportedObservationContracts.has(child)))
|
|
464
|
+
unsupportedObservationContracts.add(evidence);
|
|
465
|
+
if (evidence.schema_id === "dd-flow/check-runner-evidence@1" && [...children.values()].some(child => !child.completed && !unsupportedObservationContracts.has(child)) || [...children.values()].some(child => child.observation_unknown))
|
|
466
|
+
evidence.observation_unknown = true;
|
|
467
|
+
}
|
|
468
|
+
validatedReports.set(evidence, (evidence.steps ?? []).flatMap(step => step.report && children.has(step.id) ? [{ report: step.report, evidence: children.get(step.id) }] : []));
|
|
219
469
|
return evidence;
|
|
220
470
|
}
|
|
221
471
|
catch {
|
|
222
472
|
return null;
|
|
223
473
|
}
|
|
224
474
|
}
|
|
225
|
-
export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
|
|
475
|
+
export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.failure_projection && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
|
|
226
476
|
export function checkRepairDisposition(receipt) {
|
|
227
477
|
if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
|
|
228
478
|
return "tooling_blocker";
|
|
229
479
|
if (receipt.status === "running" || receipt.status === "aborted" || receipt.runner_evidence?.observation_unknown)
|
|
230
480
|
return "settlement_required";
|
|
481
|
+
if (receipt.runner_evidence && unsupportedObservationContracts.has(receipt.runner_evidence) || receipt.runner_evidence?.failure_projection || receipt.runner_evidence?.schema_id === "dd-flow/check-runner-evidence@2" && !receipt.runner_evidence.completed)
|
|
482
|
+
return "tooling_blocker";
|
|
231
483
|
if (receipt.status === "passed")
|
|
232
484
|
return "passed";
|
|
233
485
|
if (receipt.execution_failures?.length || receipt.mutation_paths?.length || receipt.before_fingerprint !== receipt.after_fingerprint || receipt.diagnostic_evidence_valid === false)
|
|
@@ -241,7 +493,7 @@ export function checkRepairDisposition(receipt) {
|
|
|
241
493
|
return "tooling_blocker";
|
|
242
494
|
if (!evidence.completed)
|
|
243
495
|
return "unclassified";
|
|
244
|
-
if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files)))
|
|
496
|
+
if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files, receipt.candidate_absences)))
|
|
245
497
|
return "unclassified";
|
|
246
498
|
return "source_repair";
|
|
247
499
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
2
|
+
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
3
3
|
export { assertSourceRepairEligible, checkRepairDisposition } from "./check-runner-evidence.js";
|
|
4
4
|
import { isGitlessWorkspace, ownedWorkspaceHookPath, projectServicePathFilter, workspaceGitFiles } from "./workspace-files.js";
|
|
5
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -16,6 +16,7 @@ import { confirmManagedProcess, finishManagedProcess, renewManagedProcess, manag
|
|
|
16
16
|
import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
|
|
17
17
|
import { getResourceDatabase } from "../storage/database.js";
|
|
18
18
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
19
|
+
import { ownedCheckEnvironment, validateCheckEnvironment } from "./check-environment.js";
|
|
19
20
|
const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
|
|
20
21
|
// Receipt reservation precedes child spawn so an interrupted CLI leaves an
|
|
21
22
|
// auditable attempt. A second caller must not mistake that small handoff window
|
|
@@ -61,10 +62,7 @@ export function terminalCheckSettled(context, record) {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
|
|
64
|
-
|
|
65
|
-
|| Object.keys(checkEnvironment).length !== 1 || typeof checkEnvironment.CODEX_HOME !== "string"
|
|
66
|
-
|| checkEnvironment.CODEX_HOME.length > 4096 || checkEnvironment.CODEX_HOME.includes("\0") || !path.isAbsolute(checkEnvironment.CODEX_HOME)))
|
|
67
|
-
throw new AppError("invalid_check_environment", "Owned check environment must name one absolute Codex home", 2, { effect: "no_effect" });
|
|
65
|
+
const selected = validateCheckEnvironment(checkEnvironment);
|
|
68
66
|
const suffix = crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex").slice(0, 12);
|
|
69
67
|
// The managed CLI's Node must win over a provider app's PATH: a Rosetta
|
|
70
68
|
// provider can otherwise run x64 checks against arm64-installed modules.
|
|
@@ -72,8 +70,7 @@ export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
|
|
|
72
70
|
delete environment.NODE_PATH;
|
|
73
71
|
for (const key of ["DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH", "DD_FLOW_CHECK_RUNNER_PROGRESS_PATH", "DD_FLOW_CHECK_INVOCATION_ID", "DD_FLOW_CHECK_ID", "DD_FLOW_CHECK_INPUT_HASH", "DD_FLOW_CHECK_VERIFICATION_EPOCH", "DD_FLOW_CHECK_COMMAND"])
|
|
74
72
|
delete environment[key];
|
|
75
|
-
|
|
76
|
-
environment.CODEX_HOME = checkEnvironment.CODEX_HOME;
|
|
73
|
+
Object.assign(environment, selected);
|
|
77
74
|
return environment;
|
|
78
75
|
}
|
|
79
76
|
export function workspaceOperationId(workspaceRoot) { return `workspace:${crypto.createHash("sha256").update(path.resolve(workspaceRoot)).digest("hex")}`; }
|
|
@@ -302,6 +299,9 @@ export class PreparedCheckEvidence {
|
|
|
302
299
|
Object.freeze(this);
|
|
303
300
|
}
|
|
304
301
|
static prepare(context, input) {
|
|
302
|
+
const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
|
|
303
|
+
if (checkEnvironment)
|
|
304
|
+
input = { ...input, checkEnvironment };
|
|
305
305
|
validateCheckEvidenceContext(input.evidenceContext);
|
|
306
306
|
const fail = () => { throw new AppError("check_evidence_stale", "Current declared checks lack owned passed evidence", 2, { effect: "no_effect" }); };
|
|
307
307
|
if (!input.checks.length || (input.scope === "work") !== Boolean(input.workId) || input.mode !== undefined && input.mode !== "diagnostic")
|
|
@@ -488,6 +488,9 @@ function checkEvidenceSqlSnapshot(context, input) {
|
|
|
488
488
|
}));
|
|
489
489
|
}
|
|
490
490
|
export async function runCodeChecks(context, input) {
|
|
491
|
+
const checkEnvironment = ownedCheckEnvironment(context.env, input.checkEnvironment);
|
|
492
|
+
if (checkEnvironment)
|
|
493
|
+
input = { ...input, checkEnvironment };
|
|
491
494
|
validateCheckEvidenceContext(input.evidenceContext);
|
|
492
495
|
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
493
496
|
let gate;
|
|
@@ -658,8 +661,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
658
661
|
if (result.failure)
|
|
659
662
|
physicalFailures.push(result.failure);
|
|
660
663
|
const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
664
|
+
const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
661
665
|
if (group.runnerEvidence)
|
|
662
|
-
retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles);
|
|
666
|
+
retainExecutionOutcome(evidenceDir, { id, command: group.command, input_hash: inputHash, verification_epoch: epoch, fingerprint_scope: "check_inputs", input_manifest: reserved.inputManifest ?? null, execution_contract_hash: executionContractHash }, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
|
|
663
667
|
const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(runnerEvidence), ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
|
|
664
668
|
artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
|
|
665
669
|
const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
@@ -667,6 +671,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
667
671
|
receipt.required_artifacts = requiredArtifacts;
|
|
668
672
|
receipt.candidate_root = input.workspaceRoot;
|
|
669
673
|
receipt.candidate_files = candidateFiles;
|
|
674
|
+
receipt.candidate_absences = candidateAbsences;
|
|
670
675
|
receipt.execution_failures = physicalFailures;
|
|
671
676
|
context.db.run("UPDATE check_receipts SET execution_failures_json = ? WHERE project_id = ? AND id = ? AND status = 'running'", [JSON.stringify(physicalFailures), input.projectId, id]);
|
|
672
677
|
const completion = readCompletion(completionPath);
|
|
@@ -754,6 +759,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
754
759
|
if (scoped && !completionSnapshot)
|
|
755
760
|
physicalFailures.push({ code: "completion_snapshot_unknown", message: "Detached check has no bound scoped completion snapshot; current inputs cannot prove inputs at command completion" });
|
|
756
761
|
let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
762
|
+
let candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
757
763
|
if (receipt?.runner_evidence_required) {
|
|
758
764
|
const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
|
|
759
765
|
if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
|
|
@@ -763,13 +769,14 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
763
769
|
if (proven.candidate_root) {
|
|
764
770
|
physicalFailures.push(...(proven.execution_failures ?? []));
|
|
765
771
|
candidateFiles = proven.candidate_files ?? candidateFiles;
|
|
772
|
+
candidateAbsences = proven.candidate_absences ?? [];
|
|
766
773
|
}
|
|
767
774
|
else {
|
|
768
775
|
physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
|
|
769
776
|
fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
|
|
770
777
|
}
|
|
771
778
|
}
|
|
772
|
-
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles);
|
|
779
|
+
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
|
|
773
780
|
receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), ...runnerProofArtifacts(runnerEvidence), "engine-outcome.json"])];
|
|
774
781
|
artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
|
|
775
782
|
}
|
|
@@ -782,6 +789,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
782
789
|
const repaired = { ...receipt, ...(receipt.runner_evidence_required ? { runner_evidence: runnerEvidence ?? null } : {}), completion_path: receipt.completion_path || path.join(path.dirname(pendingReceipt.receipt_path), "completion.json"), required_artifacts: receipt.required_artifacts ?? [], ...(status === "aborted" ? { abort_reason: "completion_marker_missing" } : {}), status, exit_code: exitCode, after_fingerprint: afterFingerprint, mutation_paths: mutationPaths, artifacts: artifacts.items, finished_at: finishedAt };
|
|
783
790
|
repaired.candidate_root = input.workspaceRoot;
|
|
784
791
|
repaired.candidate_files = candidateFiles;
|
|
792
|
+
repaired.candidate_absences = candidateAbsences;
|
|
785
793
|
repaired.execution_failures = physicalFailures;
|
|
786
794
|
writeReceipt(pendingReceipt.receipt_path, repaired);
|
|
787
795
|
if (status === "failed")
|
|
@@ -980,12 +988,12 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
|
980
988
|
if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
|
|
981
989
|
try {
|
|
982
990
|
const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
|
|
983
|
-
if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
|
|
991
|
+
if (value.schema_id === "dd-flow/check-execution-outcome@1" && value.id === binding.id && value.command === binding.command && value.input_hash === binding.input_hash && value.verification_epoch === binding.verification_epoch && (value.fingerprint_scope ?? null) === (binding.fingerprint_scope ?? null) && (value.execution_contract_hash ?? null) === (binding.execution_contract_hash ?? null) && JSON.stringify(value.input_manifest ?? null) === JSON.stringify(binding.input_manifest ?? null) && typeof value.candidate_root === "string" && path.isAbsolute(value.candidate_root) && (value.candidate_files === undefined || Array.isArray(value.candidate_files) && value.candidate_files.length <= 64 && value.candidate_files.every((file) => typeof file === "string" && path.isAbsolute(file) && Buffer.byteLength(file) <= 4096)) && (value.candidate_absences === undefined || candidateAbsencesValid(value.candidate_absences)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
|
|
984
992
|
outcome = value;
|
|
985
993
|
}
|
|
986
994
|
catch { /* Missing physical proof cannot grant attribution. */ }
|
|
987
995
|
}
|
|
988
|
-
return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}) };
|
|
996
|
+
return { completion_path: path.join(currentRoot, "completion.json"), required_artifacts: original.required_artifacts ?? [], inputs: original.inputs ?? [], artifacts: artifacts.map((artifact) => ({ ...artifact, path: relocate(artifact.path) })), ...(original.runner_evidence_required ? { runner_evidence_required: original.runner_evidence_required, runner_evidence: evidence } : {}), execution_failures: outcome.execution_failures ?? [], ...(outcome.candidate_root ? { candidate_root: outcome.candidate_root } : {}), ...(outcome.candidate_files ? { candidate_files: outcome.candidate_files } : {}), ...(outcome.candidate_absences ? { candidate_absences: outcome.candidate_absences } : {}) };
|
|
989
997
|
}
|
|
990
998
|
function executionFailures(input) {
|
|
991
999
|
const failures = [];
|
|
@@ -1003,11 +1011,11 @@ function executionFailures(input) {
|
|
|
1003
1011
|
failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
|
|
1004
1012
|
return failures;
|
|
1005
1013
|
}
|
|
1006
|
-
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles) {
|
|
1014
|
+
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = []) {
|
|
1007
1015
|
const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
|
|
1008
1016
|
if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
|
|
1009
1017
|
throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
|
|
1010
|
-
fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, execution_failures: failures }), { flag: "wx", mode: 0o600 });
|
|
1018
|
+
fs.writeFileSync(temporary, JSON.stringify({ schema_id: "dd-flow/check-execution-outcome@1", id: binding.id, command: binding.command, input_hash: binding.input_hash, verification_epoch: binding.verification_epoch, fingerprint_scope: binding.fingerprint_scope ?? null, input_manifest: binding.input_manifest ?? null, execution_contract_hash: binding.execution_contract_hash ?? null, candidate_root: candidateRoot, candidate_files: candidateFiles, candidate_absences: candidateAbsences, execution_failures: failures }), { flag: "wx", mode: 0o600 });
|
|
1011
1019
|
fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
|
|
1012
1020
|
}
|
|
1013
1021
|
export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
|
|
@@ -1162,7 +1170,7 @@ function environmentFingerprint(workspaceRoot, inputs, checkEnvironment) {
|
|
|
1162
1170
|
"CODEX_PERMISSION_PROFILE", "CODEX_SESSION_ID", "CODEX_VERSION", "CODEX_TOOL_CALL_ID"]);
|
|
1163
1171
|
return crypto.createHash("sha256").update(JSON.stringify(Object.entries(environment)
|
|
1164
1172
|
.filter(([name, value]) => value !== undefined && !bookkeeping.has(name)
|
|
1165
|
-
&& (inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
|
|
1173
|
+
&& (name === "PLAYWRIGHT_BROWSERS_PATH" || inputs === undefined || inputs.some(input => input.endsWith("*") ? name.startsWith(input.slice(0, -1)) : name === input)))
|
|
1166
1174
|
.sort(([left], [right]) => left.localeCompare(right)))).digest("hex");
|
|
1167
1175
|
}
|
|
1168
1176
|
function acquireWorkspaceGate(context, input) {
|
|
@@ -1337,7 +1345,7 @@ function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
|
1337
1345
|
// Receipt annotations are not execution authority. Compare the SQL-owned
|
|
1338
1346
|
// failures with the independently hash/binding-checked engine sidecar.
|
|
1339
1347
|
const hasEngineOutcome = receipt.required_artifacts.includes("engine-outcome.json") || receipt.artifacts.some(artifact => artifact.path === "engine-outcome.json" || artifact.path === path.join(path.dirname(receipt.receipt_path), "artifacts", "engine-outcome.json"));
|
|
1340
|
-
if (hasEngineOutcome && !isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []))
|
|
1348
|
+
if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? [])))
|
|
1341
1349
|
return false;
|
|
1342
1350
|
const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
|
|
1343
1351
|
if (stored.runner_evidence_required && mode !== "diagnostic" && !runnerEvidencePass(readRunnerEvidence(root, stored.runner_evidence_required, { invocation_id: receipt.id, check_id: receipt.id, command: receipt.command, input_hash: receipt.input_hash, verification_epoch: receipt.verification_epoch })))
|
|
@@ -4,10 +4,11 @@ import { FrozenGenericExecution } from "./generic-execution.js";
|
|
|
4
4
|
import { resolveExecutionPolicy, resolveStageTransition, stageExecutionCwd } from "./execution-policy.js";
|
|
5
5
|
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
6
6
|
import { hybridDigest } from "./hybrid-contracts.js";
|
|
7
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
7
8
|
/** Adapt generic routing to shared fanout APIs, not to SDLC settings. No alias,
|
|
8
9
|
* operator file, merge policy or project default participates in this selection.
|
|
9
10
|
*/
|
|
10
|
-
export function genericControllerRouting(execution) {
|
|
11
|
+
export function genericControllerRouting(execution, workspaceRoot) {
|
|
11
12
|
if (!(execution instanceof FrozenGenericExecution) || execution.settings.execution !== "managed") {
|
|
12
13
|
throw new AppError("controller_execution_invalid", "Managed controller requires managed frozen execution", 1, { effect: "no_effect" });
|
|
13
14
|
}
|
|
@@ -33,14 +34,17 @@ export function genericControllerRouting(execution) {
|
|
|
33
34
|
execution.stage(fromStage);
|
|
34
35
|
return { ...resolveExecutionPolicy({ ddFlowHome: "", policy, profiles, stage }), session_mode: selected.session_mode };
|
|
35
36
|
},
|
|
36
|
-
cwd(stage, _mergeTargetWorkspace,
|
|
37
|
+
cwd(stage, _mergeTargetWorkspace, currentRun) {
|
|
38
|
+
execution.stage(stage);
|
|
39
|
+
return path.resolve(currentRun?.workspace_root ?? workspaceRoot ?? snapshot.scope.workspace);
|
|
40
|
+
}
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
/** Restore execution from RUN authority, never from a generic manifest copy.
|
|
40
44
|
* Legacy SDLC manifests retain their original settings and routing behavior.
|
|
41
45
|
*/
|
|
42
46
|
export function selectControllerExecution(context, run, manifest) {
|
|
43
|
-
if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind)) {
|
|
47
|
+
if (manifest?.schema_id === "dd-flow/run-controller-manifest@2" || ["mb-init", "mb-upgrade"].includes(run.flow_kind) || isDiagnosticFlow(run.flow_kind)) {
|
|
44
48
|
const bundle = readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id });
|
|
45
49
|
if (!bundle || bundle.descriptor.descriptor.id !== run.flow_kind || bundle.descriptor.descriptor.id === "sdlc")
|
|
46
50
|
throw new AppError("controller_execution_invalid", "Generic controller requires its retained non-SDLC bundle", 1);
|
|
@@ -51,7 +55,9 @@ export function selectControllerExecution(context, run, manifest) {
|
|
|
51
55
|
|| manifest.request?.project_root !== run.project_root || manifest.request?.run_id !== run.id)) {
|
|
52
56
|
throw new AppError("controller_execution_invalid", "Controller execution binding differs from retained RUN authority", 1);
|
|
53
57
|
}
|
|
54
|
-
|
|
58
|
+
// Bundle admission has already checked the current RUN workspace against
|
|
59
|
+
// its trusted relocation receipts; frozen scope remains historical input.
|
|
60
|
+
return { ...genericControllerRouting(bundle.execution, run.workspace_root), bundle, binding, legacy: undefined };
|
|
55
61
|
}
|
|
56
62
|
const index = JSON.parse(run.index_json);
|
|
57
63
|
const legacy = manifest ? manifest.execution_profile : index.execution_profile;
|