@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,1427 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { compareCanonicalStrings } from "./canonical.ts";
|
|
5
|
+
import {
|
|
6
|
+
type ApprovalBoundaryCode,
|
|
7
|
+
isValidRelativePath,
|
|
8
|
+
type StructuredVerificationContract,
|
|
9
|
+
verificationBoundInputPaths,
|
|
10
|
+
} from "./contracts.ts";
|
|
11
|
+
import type { ControlStage } from "./control-contracts.ts";
|
|
12
|
+
import type {
|
|
13
|
+
GateApprovalProof,
|
|
14
|
+
ImplementPlan,
|
|
15
|
+
PlanTaskDraft,
|
|
16
|
+
} from "./delivery-compiler.ts";
|
|
17
|
+
import type { RoutePolicy } from "./route-policy.ts";
|
|
18
|
+
import { canonicalJson } from "./run-state.ts";
|
|
19
|
+
import type { OperationLease } from "./run-store.ts";
|
|
20
|
+
import type { ResolvedStateRoot } from "./state-root.ts";
|
|
21
|
+
import type { WorkflowActivityUpdate } from "./subagent-activity.ts";
|
|
22
|
+
import {
|
|
23
|
+
type CandidateContextBoundary,
|
|
24
|
+
type CandidateContextRef,
|
|
25
|
+
classifyCandidateContextRequest,
|
|
26
|
+
normalizeCandidateContextRefs,
|
|
27
|
+
} from "./submit-tool.ts";
|
|
28
|
+
import type { WorkspaceEntry } from "./workspace-store.ts";
|
|
29
|
+
|
|
30
|
+
export const SHA256 = /^[a-f0-9]{64}$/u;
|
|
31
|
+
export const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
|
|
32
|
+
export const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
|
|
33
|
+
export const PACKAGE_NAME =
|
|
34
|
+
/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/iu;
|
|
35
|
+
export const LOCKFILES = new Set([
|
|
36
|
+
"bun.lock",
|
|
37
|
+
"bun.lockb",
|
|
38
|
+
"package-lock.json",
|
|
39
|
+
"pnpm-lock.yaml",
|
|
40
|
+
"yarn.lock",
|
|
41
|
+
]);
|
|
42
|
+
export const IMPLEMENTATION_ROUTE_LIMITS = Object.freeze({
|
|
43
|
+
preferredContextWindow: 128_000,
|
|
44
|
+
preferredOutputTokens: 64_000,
|
|
45
|
+
minimumContextWindow: 16_000,
|
|
46
|
+
minimumOutputTokens: 8_000,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export function implementationRouteRequirements(
|
|
50
|
+
input: {
|
|
51
|
+
task?: PlanTaskDraft;
|
|
52
|
+
artifactCorrection?: RedArtifactCorrection;
|
|
53
|
+
repair?: { failureIdentities: string[] };
|
|
54
|
+
} = {},
|
|
55
|
+
) {
|
|
56
|
+
if (!input.task) {
|
|
57
|
+
return {
|
|
58
|
+
minContextWindow: IMPLEMENTATION_ROUTE_LIMITS.minimumContextWindow,
|
|
59
|
+
minOutputTokens: IMPLEMENTATION_ROUTE_LIMITS.minimumOutputTokens,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const serializedBytes = Buffer.byteLength(canonicalJson(input.task), "utf8");
|
|
63
|
+
const pathCount = Object.values(input.task.phases).reduce(
|
|
64
|
+
(total, phase) =>
|
|
65
|
+
total + phase.read.length + phase.write.length + phase.delete.length,
|
|
66
|
+
0,
|
|
67
|
+
);
|
|
68
|
+
const complexity =
|
|
69
|
+
serializedBytes +
|
|
70
|
+
pathCount * 512 +
|
|
71
|
+
(input.artifactCorrection ? 16_384 : 0) +
|
|
72
|
+
(input.repair ? input.repair.failureIdentities.length * 256 + 16_384 : 0);
|
|
73
|
+
return {
|
|
74
|
+
minContextWindow: IMPLEMENTATION_ROUTE_LIMITS.minimumContextWindow,
|
|
75
|
+
minOutputTokens: IMPLEMENTATION_ROUTE_LIMITS.minimumOutputTokens,
|
|
76
|
+
preferredContextWindow: Math.min(
|
|
77
|
+
IMPLEMENTATION_ROUTE_LIMITS.preferredContextWindow,
|
|
78
|
+
Math.max(
|
|
79
|
+
IMPLEMENTATION_ROUTE_LIMITS.minimumContextWindow,
|
|
80
|
+
16_384 + complexity * 2,
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
preferredOutputTokens: Math.min(
|
|
84
|
+
IMPLEMENTATION_ROUTE_LIMITS.preferredOutputTokens,
|
|
85
|
+
Math.max(
|
|
86
|
+
IMPLEMENTATION_ROUTE_LIMITS.minimumOutputTokens,
|
|
87
|
+
8_192 + Math.ceil(complexity / 2),
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const SEMANTIC_ROUTE_FAILURE_CODES = new Set([
|
|
94
|
+
"child-no-structural-submit",
|
|
95
|
+
"invalid-structural-result",
|
|
96
|
+
"structural-identity-mismatch",
|
|
97
|
+
"invalid-diff",
|
|
98
|
+
"candidate-diff-invalid",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
export function semanticRouteFailure(
|
|
102
|
+
value: DurableCandidateProposal,
|
|
103
|
+
): string | undefined {
|
|
104
|
+
return (value.kind === "retryable" || value.kind === "paused") &&
|
|
105
|
+
SEMANTIC_ROUTE_FAILURE_CODES.has(value.code)
|
|
106
|
+
? value.code
|
|
107
|
+
: undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isCancellationException(
|
|
111
|
+
error: unknown,
|
|
112
|
+
signal: AbortSignal,
|
|
113
|
+
): boolean {
|
|
114
|
+
if (!signal.aborted) return false;
|
|
115
|
+
if (error === signal.reason) return true;
|
|
116
|
+
return (
|
|
117
|
+
error instanceof Error &&
|
|
118
|
+
(error.name === "AbortError" ||
|
|
119
|
+
error.message === "cancelled" ||
|
|
120
|
+
error.message === "operation-cancelled")
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// These adapters are read-only. Cancellation fences their late settlement;
|
|
125
|
+
// mutating operations must instead be awaited through their owned lifecycle.
|
|
126
|
+
export async function cancellableRead<T>(
|
|
127
|
+
read: () => Promise<T>,
|
|
128
|
+
signal?: AbortSignal,
|
|
129
|
+
): Promise<T> {
|
|
130
|
+
signal?.throwIfAborted();
|
|
131
|
+
if (!signal) return read();
|
|
132
|
+
return new Promise<T>((resolve, reject) => {
|
|
133
|
+
const onAbort = () => reject(signal.reason);
|
|
134
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
135
|
+
const reading = Promise.resolve().then(() => {
|
|
136
|
+
signal.throwIfAborted();
|
|
137
|
+
return read();
|
|
138
|
+
});
|
|
139
|
+
reading
|
|
140
|
+
.then(resolve, reject)
|
|
141
|
+
.finally(() => signal.removeEventListener("abort", onAbort));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function emitWorkflowActivity(
|
|
146
|
+
observer: ((event: WorkflowActivityUpdate) => void) | undefined,
|
|
147
|
+
event: WorkflowActivityUpdate,
|
|
148
|
+
): void {
|
|
149
|
+
try {
|
|
150
|
+
observer?.(structuredClone(event));
|
|
151
|
+
} catch {
|
|
152
|
+
// Presentation is observational and cannot change workflow behavior.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface WorkflowWorkspaceFacts {
|
|
157
|
+
baselineRevisionId?: string;
|
|
158
|
+
currentWorkspaceRevisionId?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface WorkflowRouteFacts {
|
|
162
|
+
routeId?: string;
|
|
163
|
+
routeFingerprint?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface WorkflowVerificationStatus {
|
|
167
|
+
scope: "baseline" | DurableVerificationScope;
|
|
168
|
+
attribution: "pre-existing" | "introduced" | "unresolved" | "environment";
|
|
169
|
+
taskId?: string;
|
|
170
|
+
failureIdentities?: string[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface WorkflowContextRequest {
|
|
174
|
+
code:
|
|
175
|
+
| "approved-context-needed"
|
|
176
|
+
| "task-split-needed"
|
|
177
|
+
| "boundary-review-needed";
|
|
178
|
+
refs: CandidateContextRef[];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface RedArtifactCorrection {
|
|
182
|
+
code: "red-artifact-constraint";
|
|
183
|
+
attempt: number;
|
|
184
|
+
maxAttempts: number;
|
|
185
|
+
contextRequest?: WorkflowContextRequest;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export type RecoveryActionRequest = {
|
|
189
|
+
kind: "affected-repair" | "cumulative-repair" | "red-correction";
|
|
190
|
+
attempt: number;
|
|
191
|
+
};
|
|
192
|
+
export type RecoveryActionDecision =
|
|
193
|
+
| { allowed: true }
|
|
194
|
+
| {
|
|
195
|
+
allowed: false;
|
|
196
|
+
code: "repair-attempts-exhausted" | "artifact-attempts-exhausted";
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** One policy for every nested recovery action; execution services supply facts only. */
|
|
200
|
+
export function decideRecoveryAction(
|
|
201
|
+
plan: ImplementPlan,
|
|
202
|
+
request: RecoveryActionRequest,
|
|
203
|
+
authority: { additionalAttempt?: boolean; verificationOnly?: boolean } = {},
|
|
204
|
+
): RecoveryActionDecision {
|
|
205
|
+
const correction = request.kind === "red-correction";
|
|
206
|
+
const limit =
|
|
207
|
+
authority.additionalAttempt || authority.verificationOnly
|
|
208
|
+
? 1
|
|
209
|
+
: correction
|
|
210
|
+
? plan.verification.artifactCorrection.maxAttempts
|
|
211
|
+
: plan.verification.repair.maxAttempts;
|
|
212
|
+
return Number.isSafeInteger(request.attempt) &&
|
|
213
|
+
request.attempt > 0 &&
|
|
214
|
+
request.attempt <= limit
|
|
215
|
+
? { allowed: true }
|
|
216
|
+
: {
|
|
217
|
+
allowed: false,
|
|
218
|
+
code: correction
|
|
219
|
+
? "artifact-attempts-exhausted"
|
|
220
|
+
: "repair-attempts-exhausted",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface WorkflowRecoveryFeedback {
|
|
225
|
+
code: string;
|
|
226
|
+
attempt: number;
|
|
227
|
+
maxAttempts: number;
|
|
228
|
+
strategy:
|
|
229
|
+
| "revise-candidate"
|
|
230
|
+
| "refresh-candidate"
|
|
231
|
+
| "repair-verification"
|
|
232
|
+
| "compact-patch";
|
|
233
|
+
failureIdentities?: string[];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface WorkflowRecoveryFact {
|
|
237
|
+
key: string;
|
|
238
|
+
failures: number;
|
|
239
|
+
feedback: WorkflowRecoveryFeedback;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const APPROVAL_AUTHORITY_CATEGORIES = [
|
|
243
|
+
"observable-behavior",
|
|
244
|
+
"architecture-policy",
|
|
245
|
+
"dependency",
|
|
246
|
+
"path-boundary",
|
|
247
|
+
"conflict-resource",
|
|
248
|
+
"verification-contract",
|
|
249
|
+
"agents-contract",
|
|
250
|
+
"irreversible-scope",
|
|
251
|
+
] as const;
|
|
252
|
+
|
|
253
|
+
export type ApprovalAuthorityCategory =
|
|
254
|
+
(typeof APPROVAL_AUTHORITY_CATEGORIES)[number];
|
|
255
|
+
|
|
256
|
+
export interface ApprovalRequirement {
|
|
257
|
+
category: ApprovalAuthorityCategory;
|
|
258
|
+
requiredGates: Array<"gate-a" | "gate-b">;
|
|
259
|
+
refs: string[];
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const INTERNAL_APPROVAL_CODES = [
|
|
263
|
+
"boundary-review-needed",
|
|
264
|
+
"repair-boundary-expansion",
|
|
265
|
+
"task-write-set-empty",
|
|
266
|
+
] as const;
|
|
267
|
+
|
|
268
|
+
export type InternalApprovalCode = (typeof INTERNAL_APPROVAL_CODES)[number];
|
|
269
|
+
export type WorkflowApprovalCode = ApprovalBoundaryCode | InternalApprovalCode;
|
|
270
|
+
|
|
271
|
+
// These pauses need a new executable plan, not a new user decision. Environment,
|
|
272
|
+
// integrity and cancellation codes deliberately do not grant this continuation.
|
|
273
|
+
export function permitsPlanAmendment(code: string): boolean {
|
|
274
|
+
return ["delivery-invalid", "needs-task-split", "task-split-needed"].includes(
|
|
275
|
+
code,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export const APPROVAL_REQUIREMENT_BY_CODE = {
|
|
280
|
+
"agents-contract-insufficient": {
|
|
281
|
+
category: "agents-contract",
|
|
282
|
+
requiredGates: ["gate-b"],
|
|
283
|
+
},
|
|
284
|
+
"architecture-contract-insufficient": {
|
|
285
|
+
category: "architecture-policy",
|
|
286
|
+
requiredGates: ["gate-b"],
|
|
287
|
+
},
|
|
288
|
+
"behavior-contract-insufficient": {
|
|
289
|
+
category: "observable-behavior",
|
|
290
|
+
requiredGates: ["gate-a", "gate-b"],
|
|
291
|
+
},
|
|
292
|
+
"boundary-review-needed": {
|
|
293
|
+
category: "path-boundary",
|
|
294
|
+
requiredGates: ["gate-b"],
|
|
295
|
+
},
|
|
296
|
+
"conflict-resource-authority-insufficient": {
|
|
297
|
+
category: "conflict-resource",
|
|
298
|
+
requiredGates: ["gate-b"],
|
|
299
|
+
},
|
|
300
|
+
"irreversible-scope-insufficient": {
|
|
301
|
+
category: "irreversible-scope",
|
|
302
|
+
requiredGates: ["gate-a", "gate-b"],
|
|
303
|
+
},
|
|
304
|
+
"repair-boundary-expansion": {
|
|
305
|
+
category: "path-boundary",
|
|
306
|
+
requiredGates: ["gate-b"],
|
|
307
|
+
},
|
|
308
|
+
"task-scope-insufficient": {
|
|
309
|
+
category: "path-boundary",
|
|
310
|
+
requiredGates: ["gate-b"],
|
|
311
|
+
},
|
|
312
|
+
"task-write-set-empty": {
|
|
313
|
+
category: "path-boundary",
|
|
314
|
+
requiredGates: ["gate-b"],
|
|
315
|
+
},
|
|
316
|
+
"unapproved-dependency-change": {
|
|
317
|
+
category: "dependency",
|
|
318
|
+
requiredGates: ["gate-b"],
|
|
319
|
+
},
|
|
320
|
+
"verification-contract-insufficient": {
|
|
321
|
+
category: "verification-contract",
|
|
322
|
+
requiredGates: ["gate-b"],
|
|
323
|
+
},
|
|
324
|
+
} as const satisfies Record<
|
|
325
|
+
WorkflowApprovalCode,
|
|
326
|
+
{
|
|
327
|
+
category: ApprovalAuthorityCategory;
|
|
328
|
+
requiredGates: readonly ("gate-a" | "gate-b")[];
|
|
329
|
+
}
|
|
330
|
+
>;
|
|
331
|
+
|
|
332
|
+
export type WorkflowRetryPolicy =
|
|
333
|
+
| "artifact"
|
|
334
|
+
| "stale"
|
|
335
|
+
| "verification"
|
|
336
|
+
| "checkpoint";
|
|
337
|
+
|
|
338
|
+
export type WorkflowAttemptOutcome = (
|
|
339
|
+
| {
|
|
340
|
+
kind: "phase-committed";
|
|
341
|
+
artifactHash: string;
|
|
342
|
+
isolatedRevisionId: string;
|
|
343
|
+
exitCode: number;
|
|
344
|
+
classification: "expected-red" | "expected-green" | "expected-refactor";
|
|
345
|
+
}
|
|
346
|
+
| {
|
|
347
|
+
kind: "paused" | "retryable" | "approval-needed";
|
|
348
|
+
code: string;
|
|
349
|
+
untrustedCandidate?: Uint8Array;
|
|
350
|
+
verification?: WorkflowVerificationStatus;
|
|
351
|
+
contextRequest?: WorkflowContextRequest;
|
|
352
|
+
retryPolicy?: WorkflowRetryPolicy;
|
|
353
|
+
}
|
|
354
|
+
| { kind: "operation-cancelled"; code: "cancelled" }
|
|
355
|
+
) &
|
|
356
|
+
WorkflowWorkspaceFacts &
|
|
357
|
+
WorkflowRouteFacts & {
|
|
358
|
+
attemptDiagnostic?: SafeAttemptDiagnostic;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export interface WorkflowWorker {
|
|
362
|
+
hasPendingVerification?(
|
|
363
|
+
input: Parameters<WorkflowWorker["runAttempt"]>[0],
|
|
364
|
+
): Promise<boolean>;
|
|
365
|
+
isContextReadAvailable?(input: {
|
|
366
|
+
runId: string;
|
|
367
|
+
deliveryRevision: number;
|
|
368
|
+
plan: ImplementPlan;
|
|
369
|
+
baselineRevisionId: string;
|
|
370
|
+
currentWorkspaceRevisionId: string;
|
|
371
|
+
path: string;
|
|
372
|
+
}): boolean;
|
|
373
|
+
runAttempt(input: {
|
|
374
|
+
runId: string;
|
|
375
|
+
operationId: string;
|
|
376
|
+
deliveryRevision: number;
|
|
377
|
+
taskId: string;
|
|
378
|
+
phase: "red" | "green" | "refactor";
|
|
379
|
+
task: PlanTaskDraft;
|
|
380
|
+
plan: ImplementPlan;
|
|
381
|
+
routeId?: string;
|
|
382
|
+
routeFingerprint?: string;
|
|
383
|
+
baselineRevisionId?: string;
|
|
384
|
+
currentWorkspaceRevisionId?: string;
|
|
385
|
+
repair?: {
|
|
386
|
+
attribution: "introduced";
|
|
387
|
+
failureIdentities: string[];
|
|
388
|
+
};
|
|
389
|
+
artifactCorrection?: RedArtifactCorrection;
|
|
390
|
+
contextRequest?: WorkflowContextRequest;
|
|
391
|
+
contextReadPaths?: string[];
|
|
392
|
+
recoveryFeedback?: WorkflowRecoveryFeedback;
|
|
393
|
+
additionalAttempt?: boolean;
|
|
394
|
+
verificationOnly?: boolean;
|
|
395
|
+
reserveCandidate?: () => boolean;
|
|
396
|
+
recoveryDecision?: (
|
|
397
|
+
request: RecoveryActionRequest,
|
|
398
|
+
) => RecoveryActionDecision;
|
|
399
|
+
signal: AbortSignal;
|
|
400
|
+
onActivity?: (
|
|
401
|
+
event: Pick<
|
|
402
|
+
WorkflowActivityUpdate,
|
|
403
|
+
"state" | "code" | "attempt" | "maxAttempts" | "wait"
|
|
404
|
+
>,
|
|
405
|
+
) => void;
|
|
406
|
+
}): Promise<WorkflowAttemptOutcome>;
|
|
407
|
+
rebind(input: {
|
|
408
|
+
runId: string;
|
|
409
|
+
taskId: string;
|
|
410
|
+
task: PlanTaskDraft;
|
|
411
|
+
role: "implementation-worker";
|
|
412
|
+
routeId: string;
|
|
413
|
+
}):
|
|
414
|
+
| {
|
|
415
|
+
ok: true;
|
|
416
|
+
routeId?: string;
|
|
417
|
+
routeFingerprint?: string;
|
|
418
|
+
route?: { id?: string; fingerprint?: string };
|
|
419
|
+
}
|
|
420
|
+
| { ok: false; code: string };
|
|
421
|
+
revalidateDelivery?(input: {
|
|
422
|
+
runId: string;
|
|
423
|
+
deliveryRevision: number;
|
|
424
|
+
plan: ImplementPlan;
|
|
425
|
+
tasks: Array<{
|
|
426
|
+
taskId: string;
|
|
427
|
+
deliveryRevision: number;
|
|
428
|
+
state: string;
|
|
429
|
+
phase: "red" | "green" | "refactor";
|
|
430
|
+
contextReadPaths?: string[];
|
|
431
|
+
}>;
|
|
432
|
+
invalidatedTaskIds: string[];
|
|
433
|
+
baselineRevisionId: string;
|
|
434
|
+
currentWorkspaceRevisionId: string;
|
|
435
|
+
}): {
|
|
436
|
+
invalidatedTaskIds: string[];
|
|
437
|
+
baselineRevisionId: string;
|
|
438
|
+
currentWorkspaceRevisionId: string;
|
|
439
|
+
};
|
|
440
|
+
updateRoutePolicy?(policy: RoutePolicy): void;
|
|
441
|
+
routePolicyStatus?(): Record<string, unknown>;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export interface WorkflowDelivery {
|
|
445
|
+
gate: "gate-a" | "gate-b";
|
|
446
|
+
revision: number;
|
|
447
|
+
receiptHash: string;
|
|
448
|
+
plan: ImplementPlan;
|
|
449
|
+
approvalProofs?: {
|
|
450
|
+
gateA: GateApprovalProof;
|
|
451
|
+
gateB: GateApprovalProof;
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export interface WorkflowAvailableDelivery {
|
|
456
|
+
deliveryRevision: number;
|
|
457
|
+
receiptHash: string;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export interface WorkflowDeliverySource {
|
|
461
|
+
load(input: {
|
|
462
|
+
stage: ControlStage;
|
|
463
|
+
change: string;
|
|
464
|
+
signal?: AbortSignal;
|
|
465
|
+
deliveryRevision?: number;
|
|
466
|
+
receiptHash?: string;
|
|
467
|
+
}): Promise<WorkflowDelivery>;
|
|
468
|
+
discoverLatest?(input: {
|
|
469
|
+
stage: ControlStage;
|
|
470
|
+
change: string;
|
|
471
|
+
signal?: AbortSignal;
|
|
472
|
+
}): Promise<WorkflowAvailableDelivery | undefined>;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export interface WorkflowChangeVerifier {
|
|
476
|
+
verify(input: {
|
|
477
|
+
taskEvidence?: Array<{ taskId: string; deliveryRevision: number }>;
|
|
478
|
+
runId: string;
|
|
479
|
+
deliveryRevision: number;
|
|
480
|
+
plan: ImplementPlan;
|
|
481
|
+
baselineRevisionId?: string;
|
|
482
|
+
currentWorkspaceRevisionId?: string;
|
|
483
|
+
signal: AbortSignal;
|
|
484
|
+
}): Promise<
|
|
485
|
+
| {
|
|
486
|
+
kind: "verified";
|
|
487
|
+
verificationId: string;
|
|
488
|
+
currentWorkspaceRevisionId?: string;
|
|
489
|
+
}
|
|
490
|
+
| {
|
|
491
|
+
kind: "paused" | "retryable" | "approval-needed";
|
|
492
|
+
code: string;
|
|
493
|
+
verification?: WorkflowVerificationStatus;
|
|
494
|
+
}
|
|
495
|
+
>;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export interface WorkflowApplication {
|
|
499
|
+
prepare?(input: {
|
|
500
|
+
transactionId: string;
|
|
501
|
+
runId: string;
|
|
502
|
+
consumerRoot: string;
|
|
503
|
+
deliveryRevision: number;
|
|
504
|
+
plan: ImplementPlan;
|
|
505
|
+
contextReadPaths?: string[];
|
|
506
|
+
baselineRevisionId?: string;
|
|
507
|
+
currentWorkspaceRevisionId?: string;
|
|
508
|
+
signal: AbortSignal;
|
|
509
|
+
}): Promise<Record<string, unknown>>;
|
|
510
|
+
applyPrepared?(input: {
|
|
511
|
+
transactionId: string;
|
|
512
|
+
runId: string;
|
|
513
|
+
consumerRoot: string;
|
|
514
|
+
deliveryRevision: number;
|
|
515
|
+
plan: ImplementPlan;
|
|
516
|
+
contextReadPaths?: string[];
|
|
517
|
+
baselineRevisionId?: string;
|
|
518
|
+
currentWorkspaceRevisionId?: string;
|
|
519
|
+
signal: AbortSignal;
|
|
520
|
+
}): Promise<Record<string, unknown>>;
|
|
521
|
+
begin(input: {
|
|
522
|
+
transactionId: string;
|
|
523
|
+
runId: string;
|
|
524
|
+
consumerRoot: string;
|
|
525
|
+
deliveryRevision: number;
|
|
526
|
+
plan: ImplementPlan;
|
|
527
|
+
contextReadPaths?: string[];
|
|
528
|
+
baselineRevisionId?: string;
|
|
529
|
+
currentWorkspaceRevisionId?: string;
|
|
530
|
+
signal: AbortSignal;
|
|
531
|
+
}): Promise<Record<string, unknown>>;
|
|
532
|
+
requestControl(
|
|
533
|
+
transactionId: string,
|
|
534
|
+
intent: "cancel" | "discard",
|
|
535
|
+
context?: WorkflowApplicationContext,
|
|
536
|
+
): Record<string, unknown>;
|
|
537
|
+
recover(
|
|
538
|
+
transactionId: string,
|
|
539
|
+
context?: WorkflowApplicationContext,
|
|
540
|
+
): Promise<Record<string, unknown>>;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export interface WorkflowApplicationContext {
|
|
544
|
+
runId: string;
|
|
545
|
+
deliveryRevision: number;
|
|
546
|
+
plan: ImplementPlan;
|
|
547
|
+
contextReadPaths?: string[];
|
|
548
|
+
baselineRevisionId?: string;
|
|
549
|
+
currentWorkspaceRevisionId?: string;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export interface WorkflowRunLifecycle {
|
|
553
|
+
cleanup(runId: string): void;
|
|
554
|
+
close?(): void;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export interface WorkflowEngineOptions {
|
|
558
|
+
consumerRoot: string;
|
|
559
|
+
stateRoot: ResolvedStateRoot;
|
|
560
|
+
deliverySource: WorkflowDeliverySource;
|
|
561
|
+
worker: WorkflowWorker;
|
|
562
|
+
changeVerifier: WorkflowChangeVerifier;
|
|
563
|
+
application?: WorkflowApplication;
|
|
564
|
+
lifecycle?: WorkflowRunLifecycle;
|
|
565
|
+
now?: () => number;
|
|
566
|
+
leaseTtlMs?: number;
|
|
567
|
+
workHardLimit?: number;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export interface EngineRunRow {
|
|
571
|
+
current_revision: number | null;
|
|
572
|
+
baseline_workspace_revision: string | null;
|
|
573
|
+
current_workspace_revision: string | null;
|
|
574
|
+
cleanup_state: "none" | "retained" | "complete";
|
|
575
|
+
route_id: string | null;
|
|
576
|
+
route_fingerprint: string | null;
|
|
577
|
+
transaction_id: string | null;
|
|
578
|
+
verification_json: string | null;
|
|
579
|
+
delivery_diagnostics_json: string | null;
|
|
580
|
+
next_queue_position: number;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export interface EngineTaskRow {
|
|
584
|
+
task_id: string;
|
|
585
|
+
task_order: number;
|
|
586
|
+
delivery_revision: number;
|
|
587
|
+
plan_json: string;
|
|
588
|
+
state: string;
|
|
589
|
+
phase: "red" | "green" | "refactor";
|
|
590
|
+
pause_code: string | null;
|
|
591
|
+
context_request_json: string | null;
|
|
592
|
+
attempt_diagnostic_json: string | null;
|
|
593
|
+
route_id: string | null;
|
|
594
|
+
route_fingerprint: string | null;
|
|
595
|
+
queue_position: number | null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export interface SafeAttemptDiagnostic {
|
|
599
|
+
fingerprint?: string;
|
|
600
|
+
finalCategory?: string;
|
|
601
|
+
submitAttempts?: number;
|
|
602
|
+
schema?: string;
|
|
603
|
+
identityMismatch?: string[];
|
|
604
|
+
sameFailureCount?: number;
|
|
605
|
+
action?: "rebind-or-revise-delivery";
|
|
606
|
+
recovery?: WorkflowRecoveryFact;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function recoveryKey(
|
|
610
|
+
task: PlanTaskDraft,
|
|
611
|
+
phase: string,
|
|
612
|
+
_row: EngineTaskRow,
|
|
613
|
+
_run: EngineRunRow,
|
|
614
|
+
): string {
|
|
615
|
+
// Keep the Red witness and the active phase's obligation across replanning.
|
|
616
|
+
// Execution identities, verifier names, and wording are not progress.
|
|
617
|
+
const obligation = (boundary: PlanTaskDraft["phases"]["red"]) => {
|
|
618
|
+
const verification = structuredClone(
|
|
619
|
+
boundary.verification,
|
|
620
|
+
) as unknown as Record<string, unknown>;
|
|
621
|
+
delete verification.id;
|
|
622
|
+
return {
|
|
623
|
+
verification,
|
|
624
|
+
verificationInputs: boundary.verificationInputs
|
|
625
|
+
.map((binding) => canonicalJson(binding))
|
|
626
|
+
.sort(compareCanonicalStrings),
|
|
627
|
+
};
|
|
628
|
+
};
|
|
629
|
+
const boundary = task.phases[phase as keyof PlanTaskDraft["phases"]];
|
|
630
|
+
if (!boundary) throw new Error("workflow-task-phase-invalid");
|
|
631
|
+
return hash(
|
|
632
|
+
canonicalJson({
|
|
633
|
+
red: obligation(task.phases.red),
|
|
634
|
+
phase: obligation(boundary),
|
|
635
|
+
}),
|
|
636
|
+
phase,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export interface EngineOperationRow {
|
|
641
|
+
command: string;
|
|
642
|
+
state: string;
|
|
643
|
+
outcome_json: string | null;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export interface ActiveOperation {
|
|
647
|
+
operationId: string;
|
|
648
|
+
kind: "work" | "applying";
|
|
649
|
+
controller: AbortController;
|
|
650
|
+
settled: Promise<Record<string, unknown>>;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export interface HeldOperationLease {
|
|
654
|
+
lease: OperationLease;
|
|
655
|
+
timer: ReturnType<typeof setInterval>;
|
|
656
|
+
error?: Error;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export interface BootstrapRow {
|
|
660
|
+
handoff_id: string;
|
|
661
|
+
owner_run_id: string;
|
|
662
|
+
receipt_hash: string;
|
|
663
|
+
state: string;
|
|
664
|
+
facts_json: string;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
668
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function normalizeWorkflowVerificationStatus(
|
|
672
|
+
value: unknown,
|
|
673
|
+
): WorkflowVerificationStatus {
|
|
674
|
+
if (
|
|
675
|
+
!isRecord(value) ||
|
|
676
|
+
typeof value.scope !== "string" ||
|
|
677
|
+
![
|
|
678
|
+
"baseline",
|
|
679
|
+
"baseline-task-affected",
|
|
680
|
+
"baseline-full-suite",
|
|
681
|
+
"task-affected",
|
|
682
|
+
"change-task-affected",
|
|
683
|
+
"change-full-suite",
|
|
684
|
+
"agents-checkpoint",
|
|
685
|
+
"post-apply",
|
|
686
|
+
].includes(value.scope) ||
|
|
687
|
+
!["pre-existing", "introduced", "unresolved", "environment"].includes(
|
|
688
|
+
String(value.attribution),
|
|
689
|
+
) ||
|
|
690
|
+
(value.taskId !== undefined && typeof value.taskId !== "string") ||
|
|
691
|
+
(value.failureIdentities !== undefined &&
|
|
692
|
+
(!Array.isArray(value.failureIdentities) ||
|
|
693
|
+
value.failureIdentities.length > 256 ||
|
|
694
|
+
value.failureIdentities.some(
|
|
695
|
+
(identity) => typeof identity !== "string" || !SHA256.test(identity),
|
|
696
|
+
)))
|
|
697
|
+
) {
|
|
698
|
+
throw new Error("workflow-verification-status-invalid");
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
scope: value.scope as WorkflowVerificationStatus["scope"],
|
|
702
|
+
attribution: value.attribution as WorkflowVerificationStatus["attribution"],
|
|
703
|
+
...(typeof value.taskId === "string" ? { taskId: value.taskId } : {}),
|
|
704
|
+
...(Array.isArray(value.failureIdentities)
|
|
705
|
+
? {
|
|
706
|
+
failureIdentities: [
|
|
707
|
+
...new Set(value.failureIdentities as string[]),
|
|
708
|
+
].sort(),
|
|
709
|
+
}
|
|
710
|
+
: {}),
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function normalizeWorkflowContextRequest(
|
|
715
|
+
value: unknown,
|
|
716
|
+
): WorkflowContextRequest {
|
|
717
|
+
if (
|
|
718
|
+
!isRecord(value) ||
|
|
719
|
+
![
|
|
720
|
+
"approved-context-needed",
|
|
721
|
+
"task-split-needed",
|
|
722
|
+
"boundary-review-needed",
|
|
723
|
+
].includes(String(value.code))
|
|
724
|
+
) {
|
|
725
|
+
throw new Error("workflow-context-request-invalid");
|
|
726
|
+
}
|
|
727
|
+
let refs: CandidateContextRef[];
|
|
728
|
+
try {
|
|
729
|
+
refs = normalizeCandidateContextRefs(value.refs);
|
|
730
|
+
} catch {
|
|
731
|
+
throw new Error("workflow-context-request-invalid");
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
code: value.code as WorkflowContextRequest["code"],
|
|
735
|
+
refs,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function contextApprovalRefs(request: WorkflowContextRequest): string[] {
|
|
740
|
+
return request.refs.flatMap((ref) =>
|
|
741
|
+
ref.kind === "requested-path" ? [ref.path] : [],
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export function contextBoundaryForTask(
|
|
746
|
+
task: PlanTaskDraft,
|
|
747
|
+
phase: "red" | "green" | "refactor",
|
|
748
|
+
): CandidateContextBoundary {
|
|
749
|
+
const boundary = task.phases[phase];
|
|
750
|
+
if (!boundary) throw new Error("workflow-task-phase-invalid");
|
|
751
|
+
return {
|
|
752
|
+
phase,
|
|
753
|
+
contextReadRoots: task.roots,
|
|
754
|
+
readPaths: boundary.read,
|
|
755
|
+
writePaths: boundary.write,
|
|
756
|
+
deletePaths: boundary.delete,
|
|
757
|
+
taskPaths: [
|
|
758
|
+
...new Set(
|
|
759
|
+
Object.values(task.phases).flatMap((candidate) => [
|
|
760
|
+
...candidate.read,
|
|
761
|
+
...candidate.write,
|
|
762
|
+
...candidate.delete,
|
|
763
|
+
]),
|
|
764
|
+
),
|
|
765
|
+
],
|
|
766
|
+
redWritePaths: task.phases.red.write,
|
|
767
|
+
agents: {
|
|
768
|
+
impact: task.agents.impact,
|
|
769
|
+
...(task.agents.target ? { target: task.agents.target } : {}),
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function classifyPersistedContextRequest(
|
|
775
|
+
request: WorkflowContextRequest,
|
|
776
|
+
task: PlanTaskDraft,
|
|
777
|
+
phase: "red" | "green" | "refactor",
|
|
778
|
+
) {
|
|
779
|
+
return classifyCandidateContextRequest(
|
|
780
|
+
{
|
|
781
|
+
kind: "context-request",
|
|
782
|
+
candidateId: "candidate-durable-reclassification",
|
|
783
|
+
code: request.code,
|
|
784
|
+
refs: request.refs,
|
|
785
|
+
},
|
|
786
|
+
contextBoundaryForTask(task, phase),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function approvalRequirement(
|
|
791
|
+
code: string,
|
|
792
|
+
contextRequest?: WorkflowContextRequest,
|
|
793
|
+
): ApprovalRequirement {
|
|
794
|
+
const requirement = Object.hasOwn(APPROVAL_REQUIREMENT_BY_CODE, code)
|
|
795
|
+
? APPROVAL_REQUIREMENT_BY_CODE[
|
|
796
|
+
code as keyof typeof APPROVAL_REQUIREMENT_BY_CODE
|
|
797
|
+
]
|
|
798
|
+
: undefined;
|
|
799
|
+
if (!requirement) throw new Error("approval-code-invalid");
|
|
800
|
+
return {
|
|
801
|
+
category: requirement.category,
|
|
802
|
+
requiredGates: [...requirement.requiredGates],
|
|
803
|
+
refs: contextRequest ? contextApprovalRefs(contextRequest) : [],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export function isWorkflowApprovalCode(
|
|
808
|
+
code: string,
|
|
809
|
+
): code is WorkflowApprovalCode {
|
|
810
|
+
return Object.hasOwn(APPROVAL_REQUIREMENT_BY_CODE, code);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
export function hash(...values: string[]): string {
|
|
814
|
+
const digest = createHash("sha256");
|
|
815
|
+
for (const value of values) {
|
|
816
|
+
digest.update(`${Buffer.byteLength(value)}:`);
|
|
817
|
+
digest.update(value);
|
|
818
|
+
}
|
|
819
|
+
return digest.digest("hex");
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
export interface DependencyContractEntry {
|
|
823
|
+
value: string;
|
|
824
|
+
names: string[];
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
export class DependencyManifestError extends Error {
|
|
828
|
+
constructor() {
|
|
829
|
+
super("package-manifest-invalid");
|
|
830
|
+
this.name = "DependencyManifestError";
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export function dependencyName(selector: string): string | null {
|
|
835
|
+
const scoped = /(@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*)/iu.exec(
|
|
836
|
+
selector,
|
|
837
|
+
)?.[1];
|
|
838
|
+
if (scoped) return scoped;
|
|
839
|
+
const segment = selector.split("/").at(-1) ?? "";
|
|
840
|
+
const name = segment.replace(/@.*$/u, "");
|
|
841
|
+
return PACKAGE_NAME.test(name) ? name : null;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
export function addDependencyResolutionEntries(
|
|
845
|
+
result: Map<string, DependencyContractEntry>,
|
|
846
|
+
section: string,
|
|
847
|
+
value: unknown,
|
|
848
|
+
selectorPath: string[] = [],
|
|
849
|
+
names: string[] = [],
|
|
850
|
+
): void {
|
|
851
|
+
if (isRecord(value)) {
|
|
852
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
853
|
+
const name = dependencyName(key);
|
|
854
|
+
addDependencyResolutionEntries(
|
|
855
|
+
result,
|
|
856
|
+
section,
|
|
857
|
+
entry,
|
|
858
|
+
[...selectorPath, key],
|
|
859
|
+
name && key !== "." ? [...new Set([...names, name])] : names,
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
result.set(`${section}:${selectorPath.join(":")}`, {
|
|
865
|
+
value: canonicalJson(value),
|
|
866
|
+
names,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export function addDependencyListPolicy(
|
|
871
|
+
result: Map<string, DependencyContractEntry>,
|
|
872
|
+
section: string,
|
|
873
|
+
value: unknown,
|
|
874
|
+
): void {
|
|
875
|
+
if (value === undefined) return;
|
|
876
|
+
if (
|
|
877
|
+
!Array.isArray(value) ||
|
|
878
|
+
value.some(
|
|
879
|
+
(name) => typeof name !== "string" || !PACKAGE_NAME.test(name),
|
|
880
|
+
) ||
|
|
881
|
+
new Set(value).size !== value.length
|
|
882
|
+
) {
|
|
883
|
+
throw new DependencyManifestError();
|
|
884
|
+
}
|
|
885
|
+
result.set(section, {
|
|
886
|
+
value: canonicalJson([...value].sort()),
|
|
887
|
+
names: [],
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export function addPatchedDependencyPolicy(
|
|
892
|
+
result: Map<string, DependencyContractEntry>,
|
|
893
|
+
section: string,
|
|
894
|
+
value: unknown,
|
|
895
|
+
): void {
|
|
896
|
+
if (value === undefined) return;
|
|
897
|
+
if (!isRecord(value)) throw new DependencyManifestError();
|
|
898
|
+
for (const [selector, patchPath] of Object.entries(value)) {
|
|
899
|
+
if (
|
|
900
|
+
!dependencyName(selector) ||
|
|
901
|
+
typeof patchPath !== "string" ||
|
|
902
|
+
!isValidRelativePath(patchPath)
|
|
903
|
+
) {
|
|
904
|
+
throw new DependencyManifestError();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
result.set(section, { value: canonicalJson(value), names: [] });
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
export function dependencyContract(
|
|
911
|
+
root: string,
|
|
912
|
+
): Map<string, DependencyContractEntry> {
|
|
913
|
+
let manifest: unknown;
|
|
914
|
+
try {
|
|
915
|
+
manifest = JSON.parse(
|
|
916
|
+
readFileSync(path.join(root, "package.json"), "utf8"),
|
|
917
|
+
);
|
|
918
|
+
} catch {
|
|
919
|
+
throw new DependencyManifestError();
|
|
920
|
+
}
|
|
921
|
+
if (!isRecord(manifest)) throw new DependencyManifestError();
|
|
922
|
+
|
|
923
|
+
const result = new Map<string, DependencyContractEntry>();
|
|
924
|
+
for (const section of [
|
|
925
|
+
"dependencies",
|
|
926
|
+
"devDependencies",
|
|
927
|
+
"peerDependencies",
|
|
928
|
+
"optionalDependencies",
|
|
929
|
+
]) {
|
|
930
|
+
const dependencies = manifest[section];
|
|
931
|
+
if (dependencies === undefined) continue;
|
|
932
|
+
if (!isRecord(dependencies)) throw new DependencyManifestError();
|
|
933
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
934
|
+
if (typeof version !== "string") throw new DependencyManifestError();
|
|
935
|
+
result.set(`${section}:${name}`, { value: version, names: [name] });
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
for (const section of [
|
|
939
|
+
"overrides",
|
|
940
|
+
"resolutions",
|
|
941
|
+
"dependenciesMeta",
|
|
942
|
+
"peerDependenciesMeta",
|
|
943
|
+
]) {
|
|
944
|
+
if (manifest[section] !== undefined) {
|
|
945
|
+
addDependencyResolutionEntries(result, section, manifest[section]);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const section of ["workspaces", "catalog", "catalogs"]) {
|
|
949
|
+
if (manifest[section] !== undefined) {
|
|
950
|
+
result.set(section, {
|
|
951
|
+
value: canonicalJson(manifest[section]),
|
|
952
|
+
names: [],
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
for (const section of [
|
|
957
|
+
"trustedDependencies",
|
|
958
|
+
"blockedDependencies",
|
|
959
|
+
"bundledDependencies",
|
|
960
|
+
"bundleDependencies",
|
|
961
|
+
]) {
|
|
962
|
+
addDependencyListPolicy(result, section, manifest[section]);
|
|
963
|
+
}
|
|
964
|
+
addPatchedDependencyPolicy(
|
|
965
|
+
result,
|
|
966
|
+
"patchedDependencies",
|
|
967
|
+
manifest.patchedDependencies,
|
|
968
|
+
);
|
|
969
|
+
|
|
970
|
+
const pnpm = manifest.pnpm;
|
|
971
|
+
if (pnpm !== undefined) {
|
|
972
|
+
if (!isRecord(pnpm)) throw new DependencyManifestError();
|
|
973
|
+
const resolutionSections = [
|
|
974
|
+
"overrides",
|
|
975
|
+
"packageExtensions",
|
|
976
|
+
"peerDependencyRules",
|
|
977
|
+
"allowedDeprecatedVersions",
|
|
978
|
+
];
|
|
979
|
+
for (const section of resolutionSections) {
|
|
980
|
+
if (pnpm[section] !== undefined) {
|
|
981
|
+
addDependencyResolutionEntries(
|
|
982
|
+
result,
|
|
983
|
+
`pnpm.${section}`,
|
|
984
|
+
pnpm[section],
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
addPatchedDependencyPolicy(
|
|
989
|
+
result,
|
|
990
|
+
"pnpm.patchedDependencies",
|
|
991
|
+
pnpm.patchedDependencies,
|
|
992
|
+
);
|
|
993
|
+
const tracked = new Set([...resolutionSections, "patchedDependencies"]);
|
|
994
|
+
const policy = Object.fromEntries(
|
|
995
|
+
Object.entries(pnpm).filter(([key]) => !tracked.has(key)),
|
|
996
|
+
);
|
|
997
|
+
if (Object.keys(policy).length > 0) {
|
|
998
|
+
result.set("pnpm.policy", { value: canonicalJson(policy), names: [] });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return result;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
export function hasUnapprovedDependencyChange(
|
|
1005
|
+
before: Map<string, DependencyContractEntry>,
|
|
1006
|
+
after: Map<string, DependencyContractEntry>,
|
|
1007
|
+
approvedDependencies: readonly string[],
|
|
1008
|
+
lockfileChanged: boolean,
|
|
1009
|
+
): boolean {
|
|
1010
|
+
if (lockfileChanged) return true;
|
|
1011
|
+
const approved = new Set(approvedDependencies);
|
|
1012
|
+
for (const key of new Set([...before.keys(), ...after.keys()])) {
|
|
1013
|
+
const previous = before.get(key);
|
|
1014
|
+
const next = after.get(key);
|
|
1015
|
+
if (previous?.value === next?.value) continue;
|
|
1016
|
+
const names = new Set([...(previous?.names ?? []), ...(next?.names ?? [])]);
|
|
1017
|
+
if (names.size === 0 || [...names].some((name) => !approved.has(name))) {
|
|
1018
|
+
return true;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
export function parseJsonRecord(value: string): Record<string, unknown> {
|
|
1025
|
+
const parsed: unknown = JSON.parse(value);
|
|
1026
|
+
if (!isRecord(parsed)) throw new Error("workflow-engine-record-invalid");
|
|
1027
|
+
return parsed;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
export function parseDeliveryDiagnostics(value: string): string[] {
|
|
1031
|
+
const parsed: unknown = JSON.parse(value);
|
|
1032
|
+
if (
|
|
1033
|
+
!Array.isArray(parsed) ||
|
|
1034
|
+
parsed.length === 0 ||
|
|
1035
|
+
parsed.length > 256 ||
|
|
1036
|
+
parsed.some(
|
|
1037
|
+
(diagnostic) =>
|
|
1038
|
+
typeof diagnostic !== "string" ||
|
|
1039
|
+
diagnostic.length === 0 ||
|
|
1040
|
+
diagnostic.length > 1024,
|
|
1041
|
+
) ||
|
|
1042
|
+
new Set(parsed).size !== parsed.length
|
|
1043
|
+
) {
|
|
1044
|
+
throw new Error("workflow-delivery-diagnostics-invalid");
|
|
1045
|
+
}
|
|
1046
|
+
return [...(parsed as string[])];
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export interface BootstrapAcceptanceFact {
|
|
1050
|
+
command: string;
|
|
1051
|
+
exitCode: 0;
|
|
1052
|
+
evidenceHash: string;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
export function normalizeBootstrapAcceptanceFacts(
|
|
1056
|
+
value: unknown,
|
|
1057
|
+
): BootstrapAcceptanceFact[] | undefined {
|
|
1058
|
+
if (value === undefined) return undefined;
|
|
1059
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 32) {
|
|
1060
|
+
throw new Error("bootstrap-handoff-invalid");
|
|
1061
|
+
}
|
|
1062
|
+
const commands = new Set<string>();
|
|
1063
|
+
const facts = value.map((entry): BootstrapAcceptanceFact => {
|
|
1064
|
+
if (
|
|
1065
|
+
!isRecord(entry) ||
|
|
1066
|
+
Object.keys(entry).length !== 3 ||
|
|
1067
|
+
!Object.hasOwn(entry, "command") ||
|
|
1068
|
+
!Object.hasOwn(entry, "exitCode") ||
|
|
1069
|
+
!Object.hasOwn(entry, "evidenceHash") ||
|
|
1070
|
+
typeof entry.command !== "string" ||
|
|
1071
|
+
entry.command.length === 0 ||
|
|
1072
|
+
entry.command.length > 512 ||
|
|
1073
|
+
/[\n\r\0]/u.test(entry.command) ||
|
|
1074
|
+
entry.exitCode !== 0 ||
|
|
1075
|
+
typeof entry.evidenceHash !== "string" ||
|
|
1076
|
+
!SHA256.test(entry.evidenceHash) ||
|
|
1077
|
+
commands.has(entry.command)
|
|
1078
|
+
) {
|
|
1079
|
+
throw new Error("bootstrap-handoff-invalid");
|
|
1080
|
+
}
|
|
1081
|
+
commands.add(entry.command);
|
|
1082
|
+
return {
|
|
1083
|
+
command: entry.command,
|
|
1084
|
+
exitCode: 0,
|
|
1085
|
+
evidenceHash: entry.evidenceHash,
|
|
1086
|
+
};
|
|
1087
|
+
});
|
|
1088
|
+
return facts.sort((left, right) =>
|
|
1089
|
+
compareCanonicalStrings(left.command, right.command),
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export function bootstrapAcceptanceHash(
|
|
1094
|
+
facts: readonly BootstrapAcceptanceFact[],
|
|
1095
|
+
workspaceManifestHash: string,
|
|
1096
|
+
) {
|
|
1097
|
+
return createHash("sha256")
|
|
1098
|
+
.update(
|
|
1099
|
+
canonicalJson({
|
|
1100
|
+
acceptanceFacts: facts,
|
|
1101
|
+
workspaceManifestHash,
|
|
1102
|
+
}),
|
|
1103
|
+
)
|
|
1104
|
+
.digest("hex");
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
export function parsePlan(value: string): ImplementPlan {
|
|
1108
|
+
const parsed: unknown = JSON.parse(value);
|
|
1109
|
+
assertPlan(parsed);
|
|
1110
|
+
return parsed;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
export function assertPlan(value: unknown): asserts value is ImplementPlan {
|
|
1114
|
+
if (
|
|
1115
|
+
!isRecord(value) ||
|
|
1116
|
+
typeof value.changeId !== "string" ||
|
|
1117
|
+
!Array.isArray(value.tasks) ||
|
|
1118
|
+
!Array.isArray(value.outputs) ||
|
|
1119
|
+
!isRecord(value.verification) ||
|
|
1120
|
+
!isRecord(value.verification.artifactCorrection) ||
|
|
1121
|
+
!Number.isSafeInteger(value.verification.artifactCorrection.maxAttempts) ||
|
|
1122
|
+
(value.verification.artifactCorrection.maxAttempts as number) < 2 ||
|
|
1123
|
+
(value.verification.artifactCorrection.maxAttempts as number) > 3 ||
|
|
1124
|
+
value.tasks.length === 0
|
|
1125
|
+
) {
|
|
1126
|
+
throw new Error("delivery-plan-invalid");
|
|
1127
|
+
}
|
|
1128
|
+
const identifiers = new Set<string>();
|
|
1129
|
+
for (const task of value.tasks) {
|
|
1130
|
+
if (
|
|
1131
|
+
!isRecord(task) ||
|
|
1132
|
+
typeof task.taskId !== "string" ||
|
|
1133
|
+
!IDENTIFIER.test(task.taskId) ||
|
|
1134
|
+
identifiers.has(task.taskId) ||
|
|
1135
|
+
!Array.isArray(task.dependsOn) ||
|
|
1136
|
+
!isRecord(task.phases) ||
|
|
1137
|
+
!isRecord(task.phases.red) ||
|
|
1138
|
+
!isRecord(task.phases.green) ||
|
|
1139
|
+
!isRecord(task.scheduling)
|
|
1140
|
+
) {
|
|
1141
|
+
throw new Error("delivery-plan-invalid-task");
|
|
1142
|
+
}
|
|
1143
|
+
identifiers.add(task.taskId);
|
|
1144
|
+
}
|
|
1145
|
+
for (const task of value.tasks as unknown as PlanTaskDraft[]) {
|
|
1146
|
+
if (task.dependsOn.some((dependency) => !identifiers.has(dependency))) {
|
|
1147
|
+
throw new Error("delivery-plan-invalid-dependency");
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
export function assertDelivery(
|
|
1153
|
+
value: unknown,
|
|
1154
|
+
stage: ControlStage,
|
|
1155
|
+
change: string,
|
|
1156
|
+
requestedRevision?: number,
|
|
1157
|
+
requestedReceipt?: string,
|
|
1158
|
+
): asserts value is WorkflowDelivery {
|
|
1159
|
+
if (
|
|
1160
|
+
!isRecord(value) ||
|
|
1161
|
+
(value.gate !== "gate-a" && value.gate !== "gate-b") ||
|
|
1162
|
+
!Number.isSafeInteger(value.revision) ||
|
|
1163
|
+
(value.revision as number) < 1 ||
|
|
1164
|
+
typeof value.receiptHash !== "string" ||
|
|
1165
|
+
!SHA256.test(value.receiptHash)
|
|
1166
|
+
) {
|
|
1167
|
+
throw new Error("delivery-invalid");
|
|
1168
|
+
}
|
|
1169
|
+
assertPlan(value.plan);
|
|
1170
|
+
if (value.approvalProofs !== undefined) {
|
|
1171
|
+
if (!isRecord(value.approvalProofs)) throw new Error("delivery-invalid");
|
|
1172
|
+
for (const gate of ["gateA", "gateB"] as const) {
|
|
1173
|
+
const proof = value.approvalProofs[gate];
|
|
1174
|
+
if (
|
|
1175
|
+
!isRecord(proof) ||
|
|
1176
|
+
!Number.isSafeInteger(proof.revision) ||
|
|
1177
|
+
(proof.revision as number) < 1 ||
|
|
1178
|
+
typeof proof.contractHash !== "string" ||
|
|
1179
|
+
!SHA256.test(proof.contractHash) ||
|
|
1180
|
+
typeof proof.recordHash !== "string" ||
|
|
1181
|
+
!SHA256.test(proof.recordHash)
|
|
1182
|
+
) {
|
|
1183
|
+
throw new Error("delivery-invalid");
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (stage === "abel-implement" && value.gate !== "gate-b") {
|
|
1188
|
+
throw new Error("delivery-gate-b-required");
|
|
1189
|
+
}
|
|
1190
|
+
if ((value.plan as ImplementPlan).changeId !== change) {
|
|
1191
|
+
throw new Error("delivery-change-mismatch");
|
|
1192
|
+
}
|
|
1193
|
+
if (requestedRevision !== undefined && value.revision !== requestedRevision) {
|
|
1194
|
+
throw new Error("delivery-revision-mismatch");
|
|
1195
|
+
}
|
|
1196
|
+
if (
|
|
1197
|
+
requestedReceipt !== undefined &&
|
|
1198
|
+
value.receiptHash !== requestedReceipt
|
|
1199
|
+
) {
|
|
1200
|
+
throw new Error("delivery-receipt-mismatch");
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
export function phases(
|
|
1205
|
+
task: PlanTaskDraft,
|
|
1206
|
+
): Array<"red" | "green" | "refactor"> {
|
|
1207
|
+
return [
|
|
1208
|
+
...(!task.verificationMode || task.verificationMode === "behavior"
|
|
1209
|
+
? ["red" as const]
|
|
1210
|
+
: []),
|
|
1211
|
+
"green",
|
|
1212
|
+
...(task.phases.refactor ? (["refactor"] as const) : []),
|
|
1213
|
+
];
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
export function stringArray(value: unknown): string[] {
|
|
1217
|
+
return Array.isArray(value)
|
|
1218
|
+
? value.filter((entry): entry is string => typeof entry === "string")
|
|
1219
|
+
: [];
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
export function declarationPathsConflict(left: string, right: string): boolean {
|
|
1223
|
+
return (
|
|
1224
|
+
left === right ||
|
|
1225
|
+
left === "." ||
|
|
1226
|
+
right === "." ||
|
|
1227
|
+
left.startsWith(`${right}/`) ||
|
|
1228
|
+
right.startsWith(`${left}/`)
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
export function taskConflicts(
|
|
1233
|
+
left: PlanTaskDraft,
|
|
1234
|
+
right: PlanTaskDraft,
|
|
1235
|
+
): { taskLifetime: boolean; verification: boolean } {
|
|
1236
|
+
if (
|
|
1237
|
+
left.scheduling.conflicts.includes(right.taskId) ||
|
|
1238
|
+
right.scheduling.conflicts.includes(left.taskId)
|
|
1239
|
+
) {
|
|
1240
|
+
return { taskLifetime: true, verification: false };
|
|
1241
|
+
}
|
|
1242
|
+
const leftResources = new Set(left.scheduling.resources);
|
|
1243
|
+
if (right.scheduling.resources.some((entry) => leftResources.has(entry))) {
|
|
1244
|
+
return { taskLifetime: true, verification: false };
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const declarations = (task: PlanTaskDraft) => {
|
|
1248
|
+
const reads = new Set<string>();
|
|
1249
|
+
const writes = new Set<string>();
|
|
1250
|
+
const verificationLocks = new Set<string>();
|
|
1251
|
+
for (const phase of Object.values(task.phases)) {
|
|
1252
|
+
for (const entry of stringArray(phase.read)) reads.add(entry);
|
|
1253
|
+
for (const entry of stringArray(phase.write)) writes.add(entry);
|
|
1254
|
+
for (const entry of stringArray(phase.delete)) writes.add(entry);
|
|
1255
|
+
if (typeof phase.verificationLock === "string") {
|
|
1256
|
+
verificationLocks.add(phase.verificationLock);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return { reads, writes, verificationLocks };
|
|
1260
|
+
};
|
|
1261
|
+
const leftDeclarations = declarations(left);
|
|
1262
|
+
const rightDeclarations = declarations(right);
|
|
1263
|
+
const leftWriteConflict = [...leftDeclarations.writes].some((entry) =>
|
|
1264
|
+
[...rightDeclarations.writes, ...rightDeclarations.reads].some((other) =>
|
|
1265
|
+
declarationPathsConflict(entry, other),
|
|
1266
|
+
),
|
|
1267
|
+
);
|
|
1268
|
+
const rightWriteConflict = [...rightDeclarations.writes].some((entry) =>
|
|
1269
|
+
[...leftDeclarations.reads].some((other) =>
|
|
1270
|
+
declarationPathsConflict(entry, other),
|
|
1271
|
+
),
|
|
1272
|
+
);
|
|
1273
|
+
const agentsConflict =
|
|
1274
|
+
left.agents.impact !== "none" &&
|
|
1275
|
+
right.agents.impact !== "none" &&
|
|
1276
|
+
left.agents.target !== undefined &&
|
|
1277
|
+
left.agents.target === right.agents.target;
|
|
1278
|
+
const verification = [...leftDeclarations.verificationLocks].some((lock) =>
|
|
1279
|
+
rightDeclarations.verificationLocks.has(lock),
|
|
1280
|
+
);
|
|
1281
|
+
return {
|
|
1282
|
+
taskLifetime: leftWriteConflict || rightWriteConflict || agentsConflict,
|
|
1283
|
+
verification,
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
export function taskApprovalBoundary(
|
|
1288
|
+
plan: ImplementPlan,
|
|
1289
|
+
task: PlanTaskDraft,
|
|
1290
|
+
): string {
|
|
1291
|
+
const planBoundary = structuredClone(plan) as unknown as Record<
|
|
1292
|
+
string,
|
|
1293
|
+
unknown
|
|
1294
|
+
>;
|
|
1295
|
+
delete planBoundary.tasks;
|
|
1296
|
+
delete planBoundary.outputs;
|
|
1297
|
+
if (isRecord(planBoundary.tracking)) {
|
|
1298
|
+
delete planBoundary.tracking.taskIds;
|
|
1299
|
+
}
|
|
1300
|
+
// Retry limits and presentation do not invalidate verified product facts.
|
|
1301
|
+
if (isRecord(planBoundary.verification)) {
|
|
1302
|
+
delete planBoundary.verification.artifactCorrection;
|
|
1303
|
+
if (isRecord(planBoundary.verification.repair))
|
|
1304
|
+
delete planBoundary.verification.repair.maxAttempts;
|
|
1305
|
+
}
|
|
1306
|
+
return canonicalJson({
|
|
1307
|
+
plan: planBoundary,
|
|
1308
|
+
task,
|
|
1309
|
+
outputs: plan.outputs.filter(
|
|
1310
|
+
(output) => output.producer.taskId === task.taskId,
|
|
1311
|
+
),
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
export function workspaceEntryEqual(
|
|
1316
|
+
left: WorkspaceEntry | undefined,
|
|
1317
|
+
right: WorkspaceEntry | undefined,
|
|
1318
|
+
): boolean {
|
|
1319
|
+
if (!left || !right) return left === right;
|
|
1320
|
+
if (left.kind !== right.kind) return false;
|
|
1321
|
+
return (
|
|
1322
|
+
left.kind === "absent" ||
|
|
1323
|
+
(right.kind === "file" &&
|
|
1324
|
+
left.hash === right.hash &&
|
|
1325
|
+
left.bytes === right.bytes &&
|
|
1326
|
+
left.mode === right.mode)
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
export function expectedClassification(
|
|
1331
|
+
phase: string,
|
|
1332
|
+
): "expected-red" | "expected-green" | "expected-refactor" {
|
|
1333
|
+
return phase === "red"
|
|
1334
|
+
? "expected-red"
|
|
1335
|
+
: phase === "refactor"
|
|
1336
|
+
? "expected-refactor"
|
|
1337
|
+
: "expected-green";
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
export type DurableCandidateProposal =
|
|
1341
|
+
| { kind: "candidate"; bytes: Uint8Array }
|
|
1342
|
+
| {
|
|
1343
|
+
kind: "sealed-candidate";
|
|
1344
|
+
candidateId: string;
|
|
1345
|
+
artifactHash: string;
|
|
1346
|
+
bytes: number;
|
|
1347
|
+
paths: string[];
|
|
1348
|
+
}
|
|
1349
|
+
| {
|
|
1350
|
+
kind: "paused" | "retryable" | "approval-needed";
|
|
1351
|
+
code: string;
|
|
1352
|
+
contextRequest?: WorkflowContextRequest;
|
|
1353
|
+
attemptDiagnostic?: SafeAttemptDiagnostic;
|
|
1354
|
+
}
|
|
1355
|
+
| { kind: "operation-cancelled"; code: "cancelled" };
|
|
1356
|
+
|
|
1357
|
+
export type DurableVerificationScope =
|
|
1358
|
+
| "baseline-task-affected"
|
|
1359
|
+
| "baseline-full-suite"
|
|
1360
|
+
| "task-affected"
|
|
1361
|
+
| "change-task-affected"
|
|
1362
|
+
| "change-full-suite"
|
|
1363
|
+
| "agents-checkpoint"
|
|
1364
|
+
| "post-apply";
|
|
1365
|
+
|
|
1366
|
+
export function deliveryTrackingPath(plan: ImplementPlan): string | undefined {
|
|
1367
|
+
if (!isRecord(plan.tracking) || plan.tracking.path !== "tasks.md") {
|
|
1368
|
+
return undefined;
|
|
1369
|
+
}
|
|
1370
|
+
const relative = `openspec/changes/${plan.changeId}/tasks.md`;
|
|
1371
|
+
return isValidRelativePath(relative) ? relative : undefined;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
export function deliveryBoundPaths(plan: ImplementPlan): string[] {
|
|
1375
|
+
const paths = new Set<string>();
|
|
1376
|
+
const bindVerification = (verification: StructuredVerificationContract) => {
|
|
1377
|
+
for (const relative of verificationBoundInputPaths(verification)) {
|
|
1378
|
+
if (relative !== ".") paths.add(relative);
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
for (const task of plan.tasks) {
|
|
1382
|
+
for (const phase of Object.values(task.phases)) {
|
|
1383
|
+
for (const relative of [...phase.read, ...phase.write, ...phase.delete]) {
|
|
1384
|
+
if (relative !== ".") paths.add(relative);
|
|
1385
|
+
}
|
|
1386
|
+
for (const binding of phase.verificationInputs) {
|
|
1387
|
+
if (binding.kind === "workspace") paths.add(binding.path);
|
|
1388
|
+
}
|
|
1389
|
+
bindVerification(phase.verification);
|
|
1390
|
+
}
|
|
1391
|
+
if (hasVerificationLifecycle(plan)) {
|
|
1392
|
+
bindVerification(task.affectedVerification);
|
|
1393
|
+
bindVerification(task.repairVerification);
|
|
1394
|
+
}
|
|
1395
|
+
if (task.agents.target) paths.add(task.agents.target);
|
|
1396
|
+
}
|
|
1397
|
+
for (const output of plan.outputs) paths.add(output.path);
|
|
1398
|
+
if (hasVerificationLifecycle(plan)) {
|
|
1399
|
+
bindVerification(plan.verification.baseline.fullSuite);
|
|
1400
|
+
bindVerification(plan.verification.change.fullSuite);
|
|
1401
|
+
bindVerification(plan.verification.change.postApply);
|
|
1402
|
+
if (plan.verification.agentsCheckpoint.verification) {
|
|
1403
|
+
bindVerification(plan.verification.agentsCheckpoint.verification);
|
|
1404
|
+
}
|
|
1405
|
+
for (const operation of plan.verification.agentsCheckpoint.operations) {
|
|
1406
|
+
paths.add(operation.target);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const trackingPath = deliveryTrackingPath(plan);
|
|
1410
|
+
if (trackingPath) paths.add(trackingPath);
|
|
1411
|
+
return [...paths].sort();
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
export function hasVerificationLifecycle(plan: ImplementPlan): boolean {
|
|
1415
|
+
return (
|
|
1416
|
+
isRecord(plan.verification) &&
|
|
1417
|
+
isRecord(plan.verification.baseline) &&
|
|
1418
|
+
isRecord(plan.verification.change) &&
|
|
1419
|
+
isRecord(plan.verification.repair) &&
|
|
1420
|
+
isRecord(plan.tracking) &&
|
|
1421
|
+
plan.tasks.every(
|
|
1422
|
+
(task) =>
|
|
1423
|
+
isRecord(task.affectedVerification) &&
|
|
1424
|
+
isRecord(task.repairVerification),
|
|
1425
|
+
)
|
|
1426
|
+
);
|
|
1427
|
+
}
|