@deksden-com/dd-flow-cli 0.9.0-beta.150 → 0.9.0-beta.152
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 +12 -0
- package/README.md +17 -22
- package/dist/build-info.json +6 -6
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +26 -5
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +67 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +10 -14
- package/dist/harness-runtime/lib/dd-agy.mjs +3 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +62 -10
- package/dist/harness-runtime/lib/dd-opencode.mjs +32 -14
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +1 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +37 -12
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +13 -4
- package/dist/harness-runtime/lib/model-observations.mjs +19 -7
- package/dist/harness-runtime/lib/native-children.d.mts +12 -1
- package/dist/harness-runtime/lib/native-children.mjs +47 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +9 -1
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +97 -6
- package/dist/services/check-runner-evidence.js +54 -309
- package/dist/services/check-runner-proof.js +373 -0
- package/dist/services/code-checks.js +31 -12
- package/dist/services/controller-child-capacity.js +58 -0
- package/dist/services/controller-child-observer.js +9 -3
- package/dist/services/memory-init-native-capacity.js +1 -1
- package/dist/services/provider-capacity.js +15 -7
- package/dist/services/repair-intents.js +4 -1
- package/dist/services/run-controller-adapter.js +1 -1
- package/dist/services/run-controller.js +87 -18
- package/dist/services/run-recovery-guard.js +1 -1
- package/dist/services/run-recovery-runtime.js +1 -1
- package/dist/services/stage-context.js +56 -8
- package/dist/services/vnext-code-review.js +1 -1
- package/dist/services/vnext-code.js +3 -2
- package/dist/services/vnext-fanout.js +1 -1
- package/dist/services/vnext-merge.js +1 -1
- package/dist/services/work-registry.js +1 -1
- package/dist/verification-components.json +299 -0
- package/package.json +7 -6
- package/tools/verification-jobs.mjs +317 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isDeepStrictEqual, stripVTControlCharacters } from "node:util";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
export const RUNNER_PROOF_CONTRACT = "dd-flow/check-runner-proof@1";
|
|
6
|
+
function object(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
7
|
+
function boundedString(value, bytes) { return typeof value === "string" && Buffer.byteLength(value) <= bytes; }
|
|
8
|
+
const validatedBrowserProofs = new WeakSet();
|
|
9
|
+
const unsupportedObservationContracts = new WeakSet();
|
|
10
|
+
const validatedNativeProofs = new WeakSet();
|
|
11
|
+
const validatedLibraryInputs = new WeakMap();
|
|
12
|
+
const validatedChildren = new WeakMap();
|
|
13
|
+
const validatedReports = new WeakMap();
|
|
14
|
+
const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
15
|
+
const absoluteFile = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") && path.isAbsolute(value);
|
|
16
|
+
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)); }
|
|
17
|
+
export function candidateLibraryInputsValid(value) { return Array.isArray(value) && value.length <= 64 && new Set(value.map(item => object(item) ? item.file : null)).size === value.length && value.every(item => object(item) && absoluteFile(item.file) && hash(item.sha256)); }
|
|
18
|
+
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 !== ".."); }
|
|
19
|
+
function proofArtifact(value) { return object(value) && relativeProofPath(value.relative_path) && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256); }
|
|
20
|
+
function browserProofShape(proof) {
|
|
21
|
+
const attempt = proof.attempt, step = proof.step;
|
|
22
|
+
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)
|
|
23
|
+
&& proof.native_code === "PLAYWRIGHT_ACTION_INACTIVITY" && object(attempt) && boundedString(attempt.invocation_id, 4096) && boundedString(attempt.test_id, 4096) && Boolean(attempt.test_id)
|
|
24
|
+
&& [attempt.retry, attempt.repeat_each_index, attempt.worker_index].every(value => Number.isSafeInteger(value) && value >= 0)
|
|
25
|
+
&& 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
|
|
26
|
+
&& proofArtifact(proof.worker_snapshot) && proofArtifact(proof.native_result);
|
|
27
|
+
}
|
|
28
|
+
export function runnerFailureValid(value) {
|
|
29
|
+
if (!object(value) || typeof value.category !== "string" || !["assertion", "tooling", "unclassified"].includes(value.category))
|
|
30
|
+
return false;
|
|
31
|
+
if (value.diagnostic_artifact !== undefined && !proofArtifact(value.diagnostic_artifact))
|
|
32
|
+
return false;
|
|
33
|
+
for (const key of ["code", "message", "file", "test_id", "native_code", "classification_reason"])
|
|
34
|
+
if (value[key] !== undefined && !boundedString(value[key], 4096))
|
|
35
|
+
return false;
|
|
36
|
+
if (value.stack !== undefined && !boundedString(value.stack, 16384) || value.origin !== undefined && (typeof value.origin !== "string" || !["candidate", "tooling", "unknown"].includes(value.origin)) || value.phase !== undefined && (typeof value.phase !== "string" || !["test", "setup", "teardown", "collection", "runner"].includes(value.phase)))
|
|
37
|
+
return false;
|
|
38
|
+
if (value.repair_evidence !== undefined) {
|
|
39
|
+
const proof = value.repair_evidence;
|
|
40
|
+
if (object(proof) && proof.rule === "candidate_library_query_error")
|
|
41
|
+
return proof.exception_type === "TestingLibraryElementError" && proof.phase === "test"
|
|
42
|
+
&& boundedString(proof.test_id, 4096) && Boolean(proof.test_id) && absoluteFile(proof.file) && hash(proof.file_sha256)
|
|
43
|
+
&& [proof.line, proof.column].every(n => Number.isSafeInteger(n) && Number(n) > 0) && object(proof.library)
|
|
44
|
+
&& proof.library.name === "@testing-library/dom" && proof.library.version === "10.4.1" && hash(proof.library.package_json_sha256) && hash(proof.library.lockfile_sha256)
|
|
45
|
+
&& Array.isArray(proof.helper_frames) && proof.helper_frames.length >= 2 && proof.helper_frames.length <= 8
|
|
46
|
+
&& proof.helper_frames.every(frame => object(frame) && absoluteFile(frame.file) && hash(frame.file_sha256) && [frame.line, frame.column].every(n => Number.isSafeInteger(n) && Number(n) > 0)) && proofArtifact(proof.native_artifact);
|
|
47
|
+
if (object(proof) && proof.rule === "candidate_test_runtime_error")
|
|
48
|
+
return ["TypeError", "ReferenceError", "Error"].includes(String(proof.exception_type)) && (proof.exception_type !== "Error" || proof.phase === "test")
|
|
49
|
+
&& absoluteFile(proof.file) && hash(proof.file_sha256) && ["test", "setup", "collection"].includes(String(proof.phase))
|
|
50
|
+
&& [proof.line, proof.column].every(value => Number.isSafeInteger(value) && Number(value) > 0) && proofArtifact(proof.native_artifact);
|
|
51
|
+
if (object(proof) && proof.rule === "candidate_migration_sql")
|
|
52
|
+
return proof.native_code === "42601" && absoluteFile(proof.file) && hash(proof.file_sha256)
|
|
53
|
+
&& boundedString(proof.operation_id, 4096) && Boolean(proof.operation_id) && proofArtifact(proof.native_artifact) && object(proof.world)
|
|
54
|
+
&& boundedString(proof.world.invocation_id, 4096) && typeof proof.world.world_id === "string" && /^[a-f0-9]{32}$/.test(proof.world.world_id)
|
|
55
|
+
&& boundedString(proof.world.role, 128) && Boolean(proof.world.role) && hash(proof.world.migrations_sha256) && proof.world.phase === "migrating" && proof.world.setup_completed === false;
|
|
56
|
+
if (object(proof) && proof.rule === "candidate_tool_diagnostic")
|
|
57
|
+
return absoluteFile(proof.file) && proofArtifact(proof.native_artifact)
|
|
58
|
+
&& object(proof.tool) && { biome: "2.5.6", typescript: "5.9.3", docs: "1", "value-absence": "1" }[String(proof.tool.name)] === proof.tool.version
|
|
59
|
+
&& 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)
|
|
60
|
+
&& 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")
|
|
61
|
+
&& (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));
|
|
62
|
+
if (!object(proof) || !boundedString(proof.rule, 128) || !boundedString(proof.native_code, 128) || !boundedString(proof.file, 4096) || !object(proof.world))
|
|
63
|
+
return false;
|
|
64
|
+
const world = proof.world;
|
|
65
|
+
if (world.schema_id !== "dd-tasks/test-world-ready@1" || !boundedString(world.invocation_id, 4096) || typeof world.world_id !== "string" || !/^[a-f0-9]{32}$/.test(world.world_id) || typeof world.migrations_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(world.migrations_sha256) || world.ready !== true || world.setup_completed !== true || !boundedString(world.role, 128) || !world.role)
|
|
66
|
+
return false;
|
|
67
|
+
if (proof.rule === "candidate_browser_action_inactivity" && !browserProofShape(proof))
|
|
68
|
+
return false;
|
|
69
|
+
if (!["candidate_sql_integrity", "candidate_browser_action_inactivity"].includes(String(proof.rule)))
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
function readBytes(context, relative, limit) {
|
|
75
|
+
if (!relativeProofPath(relative))
|
|
76
|
+
throw new Error("Unsafe proof path");
|
|
77
|
+
const bytes = context.readArtifact(relative);
|
|
78
|
+
if (!Buffer.isBuffer(bytes) || bytes.length > limit)
|
|
79
|
+
throw new Error("Evidence is not bounded bytes");
|
|
80
|
+
context.consumed += bytes.length;
|
|
81
|
+
if (context.consumed > 128 * 1024 * 1024)
|
|
82
|
+
throw Object.assign(new Error("Proof byte budget exceeded"), { code: "proof_budget_exceeded" });
|
|
83
|
+
return bytes;
|
|
84
|
+
}
|
|
85
|
+
function readProofArtifact(root, ref, context, limit = 2 * 1024 * 1024) {
|
|
86
|
+
const bytes = readBytes(context, path.posix.join(root, ref.relative_path), limit);
|
|
87
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== ref.sha256)
|
|
88
|
+
throw new Error("Proof hash differs");
|
|
89
|
+
const value = JSON.parse(bytes.toString("utf8"));
|
|
90
|
+
if (!object(value))
|
|
91
|
+
throw new Error("Invalid proof object");
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
// Exact known helper bytes keep this a library-specific proof, not permission
|
|
95
|
+
// to skip arbitrary dependency frames. New versions need explicit evidence.
|
|
96
|
+
const queryInputHashes = {
|
|
97
|
+
"dist/config.js": "3627f6265cccd26604777602d7249788c2347a273bdd1cae41719c790f2c5a7a",
|
|
98
|
+
"dist/query-helpers.js": "901f513f2ac901992d7c5002e644aef69286edbcd7d050d3ee7dd6aaaebfc9cc",
|
|
99
|
+
"package.json": "4e62e2aa8b974b6a5d2d532396f7610d1b4e806d9d2aaef3c84221f0d2fab06d",
|
|
100
|
+
};
|
|
101
|
+
function validateLibraryQuery(root, proof, failure, evidence, context) {
|
|
102
|
+
const native = readProofArtifact(root, proof.native_artifact, context, 256 * 1024);
|
|
103
|
+
const facts = Object.fromEntries(Object.entries(proof).filter(([key]) => key !== "native_artifact"));
|
|
104
|
+
if (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || !isDeepStrictEqual(native.binding, evidence.binding)
|
|
105
|
+
|| Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)) || native.executing_file !== proof.file || native.exception_name !== proof.exception_type
|
|
106
|
+
|| failure.category !== "unclassified" || failure.origin !== "candidate" || failure.native_code !== proof.exception_type || failure.phase !== "test" || failure.test_id !== proof.test_id || failure.file !== proof.file
|
|
107
|
+
|| native.stack_complete !== true || native.frames_complete !== true || !Array.isArray(native.frame_lines) || native.frame_lines.length !== proof.helper_frames.length + 1
|
|
108
|
+
|| native.frame_lines.some(line => !boundedString(line, 4096)) || Buffer.byteLength(JSON.stringify(native.frame_lines)) > 128 * 1024)
|
|
109
|
+
return false;
|
|
110
|
+
const expectedFrames = [...proof.helper_frames, { file: proof.file, line: proof.line, column: proof.column }];
|
|
111
|
+
for (const [index, line] of native.frame_lines.entries()) {
|
|
112
|
+
const match = String(line).match(/^\s*at (?:.*?\()?((?:file:\/\/)?\/[^\n]+):(\d+):(\d+)\)?$/), expected = expectedFrames[index];
|
|
113
|
+
if (!match || (match[1].startsWith("file:") ? fileURLToPath(match[1]) : match[1]) !== expected.file || Number(match[2]) !== expected.line || Number(match[3]) !== expected.column)
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const packageRoot = path.dirname(path.dirname(proof.helper_frames[0].file));
|
|
117
|
+
if (proof.helper_frames[0].file !== path.join(packageRoot, "dist/config.js") || !String(native.frame_lines[0]).includes("getElementError")
|
|
118
|
+
|| proof.helper_frames.slice(1).some(frame => frame.file !== path.join(packageRoot, "dist/query-helpers.js")))
|
|
119
|
+
return false;
|
|
120
|
+
if (!Array.isArray(native.library_inputs) || native.library_inputs.length !== 4)
|
|
121
|
+
return false;
|
|
122
|
+
const inputs = [];
|
|
123
|
+
let packageJson;
|
|
124
|
+
for (const [index, value] of native.library_inputs.entries()) {
|
|
125
|
+
if (!object(value) || !absoluteFile(value.file) || !hash(value.sha256) || !proofArtifact(value.artifact))
|
|
126
|
+
return false;
|
|
127
|
+
const expectedName = Object.keys(queryInputHashes)[index];
|
|
128
|
+
if (expectedName ? value.file !== path.join(packageRoot, expectedName) || value.sha256 !== queryInputHashes[expectedName]
|
|
129
|
+
: path.basename(value.file) !== "pnpm-lock.yaml" || value.sha256 !== proof.library.lockfile_sha256)
|
|
130
|
+
return false;
|
|
131
|
+
const bytes = readBytes(context, path.posix.join(root, value.artifact.relative_path), 2 * 1024 * 1024);
|
|
132
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== value.sha256 || value.artifact.sha256 !== value.sha256)
|
|
133
|
+
return false;
|
|
134
|
+
if (index === 2)
|
|
135
|
+
packageJson = JSON.parse(bytes.toString("utf8"));
|
|
136
|
+
if (index === 3 && !bytes.toString("utf8").includes("@testing-library/dom@10.4.1"))
|
|
137
|
+
return false;
|
|
138
|
+
inputs.push({ file: value.file, sha256: value.sha256, artifact: value.artifact });
|
|
139
|
+
}
|
|
140
|
+
if (packageJson?.name !== proof.library.name || packageJson.version !== proof.library.version || inputs[2].sha256 !== proof.library.package_json_sha256
|
|
141
|
+
|| proof.helper_frames.some(frame => inputs.find(input => input.file === frame.file)?.sha256 !== frame.file_sha256))
|
|
142
|
+
return false;
|
|
143
|
+
const candidateRoot = path.dirname(inputs[3].file);
|
|
144
|
+
if ([proof.file, ...inputs.map(input => input.file)].some(file => { const relative = path.relative(candidateRoot, file); return !relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative); }))
|
|
145
|
+
return false;
|
|
146
|
+
validatedLibraryInputs.set(proof, inputs);
|
|
147
|
+
validatedNativeProofs.add(proof);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function matches(actual, expected) { return object(actual) && Object.entries(expected).every(([key, value]) => actual[key] === value); }
|
|
151
|
+
function validateBrowserArtifacts(root, proof, binding, context) {
|
|
152
|
+
const snapshot = readProofArtifact(root, proof.worker_snapshot, context), native = readProofArtifact(root, proof.native_result, context);
|
|
153
|
+
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
|
|
154
|
+
|| !isDeepStrictEqual(snapshot.target, proof.target) || !isDeepStrictEqual(native.target, proof.target) || snapshot.window_epoch !== proof.window_epoch || native.window_epoch !== proof.window_epoch))
|
|
155
|
+
return false;
|
|
156
|
+
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)))))
|
|
157
|
+
return false;
|
|
158
|
+
if (!matches(snapshot.binding, binding) || !matches(native.binding, binding) || !matches(snapshot.attempt, proof.attempt) || !matches(native.attempt, proof.attempt)
|
|
159
|
+
|| !matches(snapshot.step, proof.step) || !matches(native.step, proof.step) || !matches(snapshot.world, proof.world) || proof.world.invocation_id !== binding.invocation_id
|
|
160
|
+
|| native.status !== "timedOut" || native.phase !== "test" || native.file !== proof.file || snapshot.phase !== "body" || snapshot.observation_gap !== false || !matches(snapshot.browser, { connected: true, page_closed: false, crashed: false })
|
|
161
|
+
|| !matches(snapshot.world_health, { api: true, web: true, db: true }))
|
|
162
|
+
return false;
|
|
163
|
+
const quiet = snapshot.quiet_ms, last = Date.parse(String(snapshot.last_progress_at)), deadline = Date.parse(String(snapshot.deadline_at)), observed = Date.parse(String(snapshot.observed_at)), health = Date.parse(String(snapshot.health_observed_at)), ended = Date.parse(String(native.ended_at));
|
|
164
|
+
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;
|
|
165
|
+
}
|
|
166
|
+
export function runnerProofArtifacts(evidence) {
|
|
167
|
+
return [...new Set([...(evidence?.failures ?? []).flatMap(failure => {
|
|
168
|
+
const child = validatedChildren.get(failure);
|
|
169
|
+
if (child && failure.child_evidence)
|
|
170
|
+
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))];
|
|
171
|
+
const proof = failure.repair_evidence;
|
|
172
|
+
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, ...(proof.rule === "candidate_library_query_error" ? (validatedLibraryInputs.get(proof) ?? []).map(input => input.artifact.relative_path) : [])] : [])];
|
|
173
|
+
}), ...(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))])])];
|
|
174
|
+
}
|
|
175
|
+
function diagnostic(context, value) {
|
|
176
|
+
// Diagnostics are locators, never another copy of untrusted report content.
|
|
177
|
+
if (context.diagnostics.length >= 64 || context.bytes + Buffer.byteLength(JSON.stringify(value)) > 32 * 1024 - 64) {
|
|
178
|
+
context.truncated = true;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
context.diagnostics.push(value);
|
|
182
|
+
context.bytes += Buffer.byteLength(JSON.stringify(value));
|
|
183
|
+
}
|
|
184
|
+
export function validateRunnerProof(contract, binding, readArtifact) {
|
|
185
|
+
const context = { diagnostics: [], bytes: 0, consumed: 0, truncated: false, readArtifact };
|
|
186
|
+
const evidence = readRunnerEvidenceInternal("", contract, binding, 0, { reports: 0 }, context, "");
|
|
187
|
+
return { contract: RUNNER_PROOF_CONTRACT, evidence, required_artifacts: evidence ? runnerProofArtifacts(evidence) : [], diagnostics: context.diagnostics, ...(context.truncated ? { diagnostics_truncated: true } : {}) };
|
|
188
|
+
}
|
|
189
|
+
function readRunnerEvidenceInternal(root, contract, binding, depth, budget, context, prefix) {
|
|
190
|
+
const report = relativeProofPath(contract.path) ? path.posix.join(prefix, contract.path) : "<invalid-report-path>";
|
|
191
|
+
const count = context.diagnostics.length;
|
|
192
|
+
let accepted = false;
|
|
193
|
+
let failureReason = "invalid_shape";
|
|
194
|
+
const invalid = () => { if (context.diagnostics.length === count)
|
|
195
|
+
diagnostic(context, { reason: failureReason, report }); return null; };
|
|
196
|
+
try {
|
|
197
|
+
if (!relativeProofPath(contract.path))
|
|
198
|
+
return invalid();
|
|
199
|
+
if (depth > 4 || ++budget.reports > 64) {
|
|
200
|
+
failureReason = "proof_budget_exceeded";
|
|
201
|
+
return invalid();
|
|
202
|
+
}
|
|
203
|
+
const bytes = readBytes(context, path.posix.join(root, contract.path), 1024 * 1024);
|
|
204
|
+
failureReason = "invalid_json";
|
|
205
|
+
const evidence = JSON.parse(bytes.toString("utf8"));
|
|
206
|
+
failureReason = "invalid_shape";
|
|
207
|
+
if (!object(evidence))
|
|
208
|
+
return invalid();
|
|
209
|
+
if (!["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(contract.schema_id))
|
|
210
|
+
return null;
|
|
211
|
+
const partial = evidence.failure_projection;
|
|
212
|
+
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)
|
|
213
|
+
|| ![partial.represented, partial.omitted].every(value => Number.isSafeInteger(value) && value >= 0)))
|
|
214
|
+
return null;
|
|
215
|
+
if (evidence.schema_id !== contract.schema_id) {
|
|
216
|
+
diagnostic(context, { reason: "invalid_shape", report, expected: contract.schema_id, observed: ["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(evidence.schema_id) ? evidence.schema_id : "unsupported" });
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (!evidence.binding || Object.entries(binding).some(([key, value]) => evidence.binding[key] !== value)) {
|
|
220
|
+
diagnostic(context, { reason: "binding_mismatch", report });
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (typeof evidence.runner?.name !== "string" || !evidence.runner.name || typeof evidence.runner.version !== "string" || !evidence.runner.version || typeof evidence.completed !== "boolean" || !evidence.counts
|
|
224
|
+
|| ![evidence.counts.passed, evidence.counts.failed, evidence.counts.skipped].every(n => Number.isSafeInteger(n) && n >= 0)
|
|
225
|
+
|| !Array.isArray(evidence.failures) || evidence.failures.length > 64 || evidence.failures.some(f => !runnerFailureValid(f))
|
|
226
|
+
|| !partial && evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
|
|
227
|
+
return null;
|
|
228
|
+
const children = new Map();
|
|
229
|
+
const consumed = new Map();
|
|
230
|
+
if (evidence.steps !== undefined) {
|
|
231
|
+
if (!Array.isArray(evidence.steps) || evidence.steps.length > 64)
|
|
232
|
+
return null;
|
|
233
|
+
const ids = new Set(), invocations = new Set();
|
|
234
|
+
let stopped = false;
|
|
235
|
+
for (const [ordinal, step] of evidence.steps.entries()) {
|
|
236
|
+
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))
|
|
237
|
+
return null;
|
|
238
|
+
ids.add(step.id);
|
|
239
|
+
if (step.state === "unreached") {
|
|
240
|
+
if (!stopped || step.binding !== undefined || step.report !== undefined || step.exit_code !== undefined)
|
|
241
|
+
return null;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (stopped)
|
|
245
|
+
return null;
|
|
246
|
+
if (step.state === "failed")
|
|
247
|
+
stopped = true;
|
|
248
|
+
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)
|
|
249
|
+
return null;
|
|
250
|
+
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"))
|
|
251
|
+
return null;
|
|
252
|
+
if (crypto.createHash("sha256").update(step.binding.command).digest("hex") !== step.command_sha256)
|
|
253
|
+
return null;
|
|
254
|
+
invocations.add(step.binding.invocation_id);
|
|
255
|
+
failureReason = "artifact_mismatch";
|
|
256
|
+
readProofArtifact(root, step.report, context, 1024 * 1024);
|
|
257
|
+
failureReason = "invalid_shape";
|
|
258
|
+
const child = readRunnerEvidenceInternal(path.posix.join(root, path.posix.dirname(step.report.relative_path)), { schema_id: contract.schema_id, path: path.basename(step.report.relative_path) }, step.binding, depth + 1, budget, context, path.posix.join(prefix, path.posix.dirname(step.report.relative_path)));
|
|
259
|
+
if (!child || step.state === "passed" && (child.counts.failed !== 0 || child.failures.length !== 0) || step.state === "failed" && !child.failures.length && child.completed && !child.observation_unknown)
|
|
260
|
+
return null;
|
|
261
|
+
if (!child.completed || child.observation_unknown)
|
|
262
|
+
stopped = true;
|
|
263
|
+
children.set(step.id, child);
|
|
264
|
+
}
|
|
265
|
+
for (const key of ["passed", "failed", "skipped"])
|
|
266
|
+
if (evidence.counts[key] !== [...children.values()].reduce((total, child) => total + child.counts[key], 0))
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
for (const failure of evidence.failures) {
|
|
270
|
+
failureReason = "artifact_mismatch";
|
|
271
|
+
if (failure.child_evidence) {
|
|
272
|
+
const ref = failure.child_evidence, step = evidence.steps?.find(item => item.id === ref.step_id), child = children.get(ref.step_id);
|
|
273
|
+
if (!step || !child || !isDeepStrictEqual(ref.binding, step.binding) || !isDeepStrictEqual(ref.report, step.report))
|
|
274
|
+
return null;
|
|
275
|
+
const original = { ...failure };
|
|
276
|
+
delete original.child_evidence;
|
|
277
|
+
const used = consumed.get(child) ?? new Set();
|
|
278
|
+
consumed.set(child, used);
|
|
279
|
+
const index = child.failures.findIndex((item, index) => { const native = { ...item }; delete native.child_evidence; return !used.has(index) && isDeepStrictEqual(native, original); });
|
|
280
|
+
if (index < 0)
|
|
281
|
+
return null;
|
|
282
|
+
used.add(index);
|
|
283
|
+
const matched = child.failures[index];
|
|
284
|
+
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 }))
|
|
285
|
+
return null;
|
|
286
|
+
validatedChildren.set(failure, { failure: matched, evidence: child });
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (failure.diagnostic_artifact && !matches(readProofArtifact(root, failure.diagnostic_artifact, context, 2 * 1024 * 1024).binding, binding))
|
|
290
|
+
return null;
|
|
291
|
+
const proof = failure.repair_evidence;
|
|
292
|
+
if (proof?.rule === "candidate_library_query_error" && !validateLibraryQuery(root, proof, failure, evidence, context))
|
|
293
|
+
return null;
|
|
294
|
+
if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic" || proof?.rule === "candidate_test_runtime_error") {
|
|
295
|
+
const native = readProofArtifact(root, proof.native_artifact, context, 2 * 1024 * 1024), facts = Object.fromEntries(Object.entries(proof).filter(([key]) => key !== "native_artifact"));
|
|
296
|
+
if (!isDeepStrictEqual(native.binding, binding) || Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)))
|
|
297
|
+
return null;
|
|
298
|
+
if (proof.rule === "candidate_test_runtime_error") {
|
|
299
|
+
failureReason = "attribution_unproven";
|
|
300
|
+
if (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || native.executing_file !== proof.file || native.exception_name !== proof.exception_type
|
|
301
|
+
|| !isDeepStrictEqual(native.top_frame, { file: proof.file, line: proof.line, column: proof.column }) || !boundedString(native.stack, 16384) || !String(native.stack).startsWith(`${proof.exception_type}:`))
|
|
302
|
+
return null;
|
|
303
|
+
const firstFrame = String(native.stack).split("\n").find(line => /^\s*at\s/.test(line));
|
|
304
|
+
const frame = firstFrame?.match(/^\s*at\s+(?:.*?\()?((?:file:\/\/)?\/[^\n]+):(\d+):(\d+)\)?$/);
|
|
305
|
+
if (!frame || (frame[1].startsWith("file://") ? fileURLToPath(frame[1]) : frame[1]) !== proof.file || Number(frame[2]) !== proof.line || Number(frame[3]) !== proof.column)
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
validatedNativeProofs.add(proof);
|
|
309
|
+
}
|
|
310
|
+
if (proof?.rule === "candidate_browser_action_inactivity") {
|
|
311
|
+
if (proof.observation_contract === undefined && evidence.schema_id === "dd-flow/check-runner-evidence@1") {
|
|
312
|
+
// Legacy diagnostics remain readable; they do not prove the new window/target guarantee.
|
|
313
|
+
if (!validateBrowserArtifacts(root, proof, binding, context))
|
|
314
|
+
return null;
|
|
315
|
+
evidence.completed = false;
|
|
316
|
+
unsupportedObservationContracts.add(evidence);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (proof.observation_contract !== "dd-tasks/browser-observation@2" || !validateBrowserArtifacts(root, proof, binding, context))
|
|
320
|
+
return null;
|
|
321
|
+
validatedBrowserProofs.add(proof);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
failureReason = "invalid_shape";
|
|
325
|
+
const represented = [...consumed.values()].reduce((total, used) => total + used.size, 0), childFailures = [...children.values()].reduce((total, child) => total + child.failures.length, 0);
|
|
326
|
+
if (partial) {
|
|
327
|
+
if (children.size ? partial.represented !== represented || partial.omitted !== childFailures - represented : partial.represented !== evidence.failures.filter(failure => failure.code !== "report_truncated").length)
|
|
328
|
+
return null;
|
|
329
|
+
evidence.completed = false;
|
|
330
|
+
}
|
|
331
|
+
else if (represented !== childFailures)
|
|
332
|
+
return null;
|
|
333
|
+
// Valid but incomplete native observation is diagnostic evidence, not a
|
|
334
|
+
// malformed schema. Preserve the primary failure and references through
|
|
335
|
+
// every composite, including a parent that incorrectly claims completion.
|
|
336
|
+
if ([...children.values()].some(child => !child.completed || child.observation_unknown)) {
|
|
337
|
+
evidence.completed = false;
|
|
338
|
+
if ([...children.values()].some(child => unsupportedObservationContracts.has(child)))
|
|
339
|
+
unsupportedObservationContracts.add(evidence);
|
|
340
|
+
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))
|
|
341
|
+
evidence.observation_unknown = true;
|
|
342
|
+
}
|
|
343
|
+
validatedReports.set(evidence, (evidence.steps ?? []).flatMap(step => step.report && children.has(step.id) ? [{ report: step.report, evidence: children.get(step.id) }] : []));
|
|
344
|
+
if (unsupportedObservationContracts.has(evidence))
|
|
345
|
+
diagnostic(context, { reason: "unsupported_observation_contract", report, expected: "dd-tasks/browser-observation@2", observed: "legacy" });
|
|
346
|
+
else if (!evidence.completed || evidence.observation_unknown)
|
|
347
|
+
diagnostic(context, { reason: "observation_incomplete", report });
|
|
348
|
+
for (const failure of evidence.failures)
|
|
349
|
+
if (failure.origin === "unknown" || failure.category === "unclassified" && !failure.repair_evidence) {
|
|
350
|
+
diagnostic(context, { reason: "attribution_unproven", report });
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
accepted = true;
|
|
354
|
+
return evidence;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (error.code === "ENOENT")
|
|
358
|
+
failureReason = "missing";
|
|
359
|
+
else if (error.code === "proof_budget_exceeded")
|
|
360
|
+
failureReason = "proof_budget_exceeded";
|
|
361
|
+
return invalid();
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (!accepted && context.diagnostics.length === count)
|
|
365
|
+
diagnostic(context, { reason: failureReason, report });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.failure_projection && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
|
|
369
|
+
export const validatedRunnerChild = (failure) => validatedChildren.get(failure);
|
|
370
|
+
export const hasValidatedNativeProof = (proof) => validatedNativeProofs.has(proof);
|
|
371
|
+
export const runnerLibraryInputs = (proof) => validatedLibraryInputs.get(proof)?.map(({ file, sha256 }) => ({ file, sha256 }));
|
|
372
|
+
export const hasValidatedBrowserProof = (proof) => validatedBrowserProofs.has(proof);
|
|
373
|
+
export const hasUnsupportedObservation = (evidence) => unsupportedObservationContracts.has(evidence);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, checkRepairDisposition, readRunnerEvidence, readRunnerEvidenceDetailed, readRunnerProgress, runnerContractsValid, runnerEvidencePass, runnerObservationDurations, runnerProofArtifacts } from "./check-runner-evidence.js";
|
|
2
|
+
import { candidateAbsencesValid, candidateFailureAbsences, candidateFailureFiles, candidateLibraryInputs, candidateLibraryInputsValid, currentCandidateLibraryInputsValid, checkRepairDisposition, readRunnerEvidence, readRunnerEvidenceDetailed, 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";
|
|
@@ -17,6 +17,7 @@ import { admitRuntimeCheck, runtimeBudgetForRun } from "./runtime-budget.js";
|
|
|
17
17
|
import { getResourceDatabase } from "../storage/database.js";
|
|
18
18
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
19
19
|
import { ownedCheckEnvironment, validateCheckEnvironment } from "./check-environment.js";
|
|
20
|
+
import { browserCheckCapabilityEnvironment } from "./stage-context.js";
|
|
20
21
|
const profileRelativePath = path.join(".memory-bank", "spec", "engineering", "code-check-profile.json");
|
|
21
22
|
// Receipt reservation precedes child spawn so an interrupted CLI leaves an
|
|
22
23
|
// auditable attempt. A second caller must not mistake that small handoff window
|
|
@@ -68,6 +69,8 @@ export function codeExecutionEnvironment(workspaceRoot, checkEnvironment) {
|
|
|
68
69
|
// provider can otherwise run x64 checks against arm64-installed modules.
|
|
69
70
|
const environment = { ...process.env, PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`, DD_FLOW_LOCAL_DATABASE_SUFFIX: suffix };
|
|
70
71
|
delete environment.NODE_PATH;
|
|
72
|
+
delete environment.DD_FLOW_BROWSER_CAPABILITY_PATH;
|
|
73
|
+
delete environment.DD_FLOW_BROWSER_CAPABILITY_SHA256;
|
|
71
74
|
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"])
|
|
72
75
|
delete environment[key];
|
|
73
76
|
Object.assign(environment, selected);
|
|
@@ -349,7 +352,7 @@ export class PreparedCheckEvidence {
|
|
|
349
352
|
candidates: receipts.filter(item => item.scope === input.scope && item.work_id === (input.workId ?? null))
|
|
350
353
|
.slice(-16).map(item => ({ id: item.id, input_hash: item.input_hash, profile_hash: item.profile_hash }))
|
|
351
354
|
});
|
|
352
|
-
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode))
|
|
355
|
+
if (!checkReceiptEvidenceValidForMode(bindReceipt(receipt, group), input.mode) || !currentCandidateLibraryInputsValid(receipt, input.workspaceRoot))
|
|
353
356
|
fail();
|
|
354
357
|
passed = passed && receipt.status === "passed";
|
|
355
358
|
for (const artifact of receipt.artifacts)
|
|
@@ -621,7 +624,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
621
624
|
const runnerEnvironment = group.runnerEvidence ? { DD_FLOW_CHECK_RUNNER_EVIDENCE_PATH: path.join(evidenceDir, group.runnerEvidence.path), DD_FLOW_CHECK_RUNNER_PROGRESS_PATH: path.join(evidenceDir, "runner-progress.jsonl"), DD_FLOW_CHECK_INVOCATION_ID: id, DD_FLOW_CHECK_ID: id, DD_FLOW_CHECK_INPUT_HASH: inputHash, DD_FLOW_CHECK_VERIFICATION_EPOCH: epoch } : {};
|
|
622
625
|
if (group.runnerEvidence)
|
|
623
626
|
fs.mkdirSync(path.dirname(path.join(evidenceDir, group.runnerEvidence.path)), { recursive: true });
|
|
624
|
-
result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
|
|
627
|
+
result = await runCheck(group.command, input.workspaceRoot, { ...codeExecutionEnvironment(input.workspaceRoot, input.checkEnvironment), ...browserCheckCapabilityEnvironment(context, input.runId, input.runHome), DD_FLOW_EVIDENCE_DIR: evidenceDir, DD_FLOW_CHECK_COMPLETION_FILE: completionPath, ...runnerEnvironment, ...portEnvironment(resources) }, stdout, stderr, (elapsed) => input.progress?.(`check ${index + 1}/${groups.length} still running (${elapsed}s): ${group.command}`), (pid) => {
|
|
625
628
|
confirmedExpiry = confirmManagedProcess(context, { id: managed.id, leaseToken: managed.lease_token, pid, processGroupId: process.platform === "win32" ? null : pid }).lease_expires_at;
|
|
626
629
|
assertRunMutationAllowed(context, input.projectId, input.runId, generation);
|
|
627
630
|
if (budget)
|
|
@@ -663,8 +666,9 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
663
666
|
physicalFailures.push(result.failure);
|
|
664
667
|
const candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
665
668
|
const candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
669
|
+
const libraryInputs = candidateLibraryInputs(runnerEvidence, input.workspaceRoot);
|
|
666
670
|
if (group.runnerEvidence)
|
|
667
|
-
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);
|
|
671
|
+
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, libraryInputs);
|
|
668
672
|
const requiredArtifacts = [...new Set([...group.requiredArtifacts, ...runnerProofArtifacts(runnerEvidence), ...(group.runnerEvidence ? ["engine-outcome.json"] : [])])];
|
|
669
673
|
artifacts = collectCheckArtifacts(evidenceDir, requiredArtifacts);
|
|
670
674
|
const status = result.aborted ? "aborted" : result.exitCode === 0 && !result.error && artifacts.complete && mutationPaths.length === 0 && (!group.runnerEvidence || runnerEvidencePass(runnerEvidence)) ? "passed" : "failed";
|
|
@@ -673,6 +677,7 @@ async function runCodeChecksOwned(context, input, workspaceGateId, generation, s
|
|
|
673
677
|
receipt.candidate_root = input.workspaceRoot;
|
|
674
678
|
receipt.candidate_files = candidateFiles;
|
|
675
679
|
receipt.candidate_absences = candidateAbsences;
|
|
680
|
+
receipt.candidate_library_inputs = libraryInputs;
|
|
676
681
|
receipt.execution_failures = physicalFailures;
|
|
677
682
|
context.db.run("UPDATE check_receipts SET execution_failures_json = ? WHERE project_id = ? AND id = ? AND status = 'running'", [JSON.stringify(physicalFailures), input.projectId, id]);
|
|
678
683
|
const completion = readCompletion(completionPath);
|
|
@@ -765,6 +770,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
765
770
|
physicalFailures.push({ code: "completion_snapshot_unknown", message: "Detached check has no bound scoped completion snapshot; current inputs cannot prove inputs at command completion" });
|
|
766
771
|
let candidateFiles = candidateFailureFiles(runnerEvidence, input.workspaceRoot);
|
|
767
772
|
let candidateAbsences = candidateFailureAbsences(runnerEvidence, input.workspaceRoot);
|
|
773
|
+
let libraryInputs = candidateLibraryInputs(runnerEvidence, input.workspaceRoot);
|
|
768
774
|
if (receipt?.runner_evidence_required) {
|
|
769
775
|
const evidenceDir = path.join(path.dirname(pendingReceipt.receipt_path), "artifacts");
|
|
770
776
|
if (fs.existsSync(path.join(evidenceDir, "engine-outcome.json"))) {
|
|
@@ -775,13 +781,14 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
775
781
|
physicalFailures.push(...(proven.execution_failures ?? []));
|
|
776
782
|
candidateFiles = proven.candidate_files ?? candidateFiles;
|
|
777
783
|
candidateAbsences = proven.candidate_absences ?? [];
|
|
784
|
+
libraryInputs = proven.candidate_library_inputs ?? [];
|
|
778
785
|
}
|
|
779
786
|
else {
|
|
780
787
|
physicalFailures.push({ code: "engine_outcome_unbound", message: "Existing execution outcome is not bound to retained inventory; prior launcher/cleanup outcome is unknown" });
|
|
781
788
|
fs.renameSync(path.join(evidenceDir, "engine-outcome.json"), path.join(evidenceDir, `engine-outcome-unconfirmed-${crypto.randomUUID()}.json`));
|
|
782
789
|
}
|
|
783
790
|
}
|
|
784
|
-
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences);
|
|
791
|
+
retainExecutionOutcome(evidenceDir, receipt, input.workspaceRoot, physicalFailures, candidateFiles, candidateAbsences, libraryInputs);
|
|
785
792
|
receipt.required_artifacts = [...new Set([...(receipt.required_artifacts ?? []), ...runnerProofArtifacts(runnerEvidence), "engine-outcome.json"])];
|
|
786
793
|
artifacts = collectCheckArtifacts(evidenceDir, receipt.required_artifacts);
|
|
787
794
|
}
|
|
@@ -795,6 +802,7 @@ export function reconcileUnfinishedChecks(context, input) {
|
|
|
795
802
|
repaired.candidate_root = input.workspaceRoot;
|
|
796
803
|
repaired.candidate_files = candidateFiles;
|
|
797
804
|
repaired.candidate_absences = candidateAbsences;
|
|
805
|
+
repaired.candidate_library_inputs = libraryInputs;
|
|
798
806
|
repaired.execution_failures = physicalFailures;
|
|
799
807
|
writeReceipt(pendingReceipt.receipt_path, repaired);
|
|
800
808
|
if (status === "failed")
|
|
@@ -821,6 +829,11 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
821
829
|
const sourceCorrection = Boolean(retryPredecessor && (sourceRepairContractMatches(retryPredecessor, input.group, input.profileHash)
|
|
822
830
|
|| input.retryReason === "check_inputs_changed" && staleRetryContractMatches(retryPredecessor, input.group, input.profileHash, input)));
|
|
823
831
|
const latestContract = receipts.filter(receipt => receipt.command === input.group.command && receipt.profile_hash === input.profileHash && receipt.gate === input.group.gate && receipt.scope === input.scope && receipt.work_id === (input.workId ?? null)).at(-1);
|
|
832
|
+
// Hash files before SQLite's writer reservation. Captured identities remain
|
|
833
|
+
// valid historical evidence, but cannot authorize reuse in a changed install.
|
|
834
|
+
const priorLibrariesCurrent = !prior || currentCandidateLibraryInputsValid(prior, input.workspaceRoot);
|
|
835
|
+
const latestLibrariesCurrent = !latestContract || currentCandidateLibraryInputsValid(latestContract, input.workspaceRoot);
|
|
836
|
+
const libraryDrift = (receipt) => { throw new AppError("check_evidence_stale", "Captured library inputs differ from the current candidate installation; request an explicit restored-condition retry", 2, { reason: "candidate_library_inputs_changed", check_receipt_id: receipt.id, effect: "no_effect" }); };
|
|
824
837
|
return context.db.writeTransaction(() => {
|
|
825
838
|
assertRunMutationAllowed(context, input.projectId, input.runId, input.generation);
|
|
826
839
|
if (JSON.stringify(context.db.all(snapshotSql, [input.projectId, input.runId])) !== snapshot)
|
|
@@ -856,10 +869,16 @@ function reserveOrReuseReceipt(context, input) {
|
|
|
856
869
|
// An unchanged failed contract needs a concrete restored-condition retry,
|
|
857
870
|
// not repeated coordinator finish until it happens to become green.
|
|
858
871
|
if (!retryId && latestContract?.fingerprint_scope === "check_inputs" && latestContract.status === "failed"
|
|
859
|
-
&& latestContract.execution_failures?.some(failure => failure.code === "check_inputs_changed") && latestContract.execution_contract_hash === input.executionContractHash)
|
|
872
|
+
&& latestContract.execution_failures?.some(failure => failure.code === "check_inputs_changed") && latestContract.execution_contract_hash === input.executionContractHash) {
|
|
873
|
+
if (!latestLibrariesCurrent)
|
|
874
|
+
libraryDrift(latestContract);
|
|
860
875
|
return { receipt: latestContract };
|
|
861
|
-
|
|
876
|
+
}
|
|
877
|
+
if (prior && ["failed", "aborted"].includes(prior.status) && !retryId) {
|
|
878
|
+
if (!priorLibrariesCurrent)
|
|
879
|
+
libraryDrift(prior);
|
|
862
880
|
return { receipt: prior };
|
|
881
|
+
}
|
|
863
882
|
if (prior?.status === "passed" && !retryIds.has(prior.id) && input.group.reusable && reusablePrior)
|
|
864
883
|
return { receipt: prior };
|
|
865
884
|
let ordinal = (context.db.get("SELECT COALESCE(MAX(reservation_sequence), 0) + 1 AS sequence FROM check_receipts WHERE project_id = ? AND run_id = ?", [input.projectId, input.runId])?.sequence ?? 1);
|
|
@@ -1001,12 +1020,12 @@ function resolvedReceiptArtifacts(receiptPath, artifacts, binding) {
|
|
|
1001
1020
|
if (retainedOutcome.complete && retainedOutcome.items.every(item => artifacts.some(artifact => relocate(artifact.path) === item.path && artifact.sha256 === item.sha256))) {
|
|
1002
1021
|
try {
|
|
1003
1022
|
const value = JSON.parse(fs.readFileSync(outcomePath, "utf8"));
|
|
1004
|
-
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"))
|
|
1023
|
+
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)) && (value.candidate_library_inputs === undefined || candidateLibraryInputsValid(value.candidate_library_inputs)) && Array.isArray(value.execution_failures) && value.execution_failures.every((failure) => failure && typeof failure === "object" && typeof failure.code === "string" && typeof failure.message === "string"))
|
|
1005
1024
|
outcome = value;
|
|
1006
1025
|
}
|
|
1007
1026
|
catch { /* Missing physical proof cannot grant attribution. */ }
|
|
1008
1027
|
}
|
|
1009
|
-
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, runner_evidence_diagnostics: diagnostics, ...(diagnosticsTruncated ? { runner_evidence_diagnostics_truncated: true } : {}) } : {}), 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 } : {}) };
|
|
1028
|
+
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, runner_evidence_diagnostics: diagnostics, ...(diagnosticsTruncated ? { runner_evidence_diagnostics_truncated: true } : {}) } : {}), 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 } : {}), ...(outcome.candidate_library_inputs ? { candidate_library_inputs: outcome.candidate_library_inputs } : {}) };
|
|
1010
1029
|
}
|
|
1011
1030
|
function executionFailures(input) {
|
|
1012
1031
|
const failures = [];
|
|
@@ -1024,11 +1043,11 @@ function executionFailures(input) {
|
|
|
1024
1043
|
failures.push({ code: "invalid_runner_evidence", message: "Required runner evidence is unavailable or invalid" });
|
|
1025
1044
|
return failures;
|
|
1026
1045
|
}
|
|
1027
|
-
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = []) {
|
|
1046
|
+
function retainExecutionOutcome(evidenceDir, binding, candidateRoot, failures, candidateFiles, candidateAbsences = [], libraryInputs = []) {
|
|
1028
1047
|
const temporary = path.join(evidenceDir, `.engine-outcome-${crypto.randomUUID()}.tmp`);
|
|
1029
1048
|
if (!fs.lstatSync(evidenceDir).isDirectory() || fs.lstatSync(evidenceDir).isSymbolicLink())
|
|
1030
1049
|
throw new AppError("check_evidence_stale", "Check evidence directory was replaced", 1);
|
|
1031
|
-
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 });
|
|
1050
|
+
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, candidate_library_inputs: libraryInputs, execution_failures: failures }), { flag: "wx", mode: 0o600 });
|
|
1032
1051
|
fs.renameSync(temporary, path.join(evidenceDir, "engine-outcome.json"));
|
|
1033
1052
|
}
|
|
1034
1053
|
export function workspaceFingerprint(root) { return workspaceState(root).fingerprint; }
|
|
@@ -1358,7 +1377,7 @@ function checkReceiptEvidenceValidForMode(receipt, mode) {
|
|
|
1358
1377
|
// Receipt annotations are not execution authority. Compare the SQL-owned
|
|
1359
1378
|
// failures with the independently hash/binding-checked engine sidecar.
|
|
1360
1379
|
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"));
|
|
1361
|
-
if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? [])))
|
|
1380
|
+
if (hasEngineOutcome && (!isDeepStrictEqual(retainedProof.execution_failures ?? [], receipt.execution_failures ?? []) || !isDeepStrictEqual(retainedProof.candidate_absences ?? [], receipt.candidate_absences ?? []) || !isDeepStrictEqual(retainedProof.candidate_library_inputs ?? [], receipt.candidate_library_inputs ?? [])))
|
|
1362
1381
|
return false;
|
|
1363
1382
|
const root = path.join(path.dirname(receipt.receipt_path), "artifacts");
|
|
1364
1383
|
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 })))
|