@abelxiaoxing/cadence 1.0.9 → 1.0.11
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 +49 -1
- package/package.json +1 -1
- package/prompts/abel-design.md +14 -2
- package/prompts/abel-implement.md +10 -2
- package/skills/abel-workflow/SKILL.md +36 -0
- package/src/candidate-preflight.ts +237 -90
- package/src/child-session.ts +1 -0
- package/src/contracts.ts +482 -41
- package/src/patch.ts +13 -1
- package/src/result-store.ts +6 -9
- package/src/runtime.ts +66 -10
- package/src/verification-capability.ts +481 -0
package/src/runtime.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type AgentsCheckpointRequest,
|
|
13
13
|
type CandidateFailure,
|
|
14
14
|
type ChildFailure,
|
|
15
|
+
cloneVerificationContract,
|
|
15
16
|
type DiffResult,
|
|
16
17
|
type ImplementApplyOperation,
|
|
17
18
|
type ImplementationPhase,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
validateImplementDiscardOperation,
|
|
30
31
|
validatePhaseAttemptAgainstBoundary,
|
|
31
32
|
validateRequestEnvelope,
|
|
33
|
+
verificationInputPaths,
|
|
32
34
|
} from "./contracts.ts";
|
|
33
35
|
import { drainStage } from "./drain.ts";
|
|
34
36
|
import {
|
|
@@ -56,6 +58,7 @@ import {
|
|
|
56
58
|
resolveSubagentEndpoint,
|
|
57
59
|
type SubagentEndpoint,
|
|
58
60
|
} from "./subagent-endpoint.ts";
|
|
61
|
+
import { assessVerificationReadiness } from "./verification-capability.ts";
|
|
59
62
|
import {
|
|
60
63
|
type TaskRecord,
|
|
61
64
|
taskConflictOf,
|
|
@@ -119,10 +122,7 @@ function cloneLegacyEnvelope(request: RequestEnvelope): RequestEnvelope {
|
|
|
119
122
|
...(request.verification === undefined
|
|
120
123
|
? {}
|
|
121
124
|
: {
|
|
122
|
-
verification:
|
|
123
|
-
...request.verification,
|
|
124
|
-
argv: [...request.verification.argv],
|
|
125
|
-
},
|
|
125
|
+
verification: cloneVerificationContract(request.verification),
|
|
126
126
|
}),
|
|
127
127
|
...(request.snapshot === undefined
|
|
128
128
|
? {}
|
|
@@ -163,10 +163,7 @@ function deriveImplementEnvelope(
|
|
|
163
163
|
agentsManagedOnly: true,
|
|
164
164
|
approvedDependencies: [...boundary.approvedDependencies],
|
|
165
165
|
impactClosure: structuredClone(boundary.impactClosure),
|
|
166
|
-
verification:
|
|
167
|
-
...phase.verification,
|
|
168
|
-
argv: [...phase.verification.argv],
|
|
169
|
-
},
|
|
166
|
+
verification: cloneVerificationContract(phase.verification),
|
|
170
167
|
snapshot: structuredClone(attempt.snapshot),
|
|
171
168
|
};
|
|
172
169
|
}
|
|
@@ -318,7 +315,7 @@ function preparePreflight(
|
|
|
318
315
|
...envelope.declared.write,
|
|
319
316
|
"package.json",
|
|
320
317
|
lockPath,
|
|
321
|
-
...envelope.verification
|
|
318
|
+
...verificationInputPaths(envelope.verification),
|
|
322
319
|
]);
|
|
323
320
|
const entries = new Map<string, BaselineEntry>();
|
|
324
321
|
const observed: Bound = {};
|
|
@@ -446,6 +443,7 @@ function failureClassOf(failure: ChildFailure): FailureClass | undefined {
|
|
|
446
443
|
return "transport";
|
|
447
444
|
case "stale":
|
|
448
445
|
case "approval-boundary":
|
|
446
|
+
case "verification-adapter":
|
|
449
447
|
case "cancelled":
|
|
450
448
|
case "result-limit":
|
|
451
449
|
return undefined;
|
|
@@ -465,6 +463,7 @@ function childFailureOf(result: InternalFailureResult): ChildFailure {
|
|
|
465
463
|
case "stale":
|
|
466
464
|
case "environment":
|
|
467
465
|
case "approval-boundary":
|
|
466
|
+
case "verification-adapter":
|
|
468
467
|
case "cancelled":
|
|
469
468
|
case "result-limit":
|
|
470
469
|
case "transport":
|
|
@@ -495,6 +494,7 @@ function terminalTaskFailure(failure: ChildFailure): TaskFailure | undefined {
|
|
|
495
494
|
switch (failure.kind) {
|
|
496
495
|
case "approval-boundary":
|
|
497
496
|
case "environment":
|
|
497
|
+
case "verification-adapter":
|
|
498
498
|
case "result-limit":
|
|
499
499
|
return failure;
|
|
500
500
|
case "artifact":
|
|
@@ -515,6 +515,7 @@ function attemptFailureCause(
|
|
|
515
515
|
return failure.kind;
|
|
516
516
|
case "approval-boundary":
|
|
517
517
|
case "environment":
|
|
518
|
+
case "verification-adapter":
|
|
518
519
|
case "result-limit":
|
|
519
520
|
case "cancelled":
|
|
520
521
|
throw new Error("non-retryable failure reached the launch budget");
|
|
@@ -992,6 +993,15 @@ export class Runtime {
|
|
|
992
993
|
};
|
|
993
994
|
return blockedOutcome(identity, taskFailure);
|
|
994
995
|
}
|
|
996
|
+
case "verification-adapter": {
|
|
997
|
+
const taskFailure: TaskFailure = failure;
|
|
998
|
+
worker.state = {
|
|
999
|
+
kind: "blocked",
|
|
1000
|
+
phase,
|
|
1001
|
+
failure: structuredClone(taskFailure),
|
|
1002
|
+
};
|
|
1003
|
+
return blockedOutcome(identity, taskFailure);
|
|
1004
|
+
}
|
|
995
1005
|
case "cancelled":
|
|
996
1006
|
return cancelledOutcome(identity);
|
|
997
1007
|
case "result-limit": {
|
|
@@ -1260,6 +1270,15 @@ export class Runtime {
|
|
|
1260
1270
|
};
|
|
1261
1271
|
return blockedOutcome(outcomeIdentity, failure);
|
|
1262
1272
|
}
|
|
1273
|
+
case "verification-adapter": {
|
|
1274
|
+
this.results.discard(resultId);
|
|
1275
|
+
worker.state = {
|
|
1276
|
+
kind: "blocked",
|
|
1277
|
+
phase: identity.phase,
|
|
1278
|
+
failure: structuredClone(failure),
|
|
1279
|
+
};
|
|
1280
|
+
return blockedOutcome(outcomeIdentity, failure);
|
|
1281
|
+
}
|
|
1263
1282
|
case "stale": {
|
|
1264
1283
|
this.results.discard(resultId);
|
|
1265
1284
|
if (identity.launchIndex === 0) {
|
|
@@ -1562,6 +1581,42 @@ export class Runtime {
|
|
|
1562
1581
|
attempt.changeId,
|
|
1563
1582
|
attempt.taskId,
|
|
1564
1583
|
);
|
|
1584
|
+
if (runRequest.kind === "open-task") {
|
|
1585
|
+
const phases = runRequest.boundary.phases;
|
|
1586
|
+
const readiness = assessVerificationReadiness(
|
|
1587
|
+
workspaceRoot,
|
|
1588
|
+
[
|
|
1589
|
+
phases.red.verification,
|
|
1590
|
+
phases.green.verification,
|
|
1591
|
+
...(phases.refactor ? [phases.refactor.verification] : []),
|
|
1592
|
+
],
|
|
1593
|
+
{
|
|
1594
|
+
allowedMissingInputs: [
|
|
1595
|
+
...new Set([
|
|
1596
|
+
...phases.red.write,
|
|
1597
|
+
...phases.green.write,
|
|
1598
|
+
...(phases.refactor?.write ?? []),
|
|
1599
|
+
]),
|
|
1600
|
+
],
|
|
1601
|
+
},
|
|
1602
|
+
);
|
|
1603
|
+
const diagnostic = readiness.diagnostics[0];
|
|
1604
|
+
if (diagnostic) {
|
|
1605
|
+
if (diagnostic.kind === "design-readiness") {
|
|
1606
|
+
throw new Error(
|
|
1607
|
+
`Implement protocol error: ${diagnostic.code}: ${diagnostic.message}`,
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
return {
|
|
1611
|
+
ok: false,
|
|
1612
|
+
error: `${diagnostic.kind}/${diagnostic.code}: ${diagnostic.message}`,
|
|
1613
|
+
failure: {
|
|
1614
|
+
kind: diagnostic.kind,
|
|
1615
|
+
code: diagnostic.code,
|
|
1616
|
+
},
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1565
1620
|
if (
|
|
1566
1621
|
runRequest.kind === "open-task" &&
|
|
1567
1622
|
this.registry
|
|
@@ -1848,7 +1903,8 @@ export class Runtime {
|
|
|
1848
1903
|
envelope.stage === "abel-design" &&
|
|
1849
1904
|
!first.ok &&
|
|
1850
1905
|
first.failure?.kind === "artifact" &&
|
|
1851
|
-
first.failure.code === "invalid-structural-result"
|
|
1906
|
+
(first.failure.code === "invalid-structural-result" ||
|
|
1907
|
+
first.failure.code === "child-no-structural-submit");
|
|
1852
1908
|
if (!retryInvalidDesignResult) return this.withUsage(context, first);
|
|
1853
1909
|
|
|
1854
1910
|
const second = await this.dispatchChild(agent, envelope, ctx, signal);
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type AtomicVerificationContract,
|
|
5
|
+
isValidRelativePath,
|
|
6
|
+
type VerificationAdapterCode,
|
|
7
|
+
validateVerificationContract,
|
|
8
|
+
verificationInputPaths,
|
|
9
|
+
verificationSteps,
|
|
10
|
+
} from "./contracts.ts";
|
|
11
|
+
|
|
12
|
+
export type VerificationCapabilityDiagnostic =
|
|
13
|
+
| {
|
|
14
|
+
kind: "design-readiness";
|
|
15
|
+
code: "verification-contract-unsupported";
|
|
16
|
+
verificationId?: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
kind: "verification-adapter";
|
|
21
|
+
code: VerificationAdapterCode;
|
|
22
|
+
verificationId: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type VerificationCapabilityResult =
|
|
27
|
+
| {
|
|
28
|
+
ok: true;
|
|
29
|
+
verificationId: string;
|
|
30
|
+
runnerBindings: VerificationRunnerBinding[];
|
|
31
|
+
}
|
|
32
|
+
| { ok: false; diagnostic: VerificationCapabilityDiagnostic };
|
|
33
|
+
|
|
34
|
+
export interface VerificationRunnerBinding {
|
|
35
|
+
command: string;
|
|
36
|
+
executablePath: string;
|
|
37
|
+
mountSource?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface VerificationReadiness {
|
|
41
|
+
taskContractsExecutable: boolean;
|
|
42
|
+
diagnostics: VerificationCapabilityDiagnostic[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SHELL_OPERATOR = /[;&|`$<>\n\r\0]/u;
|
|
46
|
+
const SIMPLE_TOKEN =
|
|
47
|
+
/^(?:[a-z0-9][a-z0-9._:@/=-]*|--?[a-z0-9][a-z0-9._:@/=-]*)$/iu;
|
|
48
|
+
const NETWORKING_RUNNERS = new Set(["bunx", "pnpx"]);
|
|
49
|
+
const SYSTEM_MOUNT_ROOTS = ["/usr", "/bin", "/lib", "/lib64", "/etc"];
|
|
50
|
+
const BROAD_MOUNT_ROOTS = new Set([
|
|
51
|
+
"/",
|
|
52
|
+
"/home",
|
|
53
|
+
"/root",
|
|
54
|
+
"/tmp",
|
|
55
|
+
"/var",
|
|
56
|
+
"/var/tmp",
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
function capabilitySuccess(
|
|
60
|
+
verificationId: string,
|
|
61
|
+
runnerBindings: VerificationRunnerBinding[] = [],
|
|
62
|
+
): VerificationCapabilityResult {
|
|
63
|
+
const seen = new Set<string>();
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
verificationId,
|
|
67
|
+
runnerBindings: runnerBindings.filter((binding) => {
|
|
68
|
+
const key = `${binding.command}\0${binding.executablePath}\0${binding.mountSource ?? ""}`;
|
|
69
|
+
if (seen.has(key)) return false;
|
|
70
|
+
seen.add(key);
|
|
71
|
+
return true;
|
|
72
|
+
}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function adapterFailure(
|
|
77
|
+
verificationId: string,
|
|
78
|
+
code: VerificationAdapterCode,
|
|
79
|
+
message: string,
|
|
80
|
+
): VerificationCapabilityResult {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
diagnostic: { kind: "verification-adapter", code, verificationId, message },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fileStatus(
|
|
88
|
+
root: string,
|
|
89
|
+
relative: string,
|
|
90
|
+
): "regular" | "absent" | "unsafe" {
|
|
91
|
+
if (!isValidRelativePath(relative)) return "unsafe";
|
|
92
|
+
const resolvedRoot = path.resolve(root);
|
|
93
|
+
const absolute = path.resolve(resolvedRoot, relative);
|
|
94
|
+
if (
|
|
95
|
+
absolute === resolvedRoot ||
|
|
96
|
+
!absolute.startsWith(`${resolvedRoot}${path.sep}`)
|
|
97
|
+
) {
|
|
98
|
+
return "unsafe";
|
|
99
|
+
}
|
|
100
|
+
const stat = lstatSync(absolute, { throwIfNoEntry: false });
|
|
101
|
+
if (!stat) return "absent";
|
|
102
|
+
return stat.isFile() && !stat.isSymbolicLink() ? "regular" : "unsafe";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function regularFile(root: string, relative: string): boolean {
|
|
106
|
+
return fileStatus(root, relative) === "regular";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function localExecutable(
|
|
110
|
+
root: string,
|
|
111
|
+
executable: string,
|
|
112
|
+
dependencyOwner = root,
|
|
113
|
+
): boolean {
|
|
114
|
+
const dependencyRoot = path.resolve(dependencyOwner, "node_modules");
|
|
115
|
+
const unresolved = path.resolve(dependencyRoot, ".bin", executable);
|
|
116
|
+
try {
|
|
117
|
+
const canonicalDependencyRoot = realpathSync(dependencyRoot);
|
|
118
|
+
const canonical = realpathSync(unresolved);
|
|
119
|
+
const stat = lstatSync(canonical);
|
|
120
|
+
return (
|
|
121
|
+
canonical !== canonicalDependencyRoot &&
|
|
122
|
+
canonical.startsWith(`${canonicalDependencyRoot}${path.sep}`) &&
|
|
123
|
+
stat.isFile() &&
|
|
124
|
+
(stat.mode & 0o111) !== 0
|
|
125
|
+
);
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function within(root: string, candidate: string): boolean {
|
|
132
|
+
return candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function commonDirectory(left: string, right: string): string {
|
|
136
|
+
const leftParts = path.resolve(left).split(path.sep).filter(Boolean);
|
|
137
|
+
const rightParts = path.resolve(right).split(path.sep).filter(Boolean);
|
|
138
|
+
const common: string[] = [];
|
|
139
|
+
for (
|
|
140
|
+
let index = 0;
|
|
141
|
+
index < Math.min(leftParts.length, rightParts.length);
|
|
142
|
+
index++
|
|
143
|
+
) {
|
|
144
|
+
if (leftParts[index] !== rightParts[index]) break;
|
|
145
|
+
common.push(leftParts[index] as string);
|
|
146
|
+
}
|
|
147
|
+
return path.join(path.parse(path.resolve(left)).root, ...common);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function unsafeMountSource(source: string): boolean {
|
|
151
|
+
if (BROAD_MOUNT_ROOTS.has(source)) return true;
|
|
152
|
+
const parts = source.split(path.sep).filter(Boolean);
|
|
153
|
+
return (
|
|
154
|
+
(parts[0] === "home" && parts.length <= 2) ||
|
|
155
|
+
(parts[0] === "Users" && parts.length <= 2) ||
|
|
156
|
+
(parts[0] === "root" && parts.length <= 1)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveVerificationRunner(
|
|
161
|
+
command: string,
|
|
162
|
+
): VerificationRunnerBinding | null {
|
|
163
|
+
const candidates = [
|
|
164
|
+
...(command === "bun" && process.versions.bun ? [process.execPath] : []),
|
|
165
|
+
...(process.env.PATH ?? "")
|
|
166
|
+
.split(path.delimiter)
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
.map((directory) => path.join(directory, command)),
|
|
169
|
+
];
|
|
170
|
+
for (const candidate of candidates) {
|
|
171
|
+
try {
|
|
172
|
+
const executablePath = path.resolve(candidate);
|
|
173
|
+
const canonical = realpathSync(executablePath);
|
|
174
|
+
const stat = lstatSync(canonical);
|
|
175
|
+
if (!stat.isFile() || (stat.mode & 0o111) === 0) continue;
|
|
176
|
+
const systemVisible = SYSTEM_MOUNT_ROOTS.some(
|
|
177
|
+
(root) => within(root, executablePath) && within(root, canonical),
|
|
178
|
+
);
|
|
179
|
+
if (systemVisible) return { command, executablePath };
|
|
180
|
+
const mountSource = commonDirectory(
|
|
181
|
+
path.dirname(executablePath),
|
|
182
|
+
path.dirname(canonical),
|
|
183
|
+
);
|
|
184
|
+
if (unsafeMountSource(mountSource)) continue;
|
|
185
|
+
const sourceStat = lstatSync(mountSource);
|
|
186
|
+
if (!sourceStat.isDirectory() || sourceStat.isSymbolicLink()) continue;
|
|
187
|
+
return { command, executablePath, mountSource };
|
|
188
|
+
} catch {}
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function packageManifest(root: string): Record<string, unknown> | null {
|
|
194
|
+
if (!regularFile(root, "package.json")) return null;
|
|
195
|
+
try {
|
|
196
|
+
const value = JSON.parse(
|
|
197
|
+
readFileSync(path.join(root, "package.json"), "utf8"),
|
|
198
|
+
);
|
|
199
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
200
|
+
? (value as Record<string, unknown>)
|
|
201
|
+
: null;
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function validateScriptCommand(
|
|
208
|
+
root: string,
|
|
209
|
+
verificationId: string,
|
|
210
|
+
command: string,
|
|
211
|
+
dependencyOwner: string,
|
|
212
|
+
): VerificationCapabilityResult {
|
|
213
|
+
if (
|
|
214
|
+
command.length === 0 ||
|
|
215
|
+
command.trim() !== command ||
|
|
216
|
+
SHELL_OPERATOR.test(command)
|
|
217
|
+
) {
|
|
218
|
+
return adapterFailure(
|
|
219
|
+
verificationId,
|
|
220
|
+
"script-unsafe",
|
|
221
|
+
"approved package script contains unsupported shell syntax",
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
const tokens = command.split(/\s+/u);
|
|
225
|
+
if (!SIMPLE_TOKEN.test(tokens[0] ?? "")) {
|
|
226
|
+
return adapterFailure(
|
|
227
|
+
verificationId,
|
|
228
|
+
"script-unsafe",
|
|
229
|
+
"approved package script cannot be represented as safe tokens",
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const executable = tokens[0] ?? "";
|
|
233
|
+
const subcommands = new Set(tokens.slice(1));
|
|
234
|
+
if (
|
|
235
|
+
NETWORKING_RUNNERS.has(executable) ||
|
|
236
|
+
(executable === "bun" && subcommands.has("x")) ||
|
|
237
|
+
(executable === "npm" &&
|
|
238
|
+
(subcommands.has("exec") || subcommands.has("x"))) ||
|
|
239
|
+
(executable === "pnpm" && subcommands.has("dlx")) ||
|
|
240
|
+
(executable === "yarn" && subcommands.has("dlx"))
|
|
241
|
+
) {
|
|
242
|
+
return adapterFailure(
|
|
243
|
+
verificationId,
|
|
244
|
+
"script-unsafe",
|
|
245
|
+
"package script uses a runner that can download implicitly",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (executable === "npx") {
|
|
249
|
+
const local = tokens[1] === "--no-install" ? tokens[2] : undefined;
|
|
250
|
+
if (!local || !localExecutable(root, local, dependencyOwner)) {
|
|
251
|
+
return adapterFailure(
|
|
252
|
+
verificationId,
|
|
253
|
+
"local-executable-missing",
|
|
254
|
+
"npx requires --no-install and an installed local executable",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const runner = resolveVerificationRunner("npx");
|
|
258
|
+
return runner
|
|
259
|
+
? capabilitySuccess(verificationId, [runner])
|
|
260
|
+
: adapterFailure(
|
|
261
|
+
verificationId,
|
|
262
|
+
"runner-missing",
|
|
263
|
+
"npx is unavailable or cannot be mounted safely",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
if (["node", "bun"].includes(executable)) {
|
|
267
|
+
const runner = resolveVerificationRunner(executable);
|
|
268
|
+
return runner
|
|
269
|
+
? capabilitySuccess(verificationId, [runner])
|
|
270
|
+
: adapterFailure(
|
|
271
|
+
verificationId,
|
|
272
|
+
"runner-missing",
|
|
273
|
+
`approved runner ${executable} is unavailable or cannot be mounted safely`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
return localExecutable(root, executable, dependencyOwner)
|
|
277
|
+
? capabilitySuccess(verificationId)
|
|
278
|
+
: adapterFailure(
|
|
279
|
+
verificationId,
|
|
280
|
+
"local-executable-missing",
|
|
281
|
+
`local executable ${executable} is unavailable`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function validatePackageScript(
|
|
286
|
+
root: string,
|
|
287
|
+
verificationId: string,
|
|
288
|
+
packageManager: string,
|
|
289
|
+
script: string,
|
|
290
|
+
approvedCommand?: string,
|
|
291
|
+
dependencyOwner = root,
|
|
292
|
+
): VerificationCapabilityResult {
|
|
293
|
+
const packageRunner = resolveVerificationRunner(packageManager);
|
|
294
|
+
if (!packageRunner) {
|
|
295
|
+
return adapterFailure(
|
|
296
|
+
verificationId,
|
|
297
|
+
"runner-missing",
|
|
298
|
+
`approved package manager ${packageManager} is unavailable or cannot be mounted safely`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const manifest = packageManifest(root);
|
|
302
|
+
const scripts = manifest?.scripts;
|
|
303
|
+
const actual =
|
|
304
|
+
scripts && typeof scripts === "object" && !Array.isArray(scripts)
|
|
305
|
+
? (scripts as Record<string, unknown>)[script]
|
|
306
|
+
: undefined;
|
|
307
|
+
if (typeof actual !== "string") {
|
|
308
|
+
return adapterFailure(
|
|
309
|
+
verificationId,
|
|
310
|
+
"script-missing",
|
|
311
|
+
`package.json script ${script} is missing`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (approvedCommand !== undefined && actual !== approvedCommand) {
|
|
315
|
+
return adapterFailure(
|
|
316
|
+
verificationId,
|
|
317
|
+
"script-command-mismatch",
|
|
318
|
+
`package.json script ${script} differs from the Gate B contract`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const scriptCapability = validateScriptCommand(
|
|
322
|
+
root,
|
|
323
|
+
verificationId,
|
|
324
|
+
actual,
|
|
325
|
+
dependencyOwner,
|
|
326
|
+
);
|
|
327
|
+
return scriptCapability.ok
|
|
328
|
+
? capabilitySuccess(verificationId, [
|
|
329
|
+
packageRunner,
|
|
330
|
+
...scriptCapability.runnerBindings,
|
|
331
|
+
])
|
|
332
|
+
: scriptCapability;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function validateAtomicCapability(
|
|
336
|
+
root: string,
|
|
337
|
+
step: AtomicVerificationContract,
|
|
338
|
+
dependencyOwner: string,
|
|
339
|
+
allowedMissingInputs: ReadonlySet<string>,
|
|
340
|
+
): VerificationCapabilityResult {
|
|
341
|
+
for (const input of verificationInputPaths(step)) {
|
|
342
|
+
const status = fileStatus(root, input);
|
|
343
|
+
if (
|
|
344
|
+
status !== "regular" &&
|
|
345
|
+
!(status === "absent" && allowedMissingInputs.has(input))
|
|
346
|
+
) {
|
|
347
|
+
return adapterFailure(
|
|
348
|
+
step.id,
|
|
349
|
+
"input-missing",
|
|
350
|
+
`verification input ${input} is missing or unsafe`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (step.kind === "package-script") {
|
|
355
|
+
return validatePackageScript(
|
|
356
|
+
root,
|
|
357
|
+
step.id,
|
|
358
|
+
step.packageManager,
|
|
359
|
+
step.script,
|
|
360
|
+
step.command,
|
|
361
|
+
dependencyOwner,
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const runner = step.runner;
|
|
365
|
+
if (runner.kind === "package-script") {
|
|
366
|
+
const script = validatePackageScript(
|
|
367
|
+
root,
|
|
368
|
+
step.id,
|
|
369
|
+
runner.packageManager,
|
|
370
|
+
runner.script,
|
|
371
|
+
runner.command,
|
|
372
|
+
dependencyOwner,
|
|
373
|
+
);
|
|
374
|
+
if (!script.ok) return script;
|
|
375
|
+
if (
|
|
376
|
+
step.kind === "vitest" &&
|
|
377
|
+
!localExecutable(root, "vitest", dependencyOwner)
|
|
378
|
+
) {
|
|
379
|
+
return adapterFailure(
|
|
380
|
+
step.id,
|
|
381
|
+
"local-executable-missing",
|
|
382
|
+
"Vitest is not installed in consumer node_modules",
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return script;
|
|
386
|
+
}
|
|
387
|
+
if (runner.kind === "node") {
|
|
388
|
+
const node = resolveVerificationRunner("node");
|
|
389
|
+
return node
|
|
390
|
+
? capabilitySuccess(step.id, [node])
|
|
391
|
+
: adapterFailure(
|
|
392
|
+
step.id,
|
|
393
|
+
"runner-missing",
|
|
394
|
+
"Node is unavailable or cannot be mounted safely",
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (!localExecutable(root, runner.executable, dependencyOwner)) {
|
|
398
|
+
return adapterFailure(
|
|
399
|
+
step.id,
|
|
400
|
+
"local-executable-missing",
|
|
401
|
+
`local executable ${runner.executable} is unavailable`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (runner.kind === "npx") {
|
|
405
|
+
const npx = resolveVerificationRunner("npx");
|
|
406
|
+
return npx
|
|
407
|
+
? capabilitySuccess(step.id, [npx])
|
|
408
|
+
: adapterFailure(
|
|
409
|
+
step.id,
|
|
410
|
+
"runner-missing",
|
|
411
|
+
"npx is unavailable or cannot be mounted safely",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return capabilitySuccess(step.id);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function validateVerificationCapability(
|
|
418
|
+
root: string,
|
|
419
|
+
value: unknown,
|
|
420
|
+
options: {
|
|
421
|
+
dependencyOwner?: string;
|
|
422
|
+
allowedMissingInputs?: readonly string[];
|
|
423
|
+
} = {},
|
|
424
|
+
): VerificationCapabilityResult {
|
|
425
|
+
const validation = validateVerificationContract(value);
|
|
426
|
+
if (!validation.ok) {
|
|
427
|
+
const id =
|
|
428
|
+
value &&
|
|
429
|
+
typeof value === "object" &&
|
|
430
|
+
!Array.isArray(value) &&
|
|
431
|
+
typeof (value as Record<string, unknown>).id === "string"
|
|
432
|
+
? String((value as Record<string, unknown>).id)
|
|
433
|
+
: undefined;
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
diagnostic: {
|
|
437
|
+
kind: "design-readiness",
|
|
438
|
+
code: "verification-contract-unsupported",
|
|
439
|
+
...(id ? { verificationId: id } : {}),
|
|
440
|
+
message: validation.reason,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const dependencyOwner = options.dependencyOwner ?? root;
|
|
445
|
+
const allowedMissingInputs = new Set(options.allowedMissingInputs ?? []);
|
|
446
|
+
const runnerBindings: VerificationRunnerBinding[] = [];
|
|
447
|
+
for (const step of verificationSteps(validation.value)) {
|
|
448
|
+
const capability = validateAtomicCapability(
|
|
449
|
+
root,
|
|
450
|
+
step,
|
|
451
|
+
dependencyOwner,
|
|
452
|
+
allowedMissingInputs,
|
|
453
|
+
);
|
|
454
|
+
if (!capability.ok) return capability;
|
|
455
|
+
runnerBindings.push(...capability.runnerBindings);
|
|
456
|
+
}
|
|
457
|
+
return capabilitySuccess(validation.value.id, runnerBindings);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function assessVerificationReadiness(
|
|
461
|
+
root: string,
|
|
462
|
+
contracts: readonly unknown[],
|
|
463
|
+
options: {
|
|
464
|
+
dependencyOwner?: string;
|
|
465
|
+
allowedMissingInputs?: readonly string[];
|
|
466
|
+
} = {},
|
|
467
|
+
): VerificationReadiness {
|
|
468
|
+
const diagnostics = contracts
|
|
469
|
+
.map((contract) => validateVerificationCapability(root, contract, options))
|
|
470
|
+
.filter(
|
|
471
|
+
(
|
|
472
|
+
result,
|
|
473
|
+
): result is Extract<VerificationCapabilityResult, { ok: false }> =>
|
|
474
|
+
!result.ok,
|
|
475
|
+
)
|
|
476
|
+
.map((result) => result.diagnostic);
|
|
477
|
+
return {
|
|
478
|
+
taskContractsExecutable: diagnostics.length === 0,
|
|
479
|
+
diagnostics,
|
|
480
|
+
};
|
|
481
|
+
}
|