kiokuko-dsh 0.2.26 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context/replay-model-memory.d.ts +2 -2
- package/dist/context/replay-model-memory.d.ts.map +1 -1
- package/dist/context/replay-model-memory.js +70 -16
- package/dist/context/replay-model-memory.js.map +1 -1
- package/dist/dsh/host-adapter/lifecycle.d.ts +20 -18
- package/dist/dsh/host-adapter/lifecycle.d.ts.map +1 -1
- package/dist/dsh/host-adapter/lifecycle.js +298 -102
- package/dist/dsh/host-adapter/lifecycle.js.map +1 -1
- package/dist/dsh/host-adapter.d.ts +47 -47
- package/dist/dsh/host-adapter.d.ts.map +1 -1
- package/dist/dsh/host-adapter.js +1213 -592
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/index.d.ts +42 -41
- package/dist/dsh/index.d.ts.map +1 -1
- package/dist/dsh/index.js +166 -92
- package/dist/dsh/index.js.map +1 -1
- package/dist/dsh/lifecycle-memory.d.ts +11 -7
- package/dist/dsh/lifecycle-memory.d.ts.map +1 -1
- package/dist/dsh/lifecycle-memory.js +599 -172
- package/dist/dsh/lifecycle-memory.js.map +1 -1
- package/dist/dsh/task-completion.d.ts +7 -7
- package/dist/dsh/task-completion.d.ts.map +1 -1
- package/dist/dsh/task-completion.js +554 -194
- package/dist/dsh/task-completion.js.map +1 -1
- package/dist/dsh/task-state-host.d.ts +10 -10
- package/dist/dsh/task-state-host.d.ts.map +1 -1
- package/dist/dsh/task-state-host.js +337 -145
- package/dist/dsh/task-state-host.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/locale/en.json +1 -1
- package/dist/locale/ja.json +1 -1
- package/dist/locale/ko.json +1 -1
- package/dist/locale/zh-CN.json +1 -1
- package/dist/memory/replay-reflection.d.ts +94 -0
- package/dist/memory/replay-reflection.d.ts.map +1 -0
- package/dist/memory/replay-reflection.js +149 -0
- package/dist/memory/replay-reflection.js.map +1 -0
- package/dist/memory/replay-review.d.ts +18 -12
- package/dist/memory/replay-review.d.ts.map +1 -1
- package/dist/memory/replay-review.js +107 -46
- package/dist/memory/replay-review.js.map +1 -1
- package/package.json +2 -1
|
@@ -1,261 +1,464 @@
|
|
|
1
|
-
import { assertionForCriterion, COMPLETION_BINDING_GUIDE } from
|
|
2
|
-
import { ProtocolRecipeSchema, PROTOCOL_CAPABILITIES } from
|
|
3
|
-
import { enrollProtocol, invalidateProtocolBinding, protocolCompletionReady, unresolvedManaged } from
|
|
4
|
-
import { realpathSync, existsSync, readFileSync } from
|
|
5
|
-
import { createHash } from
|
|
6
|
-
import path from
|
|
7
|
-
import * as z from
|
|
8
|
-
import { withImmediateTransaction } from
|
|
9
|
-
import { KiokukoError } from
|
|
10
|
-
import { applicationSourceDigest } from
|
|
11
|
-
import { canonicalContentHash } from
|
|
12
|
-
import { completionVerifiers } from
|
|
13
|
-
import { readExecutionFrame } from
|
|
14
|
-
import { hasExplicitTestCases, parseNodeTapSummary, parseTestSummary, selectedNodeTestCommand } from
|
|
15
|
-
import { isHostExecutionResult } from
|
|
16
|
-
import { executionCommand, EXECUTION_COMMAND_VERSION, hasShellPipeline } from
|
|
17
|
-
import { readCodingWorkflow } from
|
|
18
|
-
import { recordCodingAttempt } from
|
|
19
|
-
export const CompletionConfig = z
|
|
1
|
+
import { assertionForCriterion, COMPLETION_BINDING_GUIDE, } from "./task-completion-guidance.js";
|
|
2
|
+
import { ProtocolRecipeSchema, PROTOCOL_CAPABILITIES, } from "./guardrail/contracts.js";
|
|
3
|
+
import { enrollProtocol, invalidateProtocolBinding, protocolCompletionReady, unresolvedManaged, } from "./guardrail/service.js";
|
|
4
|
+
import { realpathSync, existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import * as z from "zod/v4";
|
|
8
|
+
import { withImmediateTransaction } from "../db/transaction.js";
|
|
9
|
+
import { KiokukoError } from "../errors.js";
|
|
10
|
+
import { applicationSourceDigest } from "../memory/application.js";
|
|
11
|
+
import { canonicalContentHash } from "../serialization/validate.js";
|
|
12
|
+
import { completionVerifiers } from "./completion-verifiers.js";
|
|
13
|
+
import { readExecutionFrame } from "./execution-frame.js";
|
|
14
|
+
import { hasExplicitTestCases, parseNodeTapSummary, parseTestSummary, selectedNodeTestCommand, } from "./node-tap-summary.js";
|
|
15
|
+
import { isHostExecutionResult } from "./execution-result.js";
|
|
16
|
+
import { executionCommand, EXECUTION_COMMAND_VERSION, hasShellPipeline, } from "./execution-command.js";
|
|
17
|
+
import { readCodingWorkflow } from "./coding-workflow/store.js";
|
|
18
|
+
import { recordCodingAttempt } from "./coding-workflow/service.js";
|
|
19
|
+
export const CompletionConfig = z
|
|
20
|
+
.object({ mode: z.enum(["shadow", "enforce"]).default("shadow") })
|
|
21
|
+
.strict();
|
|
20
22
|
const text = z.string().trim().min(1).max(4096);
|
|
21
|
-
const relativePath = z
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const relativePath = z
|
|
24
|
+
.string()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(512)
|
|
27
|
+
.refine((value) => !path.isAbsolute(value) &&
|
|
28
|
+
!value.split(/[\\/]/u).includes("..") &&
|
|
29
|
+
!/[\p{Cc}\p{Cf}]/u.test(value));
|
|
30
|
+
export const CompletionMethodSchema = z.discriminatedUnion("kind", [
|
|
31
|
+
z
|
|
32
|
+
.object({
|
|
33
|
+
kind: z.literal("native_command"),
|
|
34
|
+
command: text,
|
|
35
|
+
cwd: text,
|
|
36
|
+
sourcePaths: z.array(relativePath).min(1).max(32),
|
|
37
|
+
assertion: z.enum([
|
|
38
|
+
"exit_zero",
|
|
39
|
+
"selected_tests_pass",
|
|
40
|
+
"output_matches",
|
|
41
|
+
"artifact_matches",
|
|
42
|
+
]),
|
|
26
43
|
protocol: ProtocolRecipeSchema.optional(),
|
|
27
|
-
expectedOutput: text.optional(),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
expectedOutput: text.optional(),
|
|
45
|
+
artifactPath: relativePath.optional(),
|
|
46
|
+
artifactSha256: z
|
|
47
|
+
.string()
|
|
48
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
49
|
+
.optional(),
|
|
50
|
+
})
|
|
51
|
+
.strict()
|
|
52
|
+
.superRefine((value, ctx) => {
|
|
53
|
+
if (value.protocol && value.assertion !== "selected_tests_pass")
|
|
54
|
+
ctx.addIssue({
|
|
55
|
+
code: "custom",
|
|
56
|
+
path: ["assertion"],
|
|
57
|
+
message: "Protocol recipes require selected tests",
|
|
58
|
+
});
|
|
59
|
+
if (value.assertion === "output_matches" && !value.expectedOutput)
|
|
60
|
+
ctx.addIssue({
|
|
61
|
+
code: "custom",
|
|
62
|
+
path: ["expectedOutput"],
|
|
63
|
+
message: "Output assertion requires expectedOutput",
|
|
64
|
+
});
|
|
65
|
+
if (value.assertion === "artifact_matches") {
|
|
34
66
|
if (!value.artifactPath)
|
|
35
|
-
ctx.addIssue({
|
|
67
|
+
ctx.addIssue({
|
|
68
|
+
code: "custom",
|
|
69
|
+
path: ["artifactPath"],
|
|
70
|
+
message: "Artifact assertion requires path",
|
|
71
|
+
});
|
|
36
72
|
if (!value.artifactSha256)
|
|
37
|
-
ctx.addIssue({
|
|
73
|
+
ctx.addIssue({
|
|
74
|
+
code: "custom",
|
|
75
|
+
path: ["artifactSha256"],
|
|
76
|
+
message: "Artifact assertion requires SHA-256",
|
|
77
|
+
});
|
|
38
78
|
}
|
|
39
79
|
}),
|
|
40
|
-
z
|
|
41
|
-
|
|
80
|
+
z
|
|
81
|
+
.object({
|
|
82
|
+
kind: z.literal("enno_verifier"),
|
|
83
|
+
verifierId: text,
|
|
84
|
+
workUnitId: text.optional(),
|
|
85
|
+
assertion: z.enum(["exit_zero", "selected_tests_pass"]),
|
|
86
|
+
})
|
|
87
|
+
.strict(),
|
|
42
88
|
]);
|
|
43
89
|
export { parseNodeTapSummary, selectedNodeTestCommand };
|
|
44
90
|
export function initializeTaskCompletion(database, runId, mode) {
|
|
45
|
-
database
|
|
91
|
+
database
|
|
92
|
+
.prepare("INSERT OR IGNORE INTO dsh_completion_runs(run_id,mode,created_at) VALUES(?,?,?)")
|
|
46
93
|
.run(runId, mode, new Date().toISOString());
|
|
47
94
|
}
|
|
48
95
|
function runMode(database, runId) {
|
|
49
|
-
return database
|
|
96
|
+
return (database
|
|
97
|
+
.prepare("SELECT mode FROM dsh_completion_runs WHERE run_id = ?")
|
|
98
|
+
.get(runId)?.mode ?? "shadow");
|
|
50
99
|
}
|
|
51
100
|
function criteriaForRun(database, runId) {
|
|
52
101
|
const coding = readCodingWorkflow(database, runId);
|
|
53
102
|
if (coding)
|
|
54
|
-
return {
|
|
55
|
-
|
|
56
|
-
|
|
103
|
+
return {
|
|
104
|
+
repositoryRoot: coding.repositoryRoot,
|
|
105
|
+
enno: database
|
|
106
|
+
.prepare("SELECT 1 FROM dsh_execution_selections WHERE run_id=? AND json_extract(state_json,'$.mode')='enno'")
|
|
107
|
+
.get(runId) !== undefined,
|
|
108
|
+
criteria: (coding.definition?.criteria ?? []).map((item) => ({
|
|
109
|
+
id: `coding:${coding.workflowId}:${item.criterionId}`,
|
|
110
|
+
description: `${item.outcome} / ${item.procedure} / ${item.expected}`,
|
|
111
|
+
revision: item.revision,
|
|
112
|
+
approval: "explicit",
|
|
113
|
+
})),
|
|
114
|
+
};
|
|
115
|
+
const row = database
|
|
116
|
+
.prepare(`SELECT contract.repository_root AS root, contract.workspace,
|
|
57
117
|
contract.orchestration_session_id AS orchestrationId FROM ledger_runs AS ledger
|
|
58
118
|
LEFT JOIN enno_contracts AS contract ON contract.run_id = ledger.run_id WHERE ledger.run_id = ?`)
|
|
59
119
|
.get(runId);
|
|
60
120
|
if (!row)
|
|
61
|
-
throw new KiokukoError(
|
|
121
|
+
throw new KiokukoError("NOT_FOUND", "Task completion run does not exist");
|
|
62
122
|
if (row.workspace && row.orchestrationId && row.root) {
|
|
63
|
-
const snapshot = completionVerifiers().criteria(database, {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
123
|
+
const snapshot = completionVerifiers().criteria(database, {
|
|
124
|
+
runId,
|
|
125
|
+
workspace: row.workspace,
|
|
126
|
+
orchestrationId: row.orchestrationId,
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
repositoryRoot: row.root,
|
|
130
|
+
enno: true,
|
|
131
|
+
criteria: snapshot.criteria.map((item) => ({
|
|
132
|
+
id: item.id,
|
|
133
|
+
description: item.description,
|
|
134
|
+
revision: snapshot.revision,
|
|
135
|
+
approval: "approved",
|
|
136
|
+
})),
|
|
137
|
+
};
|
|
67
138
|
}
|
|
68
139
|
const frame = readExecutionFrame(database, runId);
|
|
69
|
-
return {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
140
|
+
return {
|
|
141
|
+
repositoryRoot: frame?.workspace ?? "",
|
|
142
|
+
enno: false,
|
|
143
|
+
criteria: (frame?.conditions ?? [])
|
|
144
|
+
.filter((item) => item.field === "completion" && item.approval !== "proposed")
|
|
145
|
+
.map((item, index) => ({
|
|
146
|
+
id: `condition-${canonicalContentHash({
|
|
147
|
+
runId,
|
|
148
|
+
revision: frame.revision,
|
|
149
|
+
index,
|
|
150
|
+
text: item.text,
|
|
151
|
+
source: item.source,
|
|
152
|
+
}).slice(0, 24)}`,
|
|
153
|
+
description: item.text,
|
|
154
|
+
revision: frame.revision,
|
|
155
|
+
approval: item.approval,
|
|
156
|
+
})),
|
|
157
|
+
};
|
|
74
158
|
}
|
|
75
159
|
/** Advice reads the same immutable criterion contract as binding; it cannot supply execution proof. */
|
|
76
160
|
export function taskCompletionBindingGuide(database, runId) {
|
|
77
161
|
const source = criteriaForRun(database, runId), coding = readCodingWorkflow(database, runId);
|
|
78
|
-
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
162
|
+
return {
|
|
163
|
+
...structuredClone(COMPLETION_BINDING_GUIDE),
|
|
164
|
+
kind: source.enno ? "enno_verifier" : "native_command",
|
|
165
|
+
criteria: source.criteria.map((criterion) => {
|
|
166
|
+
const definition = coding?.definition?.criteria.find((item) => criterion.id === `coding:${coding.workflowId}:${item.criterionId}`);
|
|
167
|
+
return {
|
|
168
|
+
criterionId: criterion.id,
|
|
169
|
+
requiredAssertion: definition
|
|
170
|
+
? assertionForCriterion(definition.evidence)
|
|
171
|
+
: null,
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
82
175
|
}
|
|
83
176
|
function binding(database, runId, criterionId) {
|
|
84
|
-
return database
|
|
177
|
+
return database
|
|
178
|
+
.prepare("SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND criterion_id = ?")
|
|
85
179
|
.get(runId, criterionId);
|
|
86
180
|
}
|
|
87
181
|
function validateMethod(database, runId, method, repositoryRoot, enno) {
|
|
88
182
|
const coding = readCodingWorkflow(database, runId);
|
|
89
|
-
if (coding && method.assertion ===
|
|
90
|
-
throw new KiokukoError(
|
|
91
|
-
if (enno !== (method.kind ===
|
|
92
|
-
throw new KiokukoError(
|
|
93
|
-
|
|
183
|
+
if (coding && method.assertion === "exit_zero")
|
|
184
|
+
throw new KiokukoError("VALIDATION_ERROR", "Coding criteria require selected tests or observable output/artifact assertions, not exit 0 alone");
|
|
185
|
+
if (enno !== (method.kind === "enno_verifier"))
|
|
186
|
+
throw new KiokukoError("VALIDATION_ERROR", "Completion method does not match execution mode", {
|
|
187
|
+
field: "method.kind",
|
|
188
|
+
expected: enno ? "enno_verifier" : "native_command",
|
|
189
|
+
});
|
|
190
|
+
if (method.kind === "native_command") {
|
|
94
191
|
const cwd = realpathSync(path.resolve(repositoryRoot, method.cwd));
|
|
95
192
|
const relative = path.relative(realpathSync(repositoryRoot), cwd);
|
|
96
|
-
if (relative.startsWith(
|
|
97
|
-
throw new KiokukoError(
|
|
193
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
194
|
+
throw new KiokukoError("SECURITY_REJECTION", "Completion cwd must be inside the repository");
|
|
98
195
|
applicationSourceDigest(repositoryRoot, method.sourcePaths);
|
|
99
196
|
// The pre-bound command selects the operation; complete process output,
|
|
100
197
|
// rather than a command-name allowlist, establishes test coverage.
|
|
101
198
|
}
|
|
102
199
|
else {
|
|
103
|
-
const row = database
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
200
|
+
const row = database
|
|
201
|
+
.prepare("SELECT contract_json FROM enno_contracts WHERE run_id = ?")
|
|
202
|
+
.get(runId);
|
|
203
|
+
const contract = row
|
|
204
|
+
? JSON.parse(row.contract_json)
|
|
205
|
+
: undefined;
|
|
206
|
+
const verifiers = method.workUnitId
|
|
207
|
+
? contract?.workPlan?.units.find((unit) => unit.id === method.workUnitId)
|
|
208
|
+
?.focusedVerifiers
|
|
209
|
+
: contract?.finalVerifiers;
|
|
210
|
+
const verifier = verifiers?.find((item) => item.id === method.verifierId);
|
|
107
211
|
if (!verifier)
|
|
108
|
-
throw new KiokukoError(
|
|
109
|
-
if (!coding &&
|
|
110
|
-
|
|
212
|
+
throw new KiokukoError("VALIDATION_ERROR", "Completion verifier is absent from the approved Enno contract", { field: "method.verifierId", expected: "approved_contract_verifier" });
|
|
213
|
+
if (!coding &&
|
|
214
|
+
method.assertion === "selected_tests_pass" &&
|
|
215
|
+
!selectedNodeTestCommand([verifier.executable, ...verifier.args].join(" "))) {
|
|
216
|
+
throw new KiokukoError("VALIDATION_ERROR", "Selected-test proof requires an explicit Node TAP verifier");
|
|
111
217
|
}
|
|
112
218
|
}
|
|
113
219
|
}
|
|
114
220
|
export function bindTaskCriterion(database, input) {
|
|
115
221
|
const method = CompletionMethodSchema.parse(input.method);
|
|
116
|
-
const requestDigest = canonicalContentHash({
|
|
117
|
-
|
|
222
|
+
const requestDigest = canonicalContentHash({
|
|
223
|
+
criterionId: input.criterionId,
|
|
224
|
+
method,
|
|
225
|
+
});
|
|
226
|
+
const previous = database
|
|
227
|
+
.prepare("SELECT request_digest, response_json FROM dsh_completion_bind_ops WHERE run_id = ? AND call_id = ?")
|
|
118
228
|
.get(input.runId, input.callId);
|
|
119
229
|
if (previous) {
|
|
120
230
|
if (previous.request_digest !== requestDigest)
|
|
121
|
-
throw new KiokukoError(
|
|
231
|
+
throw new KiokukoError("CONFLICT", "Task completion call ID was reused with changed input");
|
|
122
232
|
return JSON.parse(previous.response_json);
|
|
123
233
|
}
|
|
124
234
|
const source = criteriaForRun(database, input.runId);
|
|
125
|
-
const criterion = source.criteria.find(item => item.id === input.criterionId);
|
|
235
|
+
const criterion = source.criteria.find((item) => item.id === input.criterionId);
|
|
126
236
|
if (!criterion)
|
|
127
|
-
throw new KiokukoError(
|
|
237
|
+
throw new KiokukoError("CONFLICT", "Completion criterion is absent or its revision changed");
|
|
128
238
|
const codingWorkflow = readCodingWorkflow(database, input.runId);
|
|
129
|
-
const codingCriterion = codingWorkflow?.definition?.criteria.find(item => input.criterionId ===
|
|
130
|
-
|
|
239
|
+
const codingCriterion = codingWorkflow?.definition?.criteria.find((item) => input.criterionId ===
|
|
240
|
+
`coding:${codingWorkflow.workflowId}:${item.criterionId}`);
|
|
241
|
+
const requiredAssertion = assertionForCriterion(codingCriterion?.evidence ?? "selected_tests");
|
|
131
242
|
if (codingCriterion && method.assertion !== requiredAssertion)
|
|
132
|
-
throw new KiokukoError(
|
|
133
|
-
if (method.kind ===
|
|
134
|
-
|
|
243
|
+
throw new KiokukoError("VALIDATION_ERROR", "Bound assertion does not match this criterion’s required evidence", { field: "method.assertion", expected: requiredAssertion });
|
|
244
|
+
if (method.kind === "native_command" &&
|
|
245
|
+
method.artifactPath &&
|
|
246
|
+
!method.sourcePaths.includes(method.artifactPath))
|
|
247
|
+
throw new KiokukoError("VALIDATION_ERROR", "Include the asserted artifact in sourcePaths so changes invalidate its evidence", { field: "method.sourcePaths", expected: "include_asserted_artifact" });
|
|
135
248
|
validateMethod(database, input.runId, method, source.repositoryRoot, source.enno);
|
|
136
|
-
const approved = input.approved ||
|
|
137
|
-
|
|
249
|
+
const approved = input.approved ||
|
|
250
|
+
criterion.description.trim() ===
|
|
251
|
+
(method.kind === "native_command" ? method.command : method.verifierId);
|
|
138
252
|
// One approved command may substantiate several criteria in the same
|
|
139
253
|
// revision, but never a criterion from a later contract revision.
|
|
140
254
|
const coding = readCodingWorkflow(database, input.runId);
|
|
141
|
-
const methodDigest = canonicalContentHash({
|
|
255
|
+
const methodDigest = canonicalContentHash({
|
|
256
|
+
revision: criterion.revision,
|
|
257
|
+
method,
|
|
258
|
+
...(coding ? { scope: coding.scopeVersion } : {}),
|
|
259
|
+
});
|
|
142
260
|
const response = { criterionId: criterion.id, approved, methodDigest };
|
|
143
261
|
withImmediateTransaction(database, () => {
|
|
144
262
|
const prior = binding(database, input.runId, criterion.id);
|
|
145
|
-
if (!prior ||
|
|
146
|
-
|
|
147
|
-
|
|
263
|
+
if (!prior ||
|
|
264
|
+
prior.criterion_revision !== criterion.revision ||
|
|
265
|
+
prior.method_digest !== methodDigest ||
|
|
266
|
+
prior.approved !== (approved ? 1 : 0)) {
|
|
267
|
+
database
|
|
268
|
+
.prepare(`INSERT INTO dsh_completion_bindings(run_id,criterion_id,criterion_revision,method_json,method_digest,approved,updated_at)
|
|
148
269
|
VALUES(?,?,?,?,?,?,?) ON CONFLICT(run_id,criterion_id) DO UPDATE SET
|
|
149
270
|
criterion_revision=excluded.criterion_revision,method_json=excluded.method_json,method_digest=excluded.method_digest,
|
|
150
271
|
approved=excluded.approved,updated_at=excluded.updated_at`)
|
|
151
272
|
.run(input.runId, criterion.id, criterion.revision, JSON.stringify(method), methodDigest, approved ? 1 : 0, new Date().toISOString());
|
|
152
273
|
}
|
|
153
|
-
if (approved && method.kind ===
|
|
154
|
-
enrollProtocol(database, {
|
|
274
|
+
if (approved && method.kind === "native_command" && method.protocol)
|
|
275
|
+
enrollProtocol(database, {
|
|
276
|
+
runId: input.runId,
|
|
277
|
+
sessionId: coding.sessionId,
|
|
278
|
+
repositoryRoot: source.repositoryRoot,
|
|
279
|
+
}, {
|
|
280
|
+
criterionId: input.criterionId,
|
|
281
|
+
methodDigest,
|
|
282
|
+
command: method.command,
|
|
283
|
+
cwd: method.cwd,
|
|
284
|
+
sourcePaths: method.sourcePaths,
|
|
285
|
+
recipe: method.protocol,
|
|
286
|
+
});
|
|
155
287
|
else
|
|
156
288
|
invalidateProtocolBinding(database, input.runId, input.criterionId, methodDigest);
|
|
157
|
-
database
|
|
289
|
+
database
|
|
290
|
+
.prepare("INSERT INTO dsh_completion_bind_ops(run_id,call_id,request_digest,response_json) VALUES(?,?,?,?)")
|
|
158
291
|
.run(input.runId, input.callId, requestDigest, JSON.stringify(response));
|
|
159
292
|
});
|
|
160
293
|
return response;
|
|
161
294
|
}
|
|
162
295
|
export function readTaskCriterionBindingReplay(database, runId, callId, criterionId, method) {
|
|
163
296
|
const parsed = CompletionMethodSchema.parse(method);
|
|
164
|
-
const row = database
|
|
297
|
+
const row = database
|
|
298
|
+
.prepare("SELECT request_digest, response_json FROM dsh_completion_bind_ops WHERE run_id = ? AND call_id = ?")
|
|
165
299
|
.get(runId, callId);
|
|
166
300
|
if (!row)
|
|
167
301
|
return undefined;
|
|
168
302
|
if (row.request_digest !== canonicalContentHash({ criterionId, method: parsed })) {
|
|
169
|
-
throw new KiokukoError(
|
|
303
|
+
throw new KiokukoError("CONFLICT", "Task completion call ID was reused with changed input");
|
|
170
304
|
}
|
|
171
305
|
return JSON.parse(row.response_json);
|
|
172
306
|
}
|
|
173
307
|
export function beginTaskCompletionExecution(database, input) {
|
|
174
308
|
const source = criteriaForRun(database, input.runId);
|
|
175
|
-
if (source.enno ||
|
|
309
|
+
if (source.enno ||
|
|
310
|
+
realpathSync(input.repositoryRoot) !== realpathSync(source.repositoryRoot))
|
|
176
311
|
return false;
|
|
177
|
-
const rows = database
|
|
312
|
+
const rows = database
|
|
313
|
+
.prepare("SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND approved = 1")
|
|
178
314
|
.all(input.runId);
|
|
179
|
-
const matching = rows.filter(row => {
|
|
315
|
+
const matching = rows.filter((row) => {
|
|
180
316
|
const method = CompletionMethodSchema.parse(JSON.parse(row.method_json));
|
|
181
|
-
return method.kind ===
|
|
182
|
-
|
|
183
|
-
|
|
317
|
+
return (method.kind === "native_command" &&
|
|
318
|
+
executionCommand(method.command) === executionCommand(input.command) &&
|
|
319
|
+
realpathSync(path.resolve(source.repositoryRoot, method.cwd)) ===
|
|
320
|
+
realpathSync(input.cwd ?? input.repositoryRoot) &&
|
|
321
|
+
source.criteria.some((criterion) => criterion.id === row.criterion_id &&
|
|
322
|
+
criterion.revision === row.criterion_revision));
|
|
184
323
|
});
|
|
185
324
|
const coding = readCodingWorkflow(database, input.runId);
|
|
186
|
-
const assessment = coding
|
|
187
|
-
|
|
188
|
-
|
|
325
|
+
const assessment = coding
|
|
326
|
+
? assessTaskCompletion(database, input.runId)
|
|
327
|
+
: undefined;
|
|
328
|
+
const goalReady = coding?.definition?.items.every((item) => item.criterionIds.every((id) => assessment.criteria.some((criterion) => criterion.criterionId === `coding:${coding.workflowId}:${id}` &&
|
|
329
|
+
criterion.state === "satisfied"))) ?? false;
|
|
330
|
+
const isGoal = (row) => coding?.definition?.goal.criterionIds.some((id) => row.criterion_id === `coding:${coding.workflowId}:${id}`);
|
|
189
331
|
// A protocol recipe changes the item method identity. Observe final Goal
|
|
190
332
|
// bindings separately, only after item evidence exists on the current source.
|
|
191
333
|
const current = coding?.protocol
|
|
192
334
|
? goalReady
|
|
193
|
-
? matching.find(row => isGoal(row) &&
|
|
194
|
-
|
|
335
|
+
? (matching.find((row) => isGoal(row) &&
|
|
336
|
+
assessment.criteria.find((item) => item.criterionId === row.criterion_id)?.state !== "satisfied") ??
|
|
337
|
+
matching.find(isGoal) ??
|
|
338
|
+
matching[0])
|
|
339
|
+
: (matching.find((row) => row.criterion_id === coding.protocol.criterionId) ?? matching[0])
|
|
195
340
|
: matching[0];
|
|
196
341
|
if (!current)
|
|
197
342
|
return false;
|
|
198
343
|
const method = CompletionMethodSchema.parse(JSON.parse(current.method_json));
|
|
199
|
-
if (method.kind !==
|
|
344
|
+
if (method.kind !== "native_command")
|
|
200
345
|
return false;
|
|
201
346
|
const digest = completionSourceDigest(database, input.runId, source.repositoryRoot, method.sourcePaths);
|
|
202
|
-
const codingInputs = coding?.definition
|
|
203
|
-
|
|
204
|
-
|
|
347
|
+
const codingInputs = coding?.definition
|
|
348
|
+
? coding.definition.items.flatMap((item) => item.criterionIds.map((id) => ({
|
|
349
|
+
id,
|
|
350
|
+
evidenceRef: assessment.criteria.find((criterion) => criterion.criterionId === `coding:${coding.workflowId}:${id}`)?.evidenceRef ?? null,
|
|
351
|
+
})))
|
|
352
|
+
: null;
|
|
353
|
+
const existing = database
|
|
354
|
+
.prepare("SELECT method_digest, source_digest, outcome FROM dsh_completion_executions WHERE run_id = ? AND call_id = ?")
|
|
205
355
|
.get(input.runId, input.callId);
|
|
206
356
|
if (existing) {
|
|
207
|
-
if (existing.method_digest !== current.method_digest ||
|
|
208
|
-
|
|
357
|
+
if (existing.method_digest !== current.method_digest ||
|
|
358
|
+
existing.source_digest !== digest) {
|
|
359
|
+
throw new KiokukoError("CONFLICT", "Task completion execution ID was reused with changed bound input");
|
|
209
360
|
}
|
|
210
|
-
return existing.outcome ===
|
|
361
|
+
return existing.outcome === "started";
|
|
211
362
|
}
|
|
212
|
-
database
|
|
363
|
+
database
|
|
364
|
+
.prepare(`INSERT OR IGNORE INTO dsh_completion_executions
|
|
213
365
|
(run_id,call_id,method_digest,source_digest,outcome,updated_at,coding_scope,coding_goal_ready,coding_inputs_json) VALUES(?,?,?,?,'started',?,?,?,?)`)
|
|
214
366
|
.run(input.runId, input.callId, current.method_digest, digest, new Date().toISOString(), coding?.scopeVersion ?? null, goalReady ? 1 : 0, codingInputs ? JSON.stringify(codingInputs) : null);
|
|
215
367
|
return true;
|
|
216
368
|
}
|
|
217
369
|
export function finishTaskCompletionExecution(database, input) {
|
|
218
|
-
const row = database
|
|
370
|
+
const row = database
|
|
371
|
+
.prepare("SELECT * FROM dsh_completion_executions WHERE run_id = ? AND call_id = ?")
|
|
219
372
|
.get(input.runId, input.callId);
|
|
220
|
-
if (!row || row.outcome !==
|
|
373
|
+
if (!row || row.outcome !== "started")
|
|
221
374
|
return;
|
|
222
375
|
const source = criteriaForRun(database, input.runId);
|
|
223
|
-
const bindingRow = database
|
|
376
|
+
const bindingRow = database
|
|
377
|
+
.prepare("SELECT * FROM dsh_completion_bindings WHERE run_id = ? AND method_digest = ?")
|
|
224
378
|
.get(input.runId, row.method_digest);
|
|
225
379
|
if (!bindingRow)
|
|
226
380
|
return;
|
|
227
381
|
const method = CompletionMethodSchema.parse(JSON.parse(bindingRow.method_json));
|
|
228
|
-
if (method.kind !==
|
|
382
|
+
if (method.kind !== "native_command")
|
|
229
383
|
return;
|
|
230
384
|
let sourceDigest;
|
|
231
385
|
try {
|
|
232
386
|
sourceDigest = completionSourceDigest(database, input.runId, source.repositoryRoot, method.sourcePaths);
|
|
233
387
|
}
|
|
234
|
-
catch {
|
|
388
|
+
catch {
|
|
389
|
+
/* stale or unavailable */
|
|
390
|
+
}
|
|
235
391
|
const response = input.result;
|
|
236
392
|
const exit = response?.value?.exitCode ?? response?.value?.exit_code;
|
|
237
|
-
const content = typeof response?.content ===
|
|
238
|
-
? response.content
|
|
393
|
+
const content = typeof response?.content === "string"
|
|
394
|
+
? response.content
|
|
395
|
+
: Array.isArray(response?.content)
|
|
396
|
+
? response.content
|
|
397
|
+
.map((block) => (typeof block?.text === "string" ? block.text : ""))
|
|
398
|
+
.join("\n")
|
|
399
|
+
: "";
|
|
239
400
|
const coding = readCodingWorkflow(database, input.runId);
|
|
240
|
-
const tap = method.assertion ===
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
:
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
401
|
+
const tap = method.assertion === "selected_tests_pass" &&
|
|
402
|
+
(!coding || hasExplicitTestCases(content))
|
|
403
|
+
? parseTestSummary(content, isHostExecutionResult(input.result))
|
|
404
|
+
: undefined;
|
|
405
|
+
const observable = method.assertion === "output_matches"
|
|
406
|
+
? content.replace(/\r\n/gu, "\n").trimEnd() ===
|
|
407
|
+
method.expectedOutput.replace(/\r\n/gu, "\n").trimEnd() &&
|
|
408
|
+
isHostExecutionResult(input.result)
|
|
409
|
+
: method.assertion === "artifact_matches"
|
|
410
|
+
? artifactMatches(source.repositoryRoot, method)
|
|
411
|
+
: true;
|
|
412
|
+
const outcome = sourceDigest !== row.source_digest ||
|
|
413
|
+
(coding &&
|
|
414
|
+
(row.coding_scope !== coding.scopeVersion ||
|
|
415
|
+
["cancelled", "superseded"].includes(coding.state)))
|
|
416
|
+
? "stale"
|
|
417
|
+
: response?.isError ||
|
|
418
|
+
response?.value?.timedOut ||
|
|
419
|
+
response?.value?.aborted ||
|
|
420
|
+
response?.value?.signal ||
|
|
421
|
+
response?.value?.kind === "background" ||
|
|
422
|
+
(Number.isSafeInteger(exit) && exit !== 0)
|
|
423
|
+
? "failed"
|
|
424
|
+
: ((coding || method.assertion === "output_matches") &&
|
|
425
|
+
!isHostExecutionResult(input.result)) ||
|
|
426
|
+
!Number.isSafeInteger(exit) ||
|
|
427
|
+
(method.assertion === "exit_zero" &&
|
|
428
|
+
hasShellPipeline(method.command)) ||
|
|
429
|
+
(method.assertion === "selected_tests_pass" && !tap)
|
|
430
|
+
? "unknown"
|
|
431
|
+
: (tap &&
|
|
432
|
+
(tap.fail > 0 ||
|
|
433
|
+
tap.cancelled > 0 ||
|
|
434
|
+
tap.skipped > 0 ||
|
|
435
|
+
tap.todo > 0)) ||
|
|
436
|
+
!observable
|
|
437
|
+
? "failed"
|
|
438
|
+
: "passed";
|
|
439
|
+
database
|
|
440
|
+
.prepare(`UPDATE dsh_completion_executions SET outcome=?,exit_code=?,tap_summary_json=?,result_digest=?,updated_at=?
|
|
250
441
|
WHERE run_id=? AND call_id=? AND outcome='started'`)
|
|
251
|
-
.run(outcome, Number.isSafeInteger(exit) ? exit : null, tap ? JSON.stringify(tap) : null, canonicalContentHash({
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
442
|
+
.run(outcome, Number.isSafeInteger(exit) ? exit : null, tap ? JSON.stringify(tap) : null, canonicalContentHash({
|
|
443
|
+
isError: response?.isError === true,
|
|
444
|
+
value: response?.value,
|
|
445
|
+
content: response?.content,
|
|
446
|
+
}), new Date().toISOString(), input.runId, input.callId);
|
|
447
|
+
if (coding?.definition && (outcome === "failed" || outcome === "unknown")) {
|
|
448
|
+
const fingerprint = canonicalContentHash({
|
|
449
|
+
method: row.method_digest,
|
|
450
|
+
source: row.source_digest,
|
|
451
|
+
outcome,
|
|
452
|
+
exit: exit ?? null,
|
|
453
|
+
tap: tap ?? null,
|
|
454
|
+
});
|
|
455
|
+
const affected = database
|
|
456
|
+
.prepare("SELECT criterion_id FROM dsh_completion_bindings WHERE run_id=? AND method_digest=?")
|
|
255
457
|
.all(input.runId, row.method_digest);
|
|
256
458
|
for (const affectedCriterion of affected) {
|
|
257
459
|
const id = affectedCriterion.criterion_id.slice(`coding:${coding.workflowId}:`.length);
|
|
258
|
-
if (coding.definition.goal.criterionIds.includes(id) &&
|
|
460
|
+
if (coding.definition.goal.criterionIds.includes(id) &&
|
|
461
|
+
row.coding_goal_ready !== 1)
|
|
259
462
|
continue;
|
|
260
463
|
recordCodingAttempt(database, coding, `failure:${input.callId}:${id}`, id, fingerprint, Date.now(), row.source_digest);
|
|
261
464
|
}
|
|
@@ -264,126 +467,283 @@ export function finishTaskCompletionExecution(database, input) {
|
|
|
264
467
|
export function assessTaskCompletion(database, runId) {
|
|
265
468
|
const source = criteriaForRun(database, runId);
|
|
266
469
|
const coding = readCodingWorkflow(database, runId);
|
|
267
|
-
const assessments = source.criteria.map(criterion => {
|
|
470
|
+
const assessments = source.criteria.map((criterion) => {
|
|
268
471
|
const row = binding(database, runId, criterion.id);
|
|
269
472
|
if (!row || row.approved !== 1)
|
|
270
|
-
return {
|
|
271
|
-
|
|
473
|
+
return {
|
|
474
|
+
criterionId: criterion.id,
|
|
475
|
+
description: criterion.description,
|
|
476
|
+
state: "unknown",
|
|
477
|
+
reason: row ? "binding_not_approved" : "no_binding",
|
|
478
|
+
};
|
|
272
479
|
if (row.criterion_revision !== criterion.revision)
|
|
273
|
-
return {
|
|
274
|
-
|
|
480
|
+
return {
|
|
481
|
+
criterionId: criterion.id,
|
|
482
|
+
description: criterion.description,
|
|
483
|
+
state: "stale",
|
|
484
|
+
reason: "criterion_changed",
|
|
485
|
+
};
|
|
275
486
|
const method = CompletionMethodSchema.parse(JSON.parse(row.method_json));
|
|
276
|
-
if (method.kind ===
|
|
277
|
-
const contractRow = database
|
|
487
|
+
if (method.kind === "enno_verifier") {
|
|
488
|
+
const contractRow = database
|
|
489
|
+
.prepare("SELECT workspace, orchestration_session_id AS orchestrationId FROM enno_contracts WHERE run_id = ?")
|
|
278
490
|
.get(runId);
|
|
279
491
|
const fresh = completionVerifiers().results(database, { runId, ...contractRow }, source.repositoryRoot, method.workUnitId);
|
|
280
|
-
const result = fresh?.find(item => item.verifierId === method.verifierId);
|
|
492
|
+
const result = fresh?.find((item) => item.verifierId === method.verifierId);
|
|
281
493
|
if (!result)
|
|
282
|
-
return {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
494
|
+
return {
|
|
495
|
+
criterionId: criterion.id,
|
|
496
|
+
description: criterion.description,
|
|
497
|
+
state: "stale",
|
|
498
|
+
reason: "verifier_evidence_missing_or_stale",
|
|
499
|
+
};
|
|
500
|
+
const complete = method.assertion === "exit_zero" ||
|
|
501
|
+
((!coding || result.explicitCases === true) &&
|
|
502
|
+
result.tapSummary !== undefined &&
|
|
503
|
+
result.tapSummary.tests > 0 &&
|
|
504
|
+
result.tapSummary.pass === result.tapSummary.tests &&
|
|
505
|
+
result.tapSummary.skipped === 0 &&
|
|
506
|
+
result.tapSummary.todo === 0);
|
|
507
|
+
const skipped = method.assertion === "selected_tests_pass" &&
|
|
508
|
+
result.tapSummary !== undefined &&
|
|
509
|
+
(result.tapSummary.skipped > 0 ||
|
|
510
|
+
result.tapSummary.todo > 0 ||
|
|
511
|
+
result.tapSummary.fail > 0);
|
|
512
|
+
return {
|
|
513
|
+
criterionId: criterion.id,
|
|
514
|
+
description: criterion.description,
|
|
515
|
+
state: result.status !== "passed" || skipped
|
|
516
|
+
? "unmet"
|
|
517
|
+
: complete
|
|
518
|
+
? "satisfied"
|
|
519
|
+
: "unknown",
|
|
520
|
+
reason: result.status !== "passed"
|
|
521
|
+
? "verifier_failed"
|
|
522
|
+
: skipped
|
|
523
|
+
? "selected_tests_unmet"
|
|
524
|
+
: complete
|
|
525
|
+
? "fresh_verifier"
|
|
526
|
+
: "selected_test_coverage_unknown",
|
|
527
|
+
evidenceRef: result.receiptId ?? `${runId}:${method.verifierId}`,
|
|
528
|
+
};
|
|
292
529
|
}
|
|
293
|
-
const executions = database
|
|
294
|
-
|
|
530
|
+
const executions = database
|
|
531
|
+
.prepare(`SELECT * FROM dsh_completion_executions WHERE run_id = ? AND method_digest = ?
|
|
532
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`)
|
|
533
|
+
.all(runId, row.method_digest);
|
|
295
534
|
const execution = executions[0];
|
|
296
535
|
if (!execution)
|
|
297
|
-
return {
|
|
536
|
+
return {
|
|
537
|
+
criterionId: criterion.id,
|
|
538
|
+
description: criterion.description,
|
|
539
|
+
state: "unknown",
|
|
540
|
+
reason: "command_not_observed",
|
|
541
|
+
};
|
|
298
542
|
let currentDigest;
|
|
299
543
|
try {
|
|
300
544
|
currentDigest = completionSourceDigest(database, runId, source.repositoryRoot, method.sourcePaths);
|
|
301
545
|
}
|
|
302
|
-
catch {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
546
|
+
catch {
|
|
547
|
+
/* unknown */
|
|
548
|
+
}
|
|
549
|
+
if (execution.source_digest !== currentDigest ||
|
|
550
|
+
execution.outcome === "stale" ||
|
|
551
|
+
(coding && execution.coding_scope !== coding.scopeVersion))
|
|
552
|
+
return {
|
|
553
|
+
criterionId: criterion.id,
|
|
554
|
+
description: criterion.description,
|
|
555
|
+
state: "stale",
|
|
556
|
+
reason: "source_changed",
|
|
557
|
+
evidenceRef: execution.call_id,
|
|
558
|
+
};
|
|
559
|
+
const tap = execution.tap_summary_json
|
|
560
|
+
? JSON.parse(execution.tap_summary_json)
|
|
561
|
+
: undefined;
|
|
562
|
+
const finalGoal = coding?.definition?.goal.criterionIds.some((id) => criterion.id === `coding:${coding.workflowId}:${id}`);
|
|
308
563
|
if (finalGoal && execution.coding_goal_ready !== 1)
|
|
309
|
-
return {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
564
|
+
return {
|
|
565
|
+
criterionId: criterion.id,
|
|
566
|
+
description: criterion.description,
|
|
567
|
+
state: "unknown",
|
|
568
|
+
reason: "final_goal_must_run_after_item_checks",
|
|
569
|
+
evidenceRef: execution.call_id,
|
|
570
|
+
};
|
|
571
|
+
const complete = method.assertion === "exit_zero" ||
|
|
572
|
+
method.assertion === "output_matches" ||
|
|
573
|
+
(method.assertion === "artifact_matches" &&
|
|
574
|
+
artifactMatches(source.repositoryRoot, method)) ||
|
|
575
|
+
(tap !== undefined &&
|
|
576
|
+
tap.tests > 0 &&
|
|
577
|
+
tap.pass === tap.tests &&
|
|
578
|
+
tap.skipped === 0 &&
|
|
579
|
+
tap.todo === 0);
|
|
580
|
+
const skipped = method.assertion === "selected_tests_pass" &&
|
|
581
|
+
tap !== undefined &&
|
|
582
|
+
(tap.skipped > 0 || tap.todo > 0 || tap.fail > 0);
|
|
583
|
+
return {
|
|
584
|
+
criterionId: criterion.id,
|
|
585
|
+
description: criterion.description,
|
|
586
|
+
state: execution.outcome === "failed" || skipped
|
|
587
|
+
? "unmet"
|
|
588
|
+
: execution.outcome === "passed" && complete
|
|
589
|
+
? "satisfied"
|
|
590
|
+
: "unknown",
|
|
591
|
+
reason: execution.outcome === "failed"
|
|
592
|
+
? "command_failed"
|
|
593
|
+
: skipped
|
|
594
|
+
? "selected_tests_unmet"
|
|
595
|
+
: execution.outcome === "passed" && complete
|
|
596
|
+
? "observed_command"
|
|
597
|
+
: method.assertion === "exit_zero" &&
|
|
598
|
+
hasShellPipeline(method.command)
|
|
599
|
+
? "pipeline_stage_status_unknown_use_direct_verifier"
|
|
600
|
+
: "coverage_unknown",
|
|
601
|
+
evidenceRef: execution.call_id,
|
|
602
|
+
};
|
|
319
603
|
});
|
|
320
604
|
const managedOperationsReady = !unresolvedManaged(database, runId);
|
|
321
|
-
const workflowReady = !coding ||
|
|
605
|
+
const workflowReady = !coding ||
|
|
606
|
+
Boolean(protocolCompletionReady(database, runId) &&
|
|
607
|
+
coding.definition &&
|
|
608
|
+
["yes", "no"].includes(coding.choice.state) &&
|
|
609
|
+
!["cancelled", "superseded", "blocked"].includes(coding.state));
|
|
322
610
|
if (coding?.definition) {
|
|
323
|
-
const find = (id) => assessments.find(item => item.criterionId === `coding:${coding.workflowId}:${id}`);
|
|
611
|
+
const find = (id) => assessments.find((item) => item.criterionId === `coding:${coding.workflowId}:${id}`);
|
|
324
612
|
const visited = new Set();
|
|
325
613
|
const evaluate = (id) => {
|
|
326
|
-
const item = coding.definition.items.find(item => item.itemId === id);
|
|
614
|
+
const item = coding.definition.items.find((item) => item.itemId === id);
|
|
327
615
|
if (!visited.has(id)) {
|
|
328
616
|
visited.add(id);
|
|
329
617
|
if (!item.dependsOn.every(evaluate))
|
|
330
618
|
for (const id of item.criterionIds) {
|
|
331
619
|
const criterion = find(id);
|
|
332
|
-
if (criterion.state ===
|
|
333
|
-
criterion.state =
|
|
334
|
-
criterion.reason =
|
|
620
|
+
if (criterion.state === "satisfied") {
|
|
621
|
+
criterion.state = "stale";
|
|
622
|
+
criterion.reason = "dependency_unmet";
|
|
335
623
|
}
|
|
336
624
|
}
|
|
337
625
|
}
|
|
338
|
-
return item.criterionIds.every(id => find(id).state ===
|
|
626
|
+
return item.criterionIds.every((id) => find(id).state === "satisfied");
|
|
339
627
|
};
|
|
340
|
-
const itemsReady = coding.definition.items
|
|
628
|
+
const itemsReady = coding.definition.items
|
|
629
|
+
.map((item) => evaluate(item.itemId))
|
|
630
|
+
.every(Boolean);
|
|
341
631
|
for (const id of coding.definition.goal.criterionIds) {
|
|
342
632
|
const criterion = find(id);
|
|
343
|
-
if (criterion.state !==
|
|
633
|
+
if (criterion.state !== "satisfied")
|
|
344
634
|
continue;
|
|
345
|
-
const row = database
|
|
635
|
+
const row = database
|
|
636
|
+
.prepare("SELECT coding_inputs_json FROM dsh_completion_executions WHERE run_id=? AND call_id=?")
|
|
346
637
|
.get(runId, criterion.evidenceRef);
|
|
347
|
-
const inputs = row?.coding_inputs_json
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
638
|
+
const inputs = row?.coding_inputs_json
|
|
639
|
+
? JSON.parse(row.coding_inputs_json)
|
|
640
|
+
: [];
|
|
641
|
+
if (!itemsReady ||
|
|
642
|
+
inputs.some((input) => find(input.id)?.evidenceRef !== input.evidenceRef &&
|
|
643
|
+
find(input.id)?.evidenceRef !== criterion.evidenceRef)) {
|
|
644
|
+
criterion.state = "stale";
|
|
645
|
+
criterion.reason = "item_evidence_changed";
|
|
351
646
|
}
|
|
352
647
|
}
|
|
353
648
|
}
|
|
354
|
-
return {
|
|
355
|
-
|
|
356
|
-
|
|
649
|
+
return {
|
|
650
|
+
mode: runMode(database, runId),
|
|
651
|
+
managedOperationsReady,
|
|
652
|
+
verified: managedOperationsReady &&
|
|
653
|
+
workflowReady &&
|
|
654
|
+
assessments.length > 0 &&
|
|
655
|
+
assessments.every((item) => item.state === "satisfied"),
|
|
656
|
+
required: assessments.length,
|
|
657
|
+
criteria: assessments,
|
|
658
|
+
...(coding
|
|
659
|
+
? {
|
|
660
|
+
workflowReady,
|
|
661
|
+
coding: {
|
|
662
|
+
workflowId: coding.workflowId,
|
|
663
|
+
state: coding.state,
|
|
664
|
+
scopeVersion: coding.scopeVersion,
|
|
665
|
+
...(coding.protocol
|
|
666
|
+
? {
|
|
667
|
+
protocol: {
|
|
668
|
+
kind: coding.protocol.recipe.kind,
|
|
669
|
+
phase: coding.protocol.schema === 2
|
|
670
|
+
? coding.protocol.phase
|
|
671
|
+
: "rebind-required",
|
|
672
|
+
...(coding.protocol.schema !== 2
|
|
673
|
+
? {
|
|
674
|
+
reason: "Legacy schema 1 evidence is retained read-only; explicitly rebind this unit",
|
|
675
|
+
}
|
|
676
|
+
: coding.protocol.reason
|
|
677
|
+
? { reason: coding.protocol.reason }
|
|
678
|
+
: {}),
|
|
679
|
+
generation: coding.protocol.generation,
|
|
680
|
+
capabilities: PROTOCOL_CAPABILITIES,
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
: {}),
|
|
684
|
+
goalCriterionIds: (coding.definition?.goal.criterionIds ?? []).map((id) => `coding:${coding.workflowId}:${id}`),
|
|
685
|
+
itemCriterionIds: (coding.definition?.items ?? []).flatMap((item) => item.criterionIds.map((id) => `coding:${coding.workflowId}:${id}`)),
|
|
686
|
+
},
|
|
687
|
+
}
|
|
688
|
+
: {}),
|
|
689
|
+
};
|
|
357
690
|
}
|
|
358
691
|
export function saveTaskCompletionReceipt(database, runId) {
|
|
359
692
|
const assessment = assessTaskCompletion(database, runId);
|
|
360
|
-
if (!database
|
|
693
|
+
if (!database
|
|
694
|
+
.prepare("SELECT run_id FROM dsh_completion_runs WHERE run_id = ?")
|
|
695
|
+
.get(runId))
|
|
361
696
|
return assessment;
|
|
362
|
-
database
|
|
697
|
+
database
|
|
698
|
+
.prepare(`INSERT INTO dsh_completion_receipts(run_id,assessment_json,updated_at) VALUES(?,?,?)
|
|
363
699
|
ON CONFLICT(run_id) DO UPDATE SET assessment_json=excluded.assessment_json,updated_at=excluded.updated_at`)
|
|
364
700
|
.run(runId, JSON.stringify(assessment), new Date().toISOString());
|
|
365
701
|
return assessment;
|
|
366
702
|
}
|
|
367
703
|
export function taskCompletionBlocksClose(assessment) {
|
|
368
|
-
return assessment.managedOperationsReady === false ||
|
|
704
|
+
return (assessment.managedOperationsReady === false ||
|
|
705
|
+
(assessment.mode === "enforce" &&
|
|
706
|
+
(assessment.required > 0 || assessment.workflowReady !== undefined) &&
|
|
707
|
+
!assessment.verified));
|
|
369
708
|
}
|
|
370
709
|
/** Include the consumer configuration and runtime alongside selected source paths. */
|
|
371
710
|
export function completionSourceDigest(database, runId, root, paths) {
|
|
372
711
|
const workflow = readCodingWorkflow(database, runId);
|
|
373
712
|
if (!workflow)
|
|
374
|
-
return canonicalContentHash({
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
713
|
+
return canonicalContentHash({
|
|
714
|
+
commandIdentityVersion: EXECUTION_COMMAND_VERSION,
|
|
715
|
+
source: applicationSourceDigest(root, paths),
|
|
716
|
+
});
|
|
717
|
+
const configuration = [
|
|
718
|
+
"package.json",
|
|
719
|
+
"package-lock.json",
|
|
720
|
+
"pnpm-lock.yaml",
|
|
721
|
+
"tsconfig.json",
|
|
722
|
+
].filter((file) => existsSync(path.join(root, file)));
|
|
723
|
+
return canonicalContentHash({
|
|
724
|
+
commandIdentityVersion: EXECUTION_COMMAND_VERSION,
|
|
725
|
+
source: applicationSourceDigest(root, [
|
|
726
|
+
...new Set([...paths, ...configuration]),
|
|
727
|
+
]),
|
|
728
|
+
scopeVersion: workflow.scopeVersion,
|
|
729
|
+
runtime: {
|
|
730
|
+
node: process.version,
|
|
731
|
+
executable: process.execPath,
|
|
732
|
+
pathDigest: canonicalContentHash(process.env.PATH ?? ""),
|
|
733
|
+
platform: process.platform,
|
|
734
|
+
arch: process.arch,
|
|
735
|
+
},
|
|
736
|
+
});
|
|
378
737
|
}
|
|
379
738
|
function artifactMatches(root, method) {
|
|
380
739
|
if (!method.artifactPath || !method.artifactSha256)
|
|
381
740
|
return false;
|
|
382
741
|
try {
|
|
383
742
|
const file = realpathSync(path.join(root, method.artifactPath)), relative = path.relative(realpathSync(root), file);
|
|
384
|
-
if (relative.startsWith(
|
|
743
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
385
744
|
return false;
|
|
386
|
-
return createHash(
|
|
745
|
+
return (createHash("sha256").update(readFileSync(file)).digest("hex") ===
|
|
746
|
+
method.artifactSha256);
|
|
387
747
|
}
|
|
388
748
|
catch {
|
|
389
749
|
return false;
|