@abelxiaoxing/cadence 1.2.4 → 1.4.0
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/README.md +155 -10
- package/package.json +5 -3
- package/prompts/abel-design.md +71 -17
- package/prompts/abel-diagnose.md +10 -1
- package/prompts/abel-implement.md +118 -20
- package/prompts/abel-init.md +9 -2
- package/src/apply-transaction.ts +16 -14
- package/src/artifact-store.ts +17 -0
- package/src/candidate-patch.ts +2 -1
- package/src/canonical.ts +16 -0
- package/src/change-contract.ts +268 -0
- package/src/child-session.ts +4 -3
- package/src/contracts.ts +141 -33
- package/src/control-contracts.ts +51 -1
- package/src/delivery-compiler.ts +148 -31
- package/src/design-control.ts +83 -15
- package/src/design-journal.ts +216 -72
- package/src/durable-workflow.ts +4385 -0
- package/src/implement-graph.ts +11 -16
- package/src/index.ts +399 -1079
- package/src/isolation-backend.ts +84 -23
- package/src/openspec-cli.ts +494 -0
- package/src/package-delivery.ts +486 -0
- package/src/package-verification.ts +509 -0
- package/src/route-policy.ts +1 -11
- package/src/run-state.ts +4 -16
- package/src/run-store.ts +5 -144
- package/src/scoped-grep-worker.mjs +23 -0
- package/src/scoped-tools.ts +312 -116
- package/src/sqlite-schema.ts +171 -0
- package/src/storage-schema.ts +352 -0
- package/src/subagent-activity.ts +14 -1
- package/src/submit-tool.ts +20 -2
- package/src/task-ledger.ts +14 -55
- package/src/verification-capability.ts +298 -102
- package/src/verification-environment.ts +213 -0
- package/src/verification-identity.ts +77 -0
- package/src/worker-broker.ts +97 -34
- package/src/workflow-engine.ts +28 -8285
- package/src/workflow-policy.ts +1427 -0
- package/src/workflow-state-machine.ts +4478 -0
- package/src/workspace-io-entry.mjs +58 -0
- package/src/workspace-io-worker.mjs +984 -0
- package/src/workspace-io.ts +84 -0
- package/src/workspace-store.ts +125 -19
- package/skills/abel-workflow/SKILL.md +0 -205
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
type AtomicVerificationContract,
|
|
6
|
+
type StructuredVerificationContract,
|
|
7
|
+
type VerificationObservation,
|
|
8
|
+
verificationSteps,
|
|
9
|
+
} from "./contracts.ts";
|
|
10
|
+
import { canonicalJson } from "./implement-graph.ts";
|
|
11
|
+
import { BubblewrapIsolationBackend } from "./isolation-backend.ts";
|
|
12
|
+
import { observeSafePath } from "./safe-path.ts";
|
|
13
|
+
import {
|
|
14
|
+
bindCurrentVerificationCapability,
|
|
15
|
+
isVerificationCapabilityCurrent,
|
|
16
|
+
type VerificationRunnerBinding,
|
|
17
|
+
} from "./verification-capability.ts";
|
|
18
|
+
import { prepareVerificationEnvironment } from "./verification-environment.ts";
|
|
19
|
+
|
|
20
|
+
function sha256(value: Uint8Array | string): string {
|
|
21
|
+
return createHash("sha256").update(value).digest("hex");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function packageScriptInvocation(
|
|
25
|
+
packageManager: string,
|
|
26
|
+
script: string,
|
|
27
|
+
args: string[],
|
|
28
|
+
): string[] {
|
|
29
|
+
return packageManager === "npm"
|
|
30
|
+
? ["run", script, "--", ...args]
|
|
31
|
+
: ["run", script, ...args];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function bindingFor(
|
|
35
|
+
bindings: readonly VerificationRunnerBinding[],
|
|
36
|
+
command: string,
|
|
37
|
+
): VerificationRunnerBinding | undefined {
|
|
38
|
+
return bindings.find((binding) => binding.command === command);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function verificationInvocation(
|
|
42
|
+
step: AtomicVerificationContract,
|
|
43
|
+
bindings: readonly VerificationRunnerBinding[],
|
|
44
|
+
reportFile: string,
|
|
45
|
+
): { executable: string; args: string[] } | undefined {
|
|
46
|
+
const reporter =
|
|
47
|
+
step.kind === "vitest"
|
|
48
|
+
? ["--reporter=json", `--outputFile.json=${reportFile}`]
|
|
49
|
+
: [];
|
|
50
|
+
if (step.kind === "package-script") {
|
|
51
|
+
const binding = bindingFor(bindings, step.packageManager);
|
|
52
|
+
return binding
|
|
53
|
+
? {
|
|
54
|
+
executable: binding.executablePath,
|
|
55
|
+
args: [
|
|
56
|
+
...(binding.fixedArgs ?? []),
|
|
57
|
+
...packageScriptInvocation(
|
|
58
|
+
step.packageManager,
|
|
59
|
+
step.script,
|
|
60
|
+
step.args,
|
|
61
|
+
),
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
: undefined;
|
|
65
|
+
}
|
|
66
|
+
const runner = step.runner;
|
|
67
|
+
if (runner.kind === "package-script") {
|
|
68
|
+
const binding = bindingFor(bindings, runner.packageManager);
|
|
69
|
+
const args =
|
|
70
|
+
step.kind === "vitest"
|
|
71
|
+
? [...step.testFiles, ...step.args, ...reporter]
|
|
72
|
+
: step.args;
|
|
73
|
+
return binding
|
|
74
|
+
? {
|
|
75
|
+
executable: binding.executablePath,
|
|
76
|
+
args: [
|
|
77
|
+
...(binding.fixedArgs ?? []),
|
|
78
|
+
...packageScriptInvocation(
|
|
79
|
+
runner.packageManager,
|
|
80
|
+
runner.script,
|
|
81
|
+
args,
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
: undefined;
|
|
86
|
+
}
|
|
87
|
+
if (runner.kind === "local-binary") {
|
|
88
|
+
const binding = bindingFor(bindings, runner.executable);
|
|
89
|
+
return {
|
|
90
|
+
executable:
|
|
91
|
+
binding?.executablePath ??
|
|
92
|
+
`/workspace/node_modules/.bin/${runner.executable}`,
|
|
93
|
+
args: [
|
|
94
|
+
...(binding?.fixedArgs ?? []),
|
|
95
|
+
...(step.kind === "vitest"
|
|
96
|
+
? [...step.testFiles, ...step.args, ...reporter]
|
|
97
|
+
: step.args),
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const command = runner.kind === "node" ? "node" : "npx";
|
|
102
|
+
const binding = bindingFor(bindings, command);
|
|
103
|
+
if (!binding) return undefined;
|
|
104
|
+
return {
|
|
105
|
+
executable: binding.executablePath,
|
|
106
|
+
args:
|
|
107
|
+
runner.kind === "node"
|
|
108
|
+
? [...(binding.fixedArgs ?? []), runner.script, ...step.args]
|
|
109
|
+
: [
|
|
110
|
+
...(binding.fixedArgs ?? []),
|
|
111
|
+
"--no-install",
|
|
112
|
+
runner.executable,
|
|
113
|
+
...(step.kind === "vitest" ? step.testFiles : []),
|
|
114
|
+
...step.args,
|
|
115
|
+
...reporter,
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function vitestReport(json: string):
|
|
121
|
+
| {
|
|
122
|
+
total: number;
|
|
123
|
+
failed: number;
|
|
124
|
+
success: boolean;
|
|
125
|
+
failures: string[];
|
|
126
|
+
failedText: string[];
|
|
127
|
+
}
|
|
128
|
+
| undefined {
|
|
129
|
+
let value: unknown;
|
|
130
|
+
try {
|
|
131
|
+
value = JSON.parse(json);
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const report = value as Record<string, unknown>;
|
|
139
|
+
if (
|
|
140
|
+
!Number.isSafeInteger(report.numTotalTests) ||
|
|
141
|
+
!Number.isSafeInteger(report.numFailedTests) ||
|
|
142
|
+
typeof report.success !== "boolean" ||
|
|
143
|
+
(report.numTotalTests as number) < 0 ||
|
|
144
|
+
(report.numFailedTests as number) < 0 ||
|
|
145
|
+
(report.numFailedTests as number) > (report.numTotalTests as number) ||
|
|
146
|
+
!Array.isArray(report.testResults) ||
|
|
147
|
+
(report.numRuntimeErrorTestSuites !== undefined &&
|
|
148
|
+
report.numRuntimeErrorTestSuites !== 0) ||
|
|
149
|
+
(Array.isArray(report.unhandledErrors) && report.unhandledErrors.length > 0)
|
|
150
|
+
) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
let assertionCount = 0;
|
|
154
|
+
for (const suite of report.testResults) {
|
|
155
|
+
if (
|
|
156
|
+
!suite ||
|
|
157
|
+
typeof suite !== "object" ||
|
|
158
|
+
!Array.isArray(suite.assertionResults)
|
|
159
|
+
)
|
|
160
|
+
return undefined;
|
|
161
|
+
for (const assertion of suite.assertionResults) {
|
|
162
|
+
if (
|
|
163
|
+
!assertion ||
|
|
164
|
+
typeof assertion !== "object" ||
|
|
165
|
+
![
|
|
166
|
+
"passed",
|
|
167
|
+
"failed",
|
|
168
|
+
"pending",
|
|
169
|
+
"skipped",
|
|
170
|
+
"todo",
|
|
171
|
+
"disabled",
|
|
172
|
+
].includes(assertion.status)
|
|
173
|
+
)
|
|
174
|
+
return undefined;
|
|
175
|
+
assertionCount++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (assertionCount !== report.numTotalTests) return undefined;
|
|
179
|
+
const failedAssertions = (
|
|
180
|
+
Array.isArray(report.testResults) ? report.testResults : []
|
|
181
|
+
).flatMap((suite): Array<{ identity: string; text: string[] }> => {
|
|
182
|
+
if (!suite || typeof suite !== "object" || Array.isArray(suite)) return [];
|
|
183
|
+
const record = suite as Record<string, unknown>;
|
|
184
|
+
const suiteName =
|
|
185
|
+
typeof record.name === "string"
|
|
186
|
+
? record.name.replace(/^\/workspace\//u, "")
|
|
187
|
+
: "unknown-suite";
|
|
188
|
+
if (!Array.isArray(record.assertionResults)) return [];
|
|
189
|
+
return record.assertionResults.flatMap(
|
|
190
|
+
(assertion): Array<{ identity: string; text: string[] }> => {
|
|
191
|
+
if (
|
|
192
|
+
!assertion ||
|
|
193
|
+
typeof assertion !== "object" ||
|
|
194
|
+
Array.isArray(assertion)
|
|
195
|
+
) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const entry = assertion as Record<string, unknown>;
|
|
199
|
+
if (entry.status !== "failed") return [];
|
|
200
|
+
const title =
|
|
201
|
+
typeof entry.fullName === "string"
|
|
202
|
+
? entry.fullName
|
|
203
|
+
: typeof entry.title === "string"
|
|
204
|
+
? entry.title
|
|
205
|
+
: "unknown-assertion";
|
|
206
|
+
const diagnostics = Array.isArray(entry.failureMessages)
|
|
207
|
+
? entry.failureMessages.filter(
|
|
208
|
+
(message): message is string => typeof message === "string",
|
|
209
|
+
)
|
|
210
|
+
: [];
|
|
211
|
+
return [
|
|
212
|
+
{
|
|
213
|
+
identity: `${suiteName}\0${title}`,
|
|
214
|
+
text: [title, ...diagnostics],
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
if (
|
|
221
|
+
failedAssertions.length !== report.numFailedTests ||
|
|
222
|
+
report.success !== (report.numFailedTests === 0)
|
|
223
|
+
)
|
|
224
|
+
return undefined;
|
|
225
|
+
return {
|
|
226
|
+
total: report.numTotalTests as number,
|
|
227
|
+
failed: report.numFailedTests as number,
|
|
228
|
+
success: report.success,
|
|
229
|
+
failures: failedAssertions.map((failure) => failure.identity),
|
|
230
|
+
failedText: failedAssertions.flatMap((failure) => failure.text),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizedFailureIdentities(input: {
|
|
235
|
+
step: AtomicVerificationContract;
|
|
236
|
+
report?: ReturnType<typeof vitestReport>;
|
|
237
|
+
output: string;
|
|
238
|
+
exitCode: number;
|
|
239
|
+
}): string[] {
|
|
240
|
+
const vitestFailures = input.report?.failures ?? [];
|
|
241
|
+
if (vitestFailures.length > 0) {
|
|
242
|
+
return [...new Set(vitestFailures)]
|
|
243
|
+
.sort()
|
|
244
|
+
.map((failure) => sha256(`vitest-failure\0${failure}`));
|
|
245
|
+
}
|
|
246
|
+
const normalizedOutput = input.output
|
|
247
|
+
.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "gu"), "")
|
|
248
|
+
.replace(/\r\n?/gu, "\n")
|
|
249
|
+
.replace(/\b\d+(?:\.\d+)?(?:ms|s)\b/gu, "<duration>")
|
|
250
|
+
.slice(0, 64 * 1024);
|
|
251
|
+
return [
|
|
252
|
+
sha256(
|
|
253
|
+
canonicalJson({
|
|
254
|
+
obligation:
|
|
255
|
+
input.step.kind === "package-script"
|
|
256
|
+
? {
|
|
257
|
+
kind: "package-script",
|
|
258
|
+
packageManager: input.step.packageManager,
|
|
259
|
+
script: input.step.script,
|
|
260
|
+
command: input.step.command,
|
|
261
|
+
args: input.step.args,
|
|
262
|
+
}
|
|
263
|
+
: {
|
|
264
|
+
kind: input.step.kind,
|
|
265
|
+
runner: input.step.runner,
|
|
266
|
+
args: input.step.args,
|
|
267
|
+
},
|
|
268
|
+
exitCode: input.exitCode,
|
|
269
|
+
output: normalizedOutput,
|
|
270
|
+
}),
|
|
271
|
+
),
|
|
272
|
+
];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const VERIFICATION_REPORT_LIMIT = 64 * 1024 * 1024;
|
|
276
|
+
|
|
277
|
+
function readReport(root: string, relative: string, maximum: number): string {
|
|
278
|
+
if (observeSafePath(root, relative).kind !== "file")
|
|
279
|
+
throw new Error("verification-report-unavailable");
|
|
280
|
+
const descriptor = openSync(
|
|
281
|
+
path.join(root, relative),
|
|
282
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
283
|
+
);
|
|
284
|
+
try {
|
|
285
|
+
const stat = fstatSync(descriptor);
|
|
286
|
+
if (!stat.isFile()) throw new Error("verification-report-unavailable");
|
|
287
|
+
if (stat.size > maximum) throw new Error("verification-report-too-large");
|
|
288
|
+
const bytes = Buffer.alloc(stat.size + 1);
|
|
289
|
+
let offset = 0;
|
|
290
|
+
while (offset < bytes.length) {
|
|
291
|
+
const count = readSync(
|
|
292
|
+
descriptor,
|
|
293
|
+
bytes,
|
|
294
|
+
offset,
|
|
295
|
+
bytes.length - offset,
|
|
296
|
+
null,
|
|
297
|
+
);
|
|
298
|
+
if (count === 0) break;
|
|
299
|
+
offset += count;
|
|
300
|
+
}
|
|
301
|
+
if (offset !== stat.size) throw new Error("verification-report-invalid");
|
|
302
|
+
return bytes.subarray(0, offset).toString("utf8");
|
|
303
|
+
} finally {
|
|
304
|
+
closeSync(descriptor);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export async function executePackageVerification(input: {
|
|
309
|
+
root: string;
|
|
310
|
+
dependencyOwner: string;
|
|
311
|
+
verification: StructuredVerificationContract;
|
|
312
|
+
signal: AbortSignal;
|
|
313
|
+
maxReportBytes?: number;
|
|
314
|
+
executionWritePaths?: readonly string[];
|
|
315
|
+
}): Promise<VerificationObservation> {
|
|
316
|
+
const unavailable = (
|
|
317
|
+
code: string,
|
|
318
|
+
category: "environment" | "adapter" | "resource" = "adapter",
|
|
319
|
+
): VerificationObservation => ({
|
|
320
|
+
kind: "unavailable",
|
|
321
|
+
category,
|
|
322
|
+
code,
|
|
323
|
+
verificationId: input.verification.id,
|
|
324
|
+
});
|
|
325
|
+
if (input.signal.aborted) return { kind: "cancelled" };
|
|
326
|
+
const maxReportBytes = input.maxReportBytes ?? VERIFICATION_REPORT_LIMIT;
|
|
327
|
+
if (!Number.isSafeInteger(maxReportBytes) || maxReportBytes < 1)
|
|
328
|
+
return unavailable("verification-report-limit-invalid", "resource");
|
|
329
|
+
const capability = bindCurrentVerificationCapability(
|
|
330
|
+
input.root,
|
|
331
|
+
input.verification,
|
|
332
|
+
{
|
|
333
|
+
dependencyOwner: input.dependencyOwner,
|
|
334
|
+
executionWritePaths: input.executionWritePaths,
|
|
335
|
+
},
|
|
336
|
+
);
|
|
337
|
+
if (!capability.ok) return unavailable(capability.diagnostic.code);
|
|
338
|
+
let environment: ReturnType<typeof prepareVerificationEnvironment>;
|
|
339
|
+
try {
|
|
340
|
+
environment = prepareVerificationEnvironment(
|
|
341
|
+
input.root,
|
|
342
|
+
input.dependencyOwner,
|
|
343
|
+
capability.value.runnerBindings,
|
|
344
|
+
);
|
|
345
|
+
} catch {
|
|
346
|
+
return unavailable("verification-environment-unavailable", "environment");
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const isolation = new BubblewrapIsolationBackend();
|
|
350
|
+
let final: VerificationObservation | undefined;
|
|
351
|
+
let sequence = 0;
|
|
352
|
+
for (const step of verificationSteps(input.verification)) {
|
|
353
|
+
const relative = `reports/${sequence++}.json`;
|
|
354
|
+
const invocation = verificationInvocation(
|
|
355
|
+
step,
|
|
356
|
+
environment.bindings,
|
|
357
|
+
`/cadence/${relative}`,
|
|
358
|
+
);
|
|
359
|
+
if (!invocation) return unavailable("runner-missing");
|
|
360
|
+
const executed = await isolation.run({
|
|
361
|
+
root: input.root,
|
|
362
|
+
...invocation,
|
|
363
|
+
mounts: environment.mounts,
|
|
364
|
+
environment: environment.environment,
|
|
365
|
+
...(step.kind !== "vitest" && step.classification === "expected-red"
|
|
366
|
+
? { outputWitness: step.expectedFailure }
|
|
367
|
+
: {}),
|
|
368
|
+
signal: input.signal,
|
|
369
|
+
});
|
|
370
|
+
if (!executed.ok) {
|
|
371
|
+
if (executed.state === "cancelled") return { kind: "cancelled" };
|
|
372
|
+
return unavailable(
|
|
373
|
+
executed.code,
|
|
374
|
+
executed.code === "isolation-execution-timeout"
|
|
375
|
+
? "resource"
|
|
376
|
+
: "environment",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
let report: ReturnType<typeof vitestReport>;
|
|
380
|
+
if (step.kind === "vitest") {
|
|
381
|
+
try {
|
|
382
|
+
report = vitestReport(
|
|
383
|
+
readReport(environment.privateRoot, relative, maxReportBytes),
|
|
384
|
+
);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
const large =
|
|
387
|
+
error instanceof Error &&
|
|
388
|
+
error.message === "verification-report-too-large";
|
|
389
|
+
return unavailable(
|
|
390
|
+
large
|
|
391
|
+
? "verification-report-too-large"
|
|
392
|
+
: "verification-report-unavailable",
|
|
393
|
+
large ? "resource" : "adapter",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (!report || (executed.exitCode === 0) !== report.success)
|
|
397
|
+
return unavailable("verification-report-invalid");
|
|
398
|
+
}
|
|
399
|
+
// A shell could not start its program; this is not evidence about the product.
|
|
400
|
+
if ([126, 127].includes(executed.exitCode))
|
|
401
|
+
return unavailable("verification-runner-launch-failed", "environment");
|
|
402
|
+
if (!isVerificationCapabilityCurrent(input.root, capability.value))
|
|
403
|
+
return unavailable("verification-input-unavailable");
|
|
404
|
+
const output = `${executed.stdout}\n${executed.stderr}`;
|
|
405
|
+
const identity =
|
|
406
|
+
step.classification !== "expected-red" ||
|
|
407
|
+
(step.kind === "vitest"
|
|
408
|
+
? report?.failedText.some((text) =>
|
|
409
|
+
text.includes(step.expectedFailure ?? ""),
|
|
410
|
+
) === true
|
|
411
|
+
: executed.outputWitnessMatched === true);
|
|
412
|
+
const accepted =
|
|
413
|
+
step.classification === "expected-red"
|
|
414
|
+
? executed.exitCode !== 0 &&
|
|
415
|
+
identity &&
|
|
416
|
+
(step.kind !== "vitest" ||
|
|
417
|
+
(report !== undefined &&
|
|
418
|
+
report.failed > 0 &&
|
|
419
|
+
report.total >= step.minTests))
|
|
420
|
+
: executed.exitCode === 0 &&
|
|
421
|
+
(step.kind !== "vitest" ||
|
|
422
|
+
(report?.success === true && report.total >= step.minTests));
|
|
423
|
+
const evidence = {
|
|
424
|
+
id: step.id,
|
|
425
|
+
exitCode: executed.exitCode,
|
|
426
|
+
classification: step.classification,
|
|
427
|
+
policy: "report-file-v3" as const,
|
|
428
|
+
...(report ? { tests: report.total } : {}),
|
|
429
|
+
failureIdentities: accepted
|
|
430
|
+
? []
|
|
431
|
+
: normalizedFailureIdentities({
|
|
432
|
+
step,
|
|
433
|
+
report,
|
|
434
|
+
output,
|
|
435
|
+
exitCode: executed.exitCode,
|
|
436
|
+
}),
|
|
437
|
+
};
|
|
438
|
+
if (!accepted)
|
|
439
|
+
return {
|
|
440
|
+
kind: "rejected",
|
|
441
|
+
code:
|
|
442
|
+
step.classification === "expected-red" && executed.exitCode === 0
|
|
443
|
+
? "red-not-witnessed"
|
|
444
|
+
: "verification-rejected",
|
|
445
|
+
evidence,
|
|
446
|
+
};
|
|
447
|
+
final = { kind: "accepted", evidence };
|
|
448
|
+
}
|
|
449
|
+
return final ?? unavailable("verification-contract-unsupported");
|
|
450
|
+
} finally {
|
|
451
|
+
environment.cleanup();
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Compatibility at the execution service boundary, without guessing from error strings. */
|
|
456
|
+
export function phaseVerificationResult(result: VerificationObservation) {
|
|
457
|
+
if (result.kind === "cancelled")
|
|
458
|
+
throw new DOMException("cancelled", "AbortError");
|
|
459
|
+
if (result.kind === "accepted")
|
|
460
|
+
return {
|
|
461
|
+
ok: true as const,
|
|
462
|
+
exitCode: result.evidence.exitCode,
|
|
463
|
+
classification: result.evidence.classification,
|
|
464
|
+
diagnostic: { kind: "assertion" as const, id: result.evidence.id },
|
|
465
|
+
};
|
|
466
|
+
if (result.kind === "rejected")
|
|
467
|
+
return {
|
|
468
|
+
ok: false as const,
|
|
469
|
+
kind: "retryable" as const,
|
|
470
|
+
code: result.code,
|
|
471
|
+
failureIdentities: result.evidence.failureIdentities,
|
|
472
|
+
};
|
|
473
|
+
return {
|
|
474
|
+
ok: false as const,
|
|
475
|
+
kind: "paused" as const,
|
|
476
|
+
code: result.code,
|
|
477
|
+
category: result.category,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function changeVerificationResult(result: VerificationObservation) {
|
|
482
|
+
if (result.kind === "accepted")
|
|
483
|
+
return {
|
|
484
|
+
ok: true as const,
|
|
485
|
+
exitCode: 0 as const,
|
|
486
|
+
classification: result.evidence.classification,
|
|
487
|
+
};
|
|
488
|
+
if (result.kind === "cancelled")
|
|
489
|
+
return {
|
|
490
|
+
ok: false as const,
|
|
491
|
+
kind: "cancelled" as const,
|
|
492
|
+
code: "cancelled",
|
|
493
|
+
};
|
|
494
|
+
if (result.kind === "rejected")
|
|
495
|
+
return {
|
|
496
|
+
ok: false as const,
|
|
497
|
+
kind: "verification" as const,
|
|
498
|
+
code: result.code,
|
|
499
|
+
failureIdentities: result.evidence.failureIdentities,
|
|
500
|
+
};
|
|
501
|
+
return {
|
|
502
|
+
ok: false as const,
|
|
503
|
+
kind:
|
|
504
|
+
result.category === "adapter"
|
|
505
|
+
? ("verification-adapter" as const)
|
|
506
|
+
: ("environment" as const),
|
|
507
|
+
code: result.code,
|
|
508
|
+
};
|
|
509
|
+
}
|
package/src/route-policy.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstatSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { canonicalJson } from "./canonical.ts";
|
|
5
6
|
import { ROLES } from "./contracts.ts";
|
|
6
7
|
|
|
7
8
|
export const ROUTE_DIALECTS = [
|
|
@@ -166,17 +167,6 @@ function httpUrl(value: unknown): value is string {
|
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
function canonicalJson(value: unknown): string {
|
|
170
|
-
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
171
|
-
if (isRecord(value)) {
|
|
172
|
-
return `{${Object.entries(value)
|
|
173
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
174
|
-
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
|
175
|
-
.join(",")}}`;
|
|
176
|
-
}
|
|
177
|
-
return JSON.stringify(value) ?? "null";
|
|
178
|
-
}
|
|
179
|
-
|
|
180
170
|
function routeFingerprint(route: Record<string, unknown>): string {
|
|
181
171
|
return createHash("sha256").update(canonicalJson(route)).digest("hex");
|
|
182
172
|
}
|
package/src/run-state.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalJson, compareCanonicalStrings } from "./canonical.ts";
|
|
2
3
|
|
|
3
4
|
export const RUN_STAGES = ["abel-design", "abel-implement"] as const;
|
|
4
5
|
export type RunStage = (typeof RUN_STAGES)[number];
|
|
@@ -161,21 +162,6 @@ const LEGAL_TRANSITIONS: Readonly<Record<RunState, readonly RunState[]>> = {
|
|
|
161
162
|
rejected: [],
|
|
162
163
|
};
|
|
163
164
|
|
|
164
|
-
export function canonicalJson(value: unknown): string {
|
|
165
|
-
if (Array.isArray(value)) {
|
|
166
|
-
return `[${value.map(canonicalJson).join(",")}]`;
|
|
167
|
-
}
|
|
168
|
-
if (value && typeof value === "object") {
|
|
169
|
-
const entries = Object.entries(value as Record<string, unknown>)
|
|
170
|
-
.filter(([, entry]) => entry !== undefined)
|
|
171
|
-
.sort(([left], [right]) => left.localeCompare(right));
|
|
172
|
-
return `{${entries
|
|
173
|
-
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
|
|
174
|
-
.join(",")}}`;
|
|
175
|
-
}
|
|
176
|
-
return JSON.stringify(value) ?? "null";
|
|
177
|
-
}
|
|
178
|
-
|
|
179
165
|
export function hashRunEvent(event: Omit<RunEvent, "hash">): string {
|
|
180
166
|
return createHash("sha256").update(canonicalJson(event)).digest("hex");
|
|
181
167
|
}
|
|
@@ -329,7 +315,7 @@ function applyVerifiedEvent(
|
|
|
329
315
|
: [...projection.deliveryBindings, binding].sort(
|
|
330
316
|
(left, right) =>
|
|
331
317
|
left.revision - right.revision ||
|
|
332
|
-
left.gate
|
|
318
|
+
compareCanonicalStrings(left.gate, right.gate),
|
|
333
319
|
);
|
|
334
320
|
return {
|
|
335
321
|
...projection,
|
|
@@ -430,3 +416,5 @@ export function legalControlCommands(state: RunState): string[] {
|
|
|
430
416
|
if (state === "recovering") return ["status", "resume", "discard"];
|
|
431
417
|
return ["status", "cancel", "discard"];
|
|
432
418
|
}
|
|
419
|
+
|
|
420
|
+
export { canonicalJson } from "./canonical.ts";
|