@abelxiaoxing/cadence 1.0.13 → 1.1.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 +29 -13
- package/agents/contract-reviewer.md +2 -2
- package/agents/design-explorer.md +1 -1
- package/agents/diagnosis-worker.md +6 -3
- package/agents/implementation-worker.md +14 -9
- package/config/.env.example +5 -17
- package/config/routes.example.json +42 -0
- package/package.json +3 -3
- package/prompts/abel-design.md +150 -228
- package/prompts/abel-diagnose.md +37 -18
- package/prompts/abel-implement.md +111 -178
- package/prompts/abel-init.md +36 -33
- package/skills/abel-workflow/SKILL.md +156 -168
- package/src/apply-transaction.ts +1427 -0
- package/src/artifact-store.ts +204 -0
- package/src/child-session.ts +136 -17
- package/src/contracts.ts +105 -696
- package/src/control-contracts.ts +259 -0
- package/src/delivery-compiler.ts +1088 -0
- package/src/file-snapshot.ts +5 -0
- package/src/implement-graph.ts +11 -4
- package/src/index.ts +1465 -133
- package/src/isolation-backend.ts +269 -0
- package/src/packet-runtime.ts +533 -0
- package/src/parent-provider.ts +23 -4
- package/src/route-policy.ts +483 -0
- package/src/run-state.ts +393 -0
- package/src/run-store.ts +918 -0
- package/src/safe-path.ts +11 -0
- package/src/scoped-tools.ts +36 -0
- package/src/state-root.ts +215 -0
- package/src/subagent-activity.ts +526 -27
- package/src/submit-tool.ts +274 -0
- package/src/task-ledger.ts +1380 -0
- package/src/verification-capability.ts +529 -42
- package/src/worker-broker.ts +703 -0
- package/src/workflow-engine.ts +7391 -0
- package/src/workspace-store.ts +544 -0
- package/src/candidate-preflight.ts +0 -1618
- package/src/drain.ts +0 -20
- package/src/patch.ts +0 -505
- package/src/result-store.ts +0 -312
- package/src/runtime.ts +0 -2436
- package/src/scheduler.ts +0 -384
- package/src/subagent-endpoint.ts +0 -158
- package/src/worker.ts +0 -204
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
type AgentsImpact,
|
|
4
|
+
type ImplementGraphBoundary,
|
|
5
|
+
type ImplementGraphOutput,
|
|
6
|
+
type ImplementTaskBoundary,
|
|
7
|
+
isAgentsPath,
|
|
8
|
+
isValidRelativePath,
|
|
9
|
+
type PhaseBoundary,
|
|
10
|
+
type StructuredVerificationContract,
|
|
11
|
+
validateImplementGraphBoundary,
|
|
12
|
+
validateVerificationContract,
|
|
13
|
+
} from "./contracts.ts";
|
|
14
|
+
import {
|
|
15
|
+
assessImplementGraphReadiness,
|
|
16
|
+
canonicalJson,
|
|
17
|
+
hashCanonicalValue,
|
|
18
|
+
type ImplementGraphReadiness,
|
|
19
|
+
} from "./implement-graph.ts";
|
|
20
|
+
import { validateVerificationAdapterCapability } from "./verification-capability.ts";
|
|
21
|
+
|
|
22
|
+
export const IMPLEMENT_PLAN_SCHEMA_VERSION = 3 as const;
|
|
23
|
+
export const IMPLEMENT_PLAN_PATH = "implement-plan.json" as const;
|
|
24
|
+
export const DELIVERY_RECEIPT_VERSION = 3 as const;
|
|
25
|
+
export const GATE_A_RECEIPT_PATH = "gate-a.yaml" as const;
|
|
26
|
+
export const READY_RECEIPT_PATH = "ready.yaml" as const;
|
|
27
|
+
|
|
28
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
29
|
+
const CHANGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,126}[a-z0-9])?$/u;
|
|
30
|
+
const AGENTS_START = "<!-- ABEL:AGENTS-INDEX:START -->";
|
|
31
|
+
const AGENTS_END = "<!-- ABEL:AGENTS-INDEX:END -->";
|
|
32
|
+
|
|
33
|
+
export interface DeliveryArtifactBinding {
|
|
34
|
+
path: string;
|
|
35
|
+
rawSha256: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GateAReceipt {
|
|
39
|
+
receiptVersion: typeof DELIVERY_RECEIPT_VERSION;
|
|
40
|
+
change: string;
|
|
41
|
+
schema: string;
|
|
42
|
+
approval: {
|
|
43
|
+
approved: true;
|
|
44
|
+
blockingDecisions: 0;
|
|
45
|
+
};
|
|
46
|
+
artifacts: DeliveryArtifactBinding[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DeliveryTraceability {
|
|
50
|
+
taskPath: "tasks.md";
|
|
51
|
+
referenceCount: number;
|
|
52
|
+
taskCount: number;
|
|
53
|
+
verificationCount: number;
|
|
54
|
+
canonicalHash: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ReadyReceipt {
|
|
58
|
+
receiptVersion: typeof DELIVERY_RECEIPT_VERSION;
|
|
59
|
+
change: string;
|
|
60
|
+
schema: string;
|
|
61
|
+
deliveryRevision: number;
|
|
62
|
+
approvals: {
|
|
63
|
+
gateA: {
|
|
64
|
+
path: typeof GATE_A_RECEIPT_PATH;
|
|
65
|
+
rawSha256: string;
|
|
66
|
+
};
|
|
67
|
+
gateB: {
|
|
68
|
+
approved: true;
|
|
69
|
+
blockingDecisions: 0;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
artifacts: DeliveryArtifactBinding[];
|
|
73
|
+
plan: CompiledDelivery["receipt"]["plan"];
|
|
74
|
+
verificationClosure: ImplementGraphReadiness["closure"];
|
|
75
|
+
traceability: DeliveryTraceability;
|
|
76
|
+
openspec: {
|
|
77
|
+
strict: true;
|
|
78
|
+
planningComplete: true;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type DeliveryTraceabilityAssessment =
|
|
83
|
+
| { ok: true; value: DeliveryTraceability }
|
|
84
|
+
| { ok: false; diagnostics: string[] };
|
|
85
|
+
|
|
86
|
+
export class DeliveryValidationError extends Error {
|
|
87
|
+
readonly diagnostics: readonly string[];
|
|
88
|
+
|
|
89
|
+
constructor(diagnostics: readonly string[]) {
|
|
90
|
+
const normalized = [...new Set(diagnostics)].sort();
|
|
91
|
+
super("delivery-invalid");
|
|
92
|
+
this.name = "DeliveryValidationError";
|
|
93
|
+
this.diagnostics = Object.freeze(normalized);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function assessDeliveryTraceability(input: {
|
|
98
|
+
tasksMarkdown: string;
|
|
99
|
+
specs: readonly { path: string; text: string }[];
|
|
100
|
+
plan: ImplementPlan;
|
|
101
|
+
}): DeliveryTraceabilityAssessment {
|
|
102
|
+
const diagnostics = new Set<string>();
|
|
103
|
+
const headings = new Set<string>();
|
|
104
|
+
for (const spec of input.specs) {
|
|
105
|
+
if (!isValidRelativePath(spec.path) || !spec.path.endsWith("/spec.md")) {
|
|
106
|
+
diagnostics.add("traceability-spec-path-invalid");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
let requirement = "";
|
|
110
|
+
for (const line of spec.text.split(/\r?\n/u)) {
|
|
111
|
+
const requirementMatch = /^### Requirement: (.+)$/u.exec(line);
|
|
112
|
+
if (requirementMatch?.[1]) {
|
|
113
|
+
requirement = requirementMatch[1].trim();
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const scenarioMatch = /^#### Scenario: (.+)$/u.exec(line);
|
|
117
|
+
if (scenarioMatch?.[1] && requirement) {
|
|
118
|
+
const heading = `${spec.path}#${requirement}/${scenarioMatch[1].trim()}`;
|
|
119
|
+
if (headings.has(heading)) {
|
|
120
|
+
diagnostics.add("traceability-heading-duplicate");
|
|
121
|
+
}
|
|
122
|
+
headings.add(heading);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const references = [
|
|
127
|
+
...input.tasksMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
|
|
128
|
+
].flatMap((match) => (match[1] ? [match[1].trim()] : []));
|
|
129
|
+
if (references.length === 0) diagnostics.add("traceability-reference-empty");
|
|
130
|
+
const referenceSet = new Set(references);
|
|
131
|
+
if (referenceSet.size !== references.length) {
|
|
132
|
+
diagnostics.add("traceability-reference-duplicate");
|
|
133
|
+
}
|
|
134
|
+
if (references.some((reference) => !headings.has(reference))) {
|
|
135
|
+
diagnostics.add("traceability-reference-unresolved");
|
|
136
|
+
}
|
|
137
|
+
if ([...headings].some((heading) => !referenceSet.has(heading))) {
|
|
138
|
+
diagnostics.add("traceability-scenario-unowned");
|
|
139
|
+
}
|
|
140
|
+
const taskIds = input.plan.tasks.map((task) => task.taskId).sort();
|
|
141
|
+
if (
|
|
142
|
+
taskIds.some((taskId) => !input.tasksMarkdown.includes(`\`${taskId}\``))
|
|
143
|
+
) {
|
|
144
|
+
diagnostics.add("traceability-task-unmapped");
|
|
145
|
+
}
|
|
146
|
+
const verificationIds = input.plan.tasks
|
|
147
|
+
.flatMap((task) => [
|
|
148
|
+
task.phases.red.verification.id,
|
|
149
|
+
task.phases.green.verification.id,
|
|
150
|
+
...(task.phases.refactor ? [task.phases.refactor.verification.id] : []),
|
|
151
|
+
])
|
|
152
|
+
.sort();
|
|
153
|
+
if (
|
|
154
|
+
verificationIds.some(
|
|
155
|
+
(verificationId) => !input.tasksMarkdown.includes(verificationId),
|
|
156
|
+
)
|
|
157
|
+
) {
|
|
158
|
+
diagnostics.add("traceability-verification-unmapped");
|
|
159
|
+
}
|
|
160
|
+
if (diagnostics.size > 0) {
|
|
161
|
+
return { ok: false, diagnostics: [...diagnostics].sort() };
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
value: {
|
|
166
|
+
taskPath: "tasks.md",
|
|
167
|
+
referenceCount: references.length,
|
|
168
|
+
taskCount: taskIds.length,
|
|
169
|
+
verificationCount: verificationIds.length,
|
|
170
|
+
canonicalHash: hashCanonicalValue({
|
|
171
|
+
references: [...referenceSet].sort(),
|
|
172
|
+
taskIds,
|
|
173
|
+
verificationIds,
|
|
174
|
+
}),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type PlanPhaseDraft = PhaseBoundary;
|
|
180
|
+
|
|
181
|
+
export interface PlanTaskDraft extends Omit<ImplementTaskBoundary, "phases"> {
|
|
182
|
+
phases: {
|
|
183
|
+
red: PlanPhaseDraft;
|
|
184
|
+
green: PlanPhaseDraft;
|
|
185
|
+
refactor?: PlanPhaseDraft;
|
|
186
|
+
};
|
|
187
|
+
affectedVerification: StructuredVerificationContract;
|
|
188
|
+
repairVerification: StructuredVerificationContract;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface PlanVerification {
|
|
192
|
+
baseline: {
|
|
193
|
+
target: "task-red-contracts";
|
|
194
|
+
affected: "task-affected-contracts";
|
|
195
|
+
fullSuite: StructuredVerificationContract;
|
|
196
|
+
failureIdentity: "normalized-v1";
|
|
197
|
+
};
|
|
198
|
+
change: {
|
|
199
|
+
affected: "task-affected-contracts";
|
|
200
|
+
fullSuite: StructuredVerificationContract;
|
|
201
|
+
postApply: StructuredVerificationContract;
|
|
202
|
+
};
|
|
203
|
+
artifactCorrection: {
|
|
204
|
+
maxAttempts: number;
|
|
205
|
+
};
|
|
206
|
+
repair: {
|
|
207
|
+
maxAttempts: number;
|
|
208
|
+
inBoundaryOnly: true;
|
|
209
|
+
approvalOnBoundaryExpansion: true;
|
|
210
|
+
attribution: ["pre-existing", "introduced", "unresolved", "environment"];
|
|
211
|
+
};
|
|
212
|
+
agentsCheckpoint: {
|
|
213
|
+
required: boolean;
|
|
214
|
+
verification: StructuredVerificationContract | null;
|
|
215
|
+
operations: PlanAgentsCheckpointOperation[];
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface PlanAgentsCheckpointOperation {
|
|
220
|
+
target: string;
|
|
221
|
+
impact: Exclude<AgentsImpact, "none">;
|
|
222
|
+
taskIds: string[];
|
|
223
|
+
managedBlock: string | null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface PlanTracking {
|
|
227
|
+
path: "tasks.md";
|
|
228
|
+
format: "markdown-checkbox";
|
|
229
|
+
taskIds: string[];
|
|
230
|
+
completionOwner: "parent";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface PlanDraft {
|
|
234
|
+
schemaVersion: typeof IMPLEMENT_PLAN_SCHEMA_VERSION;
|
|
235
|
+
changeId: string;
|
|
236
|
+
tasks: PlanTaskDraft[];
|
|
237
|
+
outputs: ImplementGraphOutput[];
|
|
238
|
+
verification: PlanVerification;
|
|
239
|
+
tracking: PlanTracking;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface ImplementPlan extends PlanDraft {
|
|
243
|
+
schemaVersion: typeof IMPLEMENT_PLAN_SCHEMA_VERSION;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface CompiledDelivery {
|
|
247
|
+
plan: ImplementPlan;
|
|
248
|
+
bytes: Uint8Array;
|
|
249
|
+
rawSha256: string;
|
|
250
|
+
planHash: string;
|
|
251
|
+
closure: ImplementGraphReadiness["closure"];
|
|
252
|
+
receipt: {
|
|
253
|
+
plan: {
|
|
254
|
+
path: typeof IMPLEMENT_PLAN_PATH;
|
|
255
|
+
schemaVersion: typeof IMPLEMENT_PLAN_SCHEMA_VERSION;
|
|
256
|
+
rawSha256: string;
|
|
257
|
+
canonicalHash: string;
|
|
258
|
+
};
|
|
259
|
+
verificationClosure: ImplementGraphReadiness["closure"];
|
|
260
|
+
};
|
|
261
|
+
tasksMarkdown: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
265
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function exactKeys(
|
|
269
|
+
value: Record<string, unknown>,
|
|
270
|
+
keys: readonly string[],
|
|
271
|
+
): boolean {
|
|
272
|
+
const actual = Object.keys(value);
|
|
273
|
+
return (
|
|
274
|
+
actual.length === keys.length && actual.every((key) => keys.includes(key))
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function canonicalBytes(value: unknown): Uint8Array {
|
|
279
|
+
return new TextEncoder().encode(`${canonicalJson(value)}\n`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeArtifactBindings(
|
|
283
|
+
value: readonly DeliveryArtifactBinding[],
|
|
284
|
+
): DeliveryArtifactBinding[] {
|
|
285
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
286
|
+
throw new Error("delivery-artifacts-invalid");
|
|
287
|
+
}
|
|
288
|
+
const paths = new Set<string>();
|
|
289
|
+
const normalized = value.map((binding) => {
|
|
290
|
+
if (
|
|
291
|
+
!isRecord(binding) ||
|
|
292
|
+
!exactKeys(binding, ["path", "rawSha256"]) ||
|
|
293
|
+
typeof binding.path !== "string" ||
|
|
294
|
+
!isValidRelativePath(binding.path) ||
|
|
295
|
+
binding.path === READY_RECEIPT_PATH ||
|
|
296
|
+
typeof binding.rawSha256 !== "string" ||
|
|
297
|
+
!SHA256.test(binding.rawSha256) ||
|
|
298
|
+
paths.has(binding.path)
|
|
299
|
+
) {
|
|
300
|
+
throw new Error("delivery-artifacts-invalid");
|
|
301
|
+
}
|
|
302
|
+
paths.add(binding.path);
|
|
303
|
+
return { path: binding.path, rawSha256: binding.rawSha256 };
|
|
304
|
+
});
|
|
305
|
+
return normalized.sort((left, right) => left.path.localeCompare(right.path));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function requireReceiptIdentity(
|
|
309
|
+
change: unknown,
|
|
310
|
+
schema: unknown,
|
|
311
|
+
): asserts change is string {
|
|
312
|
+
if (
|
|
313
|
+
typeof change !== "string" ||
|
|
314
|
+
!CHANGE_NAME.test(change) ||
|
|
315
|
+
typeof schema !== "string" ||
|
|
316
|
+
schema.length === 0 ||
|
|
317
|
+
schema.length > 128
|
|
318
|
+
) {
|
|
319
|
+
throw new Error("delivery-receipt-identity-invalid");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
|
|
324
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
325
|
+
let parsed: unknown;
|
|
326
|
+
try {
|
|
327
|
+
parsed = JSON.parse(text);
|
|
328
|
+
} catch {
|
|
329
|
+
throw new Error(code);
|
|
330
|
+
}
|
|
331
|
+
if (`${canonicalJson(parsed)}\n` !== text) throw new Error(code);
|
|
332
|
+
return parsed as T;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function compileGateAReceipt(input: {
|
|
336
|
+
change: string;
|
|
337
|
+
schema: string;
|
|
338
|
+
artifacts: readonly DeliveryArtifactBinding[];
|
|
339
|
+
}): { receipt: GateAReceipt; bytes: Uint8Array; rawSha256: string } {
|
|
340
|
+
requireReceiptIdentity(input.change, input.schema);
|
|
341
|
+
const receipt: GateAReceipt = {
|
|
342
|
+
receiptVersion: DELIVERY_RECEIPT_VERSION,
|
|
343
|
+
change: input.change,
|
|
344
|
+
schema: input.schema,
|
|
345
|
+
approval: { approved: true, blockingDecisions: 0 },
|
|
346
|
+
artifacts: normalizeArtifactBindings(input.artifacts),
|
|
347
|
+
};
|
|
348
|
+
const bytes = canonicalBytes(receipt);
|
|
349
|
+
return {
|
|
350
|
+
receipt,
|
|
351
|
+
bytes,
|
|
352
|
+
rawSha256: createHash("sha256").update(bytes).digest("hex"),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
|
|
357
|
+
const receipt = parseCanonicalReceipt<GateAReceipt>(
|
|
358
|
+
bytes,
|
|
359
|
+
"gate-a-receipt-invalid",
|
|
360
|
+
);
|
|
361
|
+
if (
|
|
362
|
+
!isRecord(receipt) ||
|
|
363
|
+
!exactKeys(receipt, [
|
|
364
|
+
"receiptVersion",
|
|
365
|
+
"change",
|
|
366
|
+
"schema",
|
|
367
|
+
"approval",
|
|
368
|
+
"artifacts",
|
|
369
|
+
]) ||
|
|
370
|
+
receipt.receiptVersion !== DELIVERY_RECEIPT_VERSION ||
|
|
371
|
+
!isRecord(receipt.approval) ||
|
|
372
|
+
!exactKeys(receipt.approval, ["approved", "blockingDecisions"]) ||
|
|
373
|
+
receipt.approval.approved !== true ||
|
|
374
|
+
receipt.approval.blockingDecisions !== 0
|
|
375
|
+
) {
|
|
376
|
+
throw new Error("gate-a-receipt-invalid");
|
|
377
|
+
}
|
|
378
|
+
requireReceiptIdentity(receipt.change, receipt.schema);
|
|
379
|
+
const artifacts = normalizeArtifactBindings(receipt.artifacts);
|
|
380
|
+
if (canonicalJson(artifacts) !== canonicalJson(receipt.artifacts)) {
|
|
381
|
+
throw new Error("gate-a-receipt-invalid");
|
|
382
|
+
}
|
|
383
|
+
return structuredClone(receipt);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function normalizeTraceability(
|
|
387
|
+
value: DeliveryTraceability,
|
|
388
|
+
): DeliveryTraceability {
|
|
389
|
+
if (
|
|
390
|
+
!isRecord(value) ||
|
|
391
|
+
!exactKeys(value, [
|
|
392
|
+
"taskPath",
|
|
393
|
+
"referenceCount",
|
|
394
|
+
"taskCount",
|
|
395
|
+
"verificationCount",
|
|
396
|
+
"canonicalHash",
|
|
397
|
+
]) ||
|
|
398
|
+
value.taskPath !== "tasks.md" ||
|
|
399
|
+
!Number.isSafeInteger(value.referenceCount) ||
|
|
400
|
+
value.referenceCount < 1 ||
|
|
401
|
+
!Number.isSafeInteger(value.taskCount) ||
|
|
402
|
+
value.taskCount < 1 ||
|
|
403
|
+
!Number.isSafeInteger(value.verificationCount) ||
|
|
404
|
+
value.verificationCount < 1 ||
|
|
405
|
+
typeof value.canonicalHash !== "string" ||
|
|
406
|
+
!SHA256.test(value.canonicalHash)
|
|
407
|
+
) {
|
|
408
|
+
throw new Error("delivery-traceability-invalid");
|
|
409
|
+
}
|
|
410
|
+
return structuredClone(value);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export function compileReadyReceipt(input: {
|
|
414
|
+
change: string;
|
|
415
|
+
schema: string;
|
|
416
|
+
deliveryRevision: number;
|
|
417
|
+
gateA: { rawSha256: string };
|
|
418
|
+
artifacts: readonly DeliveryArtifactBinding[];
|
|
419
|
+
compiledPlan: CompiledDelivery;
|
|
420
|
+
traceability: DeliveryTraceability;
|
|
421
|
+
}): { receipt: ReadyReceipt; bytes: Uint8Array; rawSha256: string } {
|
|
422
|
+
requireReceiptIdentity(input.change, input.schema);
|
|
423
|
+
if (
|
|
424
|
+
!Number.isSafeInteger(input.deliveryRevision) ||
|
|
425
|
+
input.deliveryRevision < 1 ||
|
|
426
|
+
!SHA256.test(input.gateA.rawSha256) ||
|
|
427
|
+
input.compiledPlan.plan.changeId !== input.change
|
|
428
|
+
) {
|
|
429
|
+
throw new Error("delivery-receipt-invalid");
|
|
430
|
+
}
|
|
431
|
+
const receipt: ReadyReceipt = {
|
|
432
|
+
receiptVersion: DELIVERY_RECEIPT_VERSION,
|
|
433
|
+
change: input.change,
|
|
434
|
+
schema: input.schema,
|
|
435
|
+
deliveryRevision: input.deliveryRevision,
|
|
436
|
+
approvals: {
|
|
437
|
+
gateA: {
|
|
438
|
+
path: GATE_A_RECEIPT_PATH,
|
|
439
|
+
rawSha256: input.gateA.rawSha256,
|
|
440
|
+
},
|
|
441
|
+
gateB: { approved: true, blockingDecisions: 0 },
|
|
442
|
+
},
|
|
443
|
+
artifacts: normalizeArtifactBindings(input.artifacts),
|
|
444
|
+
plan: structuredClone(input.compiledPlan.receipt.plan),
|
|
445
|
+
verificationClosure: structuredClone(
|
|
446
|
+
input.compiledPlan.receipt.verificationClosure,
|
|
447
|
+
),
|
|
448
|
+
traceability: normalizeTraceability(input.traceability),
|
|
449
|
+
openspec: { strict: true, planningComplete: true },
|
|
450
|
+
};
|
|
451
|
+
const bytes = canonicalBytes(receipt);
|
|
452
|
+
return {
|
|
453
|
+
receipt,
|
|
454
|
+
bytes,
|
|
455
|
+
rawSha256: createHash("sha256").update(bytes).digest("hex"),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
|
|
460
|
+
const receipt = parseCanonicalReceipt<ReadyReceipt>(
|
|
461
|
+
bytes,
|
|
462
|
+
"delivery-receipt-invalid",
|
|
463
|
+
);
|
|
464
|
+
if (
|
|
465
|
+
!isRecord(receipt) ||
|
|
466
|
+
!exactKeys(receipt, [
|
|
467
|
+
"receiptVersion",
|
|
468
|
+
"change",
|
|
469
|
+
"schema",
|
|
470
|
+
"deliveryRevision",
|
|
471
|
+
"approvals",
|
|
472
|
+
"artifacts",
|
|
473
|
+
"plan",
|
|
474
|
+
"verificationClosure",
|
|
475
|
+
"traceability",
|
|
476
|
+
"openspec",
|
|
477
|
+
]) ||
|
|
478
|
+
receipt.receiptVersion !== DELIVERY_RECEIPT_VERSION ||
|
|
479
|
+
!Number.isSafeInteger(receipt.deliveryRevision) ||
|
|
480
|
+
receipt.deliveryRevision < 1 ||
|
|
481
|
+
!isRecord(receipt.approvals) ||
|
|
482
|
+
!exactKeys(receipt.approvals, ["gateA", "gateB"]) ||
|
|
483
|
+
!isRecord(receipt.approvals.gateA) ||
|
|
484
|
+
!exactKeys(receipt.approvals.gateA, ["path", "rawSha256"]) ||
|
|
485
|
+
receipt.approvals.gateA.path !== GATE_A_RECEIPT_PATH ||
|
|
486
|
+
!SHA256.test(receipt.approvals.gateA.rawSha256) ||
|
|
487
|
+
!isRecord(receipt.approvals.gateB) ||
|
|
488
|
+
!exactKeys(receipt.approvals.gateB, ["approved", "blockingDecisions"]) ||
|
|
489
|
+
receipt.approvals.gateB.approved !== true ||
|
|
490
|
+
receipt.approvals.gateB.blockingDecisions !== 0 ||
|
|
491
|
+
!isRecord(receipt.plan) ||
|
|
492
|
+
!exactKeys(receipt.plan, [
|
|
493
|
+
"path",
|
|
494
|
+
"schemaVersion",
|
|
495
|
+
"rawSha256",
|
|
496
|
+
"canonicalHash",
|
|
497
|
+
]) ||
|
|
498
|
+
receipt.plan.path !== IMPLEMENT_PLAN_PATH ||
|
|
499
|
+
receipt.plan.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
|
|
500
|
+
!SHA256.test(receipt.plan.rawSha256) ||
|
|
501
|
+
!SHA256.test(receipt.plan.canonicalHash) ||
|
|
502
|
+
!isRecord(receipt.verificationClosure) ||
|
|
503
|
+
!exactKeys(receipt.verificationClosure, ["executable", "diagnostics"]) ||
|
|
504
|
+
receipt.verificationClosure.executable !== true ||
|
|
505
|
+
!Array.isArray(receipt.verificationClosure.diagnostics) ||
|
|
506
|
+
receipt.verificationClosure.diagnostics.length !== 0 ||
|
|
507
|
+
!isRecord(receipt.openspec) ||
|
|
508
|
+
!exactKeys(receipt.openspec, ["strict", "planningComplete"]) ||
|
|
509
|
+
receipt.openspec.strict !== true ||
|
|
510
|
+
receipt.openspec.planningComplete !== true
|
|
511
|
+
) {
|
|
512
|
+
throw new Error("delivery-receipt-invalid");
|
|
513
|
+
}
|
|
514
|
+
requireReceiptIdentity(receipt.change, receipt.schema);
|
|
515
|
+
const artifacts = normalizeArtifactBindings(receipt.artifacts);
|
|
516
|
+
const traceability = normalizeTraceability(receipt.traceability);
|
|
517
|
+
if (
|
|
518
|
+
canonicalJson(artifacts) !== canonicalJson(receipt.artifacts) ||
|
|
519
|
+
canonicalJson(traceability) !== canonicalJson(receipt.traceability)
|
|
520
|
+
) {
|
|
521
|
+
throw new Error("delivery-receipt-invalid");
|
|
522
|
+
}
|
|
523
|
+
return structuredClone(receipt);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function sortStrings(values: readonly string[]): string[] {
|
|
527
|
+
return [...values].sort((left, right) => left.localeCompare(right));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function sortCanonical<T>(values: readonly T[]): T[] {
|
|
531
|
+
return [...values].sort((left, right) =>
|
|
532
|
+
canonicalJson(left).localeCompare(canonicalJson(right)),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function normalizeVerification(
|
|
537
|
+
verification: unknown,
|
|
538
|
+
): StructuredVerificationContract {
|
|
539
|
+
const validation = validateVerificationContract(verification);
|
|
540
|
+
if (!validation.ok) throw new Error("delivery-verification-invalid");
|
|
541
|
+
const cloned = validation.value;
|
|
542
|
+
if (cloned.kind === "vitest") {
|
|
543
|
+
cloned.testFiles = sortStrings(cloned.testFiles);
|
|
544
|
+
} else if (cloned.kind === "steps") {
|
|
545
|
+
cloned.steps = cloned.steps.map((step) => {
|
|
546
|
+
if (step.kind !== "vitest") return step;
|
|
547
|
+
return { ...step, testFiles: sortStrings(step.testFiles) };
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return cloned;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function normalizePhase(value: unknown): PlanPhaseDraft {
|
|
554
|
+
if (
|
|
555
|
+
!isRecord(value) ||
|
|
556
|
+
!exactKeys(value, [
|
|
557
|
+
"read",
|
|
558
|
+
"write",
|
|
559
|
+
"delete",
|
|
560
|
+
"verification",
|
|
561
|
+
"verificationInputs",
|
|
562
|
+
...(Object.hasOwn(value, "verificationLock") ? ["verificationLock"] : []),
|
|
563
|
+
]) ||
|
|
564
|
+
!Array.isArray(value.read) ||
|
|
565
|
+
!Array.isArray(value.write) ||
|
|
566
|
+
!Array.isArray(value.delete) ||
|
|
567
|
+
!Array.isArray(value.verificationInputs) ||
|
|
568
|
+
!value.read.every(isValidRelativePath) ||
|
|
569
|
+
!value.write.every(isValidRelativePath) ||
|
|
570
|
+
!value.delete.every(isValidRelativePath)
|
|
571
|
+
) {
|
|
572
|
+
throw new Error("delivery-plan-invalid-phase");
|
|
573
|
+
}
|
|
574
|
+
const write = sortStrings(value.write as string[]);
|
|
575
|
+
const deletions = sortStrings(value.delete as string[]);
|
|
576
|
+
if (write.some((candidate) => deletions.includes(candidate))) {
|
|
577
|
+
throw new Error("delivery-plan-write-delete-overlap");
|
|
578
|
+
}
|
|
579
|
+
return {
|
|
580
|
+
read: sortStrings(value.read as string[]),
|
|
581
|
+
write,
|
|
582
|
+
delete: deletions,
|
|
583
|
+
verification: normalizeVerification(
|
|
584
|
+
value.verification as StructuredVerificationContract,
|
|
585
|
+
),
|
|
586
|
+
verificationInputs: sortCanonical(
|
|
587
|
+
structuredClone(
|
|
588
|
+
value.verificationInputs,
|
|
589
|
+
) as PhaseBoundary["verificationInputs"],
|
|
590
|
+
),
|
|
591
|
+
...(typeof value.verificationLock === "string"
|
|
592
|
+
? { verificationLock: value.verificationLock }
|
|
593
|
+
: {}),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function normalizeTask(value: unknown): PlanTaskDraft {
|
|
598
|
+
if (
|
|
599
|
+
!isRecord(value) ||
|
|
600
|
+
!isRecord(value.phases) ||
|
|
601
|
+
!isRecord(value.affectedVerification) ||
|
|
602
|
+
!isRecord(value.repairVerification)
|
|
603
|
+
) {
|
|
604
|
+
throw new Error("delivery-plan-invalid-task");
|
|
605
|
+
}
|
|
606
|
+
const phases = value.phases;
|
|
607
|
+
if (!Object.hasOwn(phases, "red") || !Object.hasOwn(phases, "green")) {
|
|
608
|
+
throw new Error("delivery-plan-invalid-task-phases");
|
|
609
|
+
}
|
|
610
|
+
const cloned = structuredClone(value) as unknown as PlanTaskDraft;
|
|
611
|
+
const affectedVerification = normalizeVerification(
|
|
612
|
+
value.affectedVerification,
|
|
613
|
+
);
|
|
614
|
+
const repairVerification = normalizeVerification(value.repairVerification);
|
|
615
|
+
if (
|
|
616
|
+
affectedVerification.classification !== "expected-green" ||
|
|
617
|
+
repairVerification.classification !== "expected-green"
|
|
618
|
+
) {
|
|
619
|
+
throw new Error("delivery-task-verification-invalid");
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
...cloned,
|
|
623
|
+
dependsOn: sortStrings(cloned.dependsOn),
|
|
624
|
+
roots: sortStrings(cloned.roots),
|
|
625
|
+
phases: {
|
|
626
|
+
red: normalizePhase(phases.red),
|
|
627
|
+
green: normalizePhase(phases.green),
|
|
628
|
+
...(phases.refactor !== undefined
|
|
629
|
+
? { refactor: normalizePhase(phases.refactor) }
|
|
630
|
+
: {}),
|
|
631
|
+
},
|
|
632
|
+
scheduling: {
|
|
633
|
+
conflicts: sortStrings(cloned.scheduling.conflicts),
|
|
634
|
+
resources: sortStrings(cloned.scheduling.resources),
|
|
635
|
+
},
|
|
636
|
+
approvedDependencies: sortStrings(cloned.approvedDependencies),
|
|
637
|
+
affectedVerification,
|
|
638
|
+
repairVerification,
|
|
639
|
+
impactClosure: {
|
|
640
|
+
changedSurfaces: sortStrings(
|
|
641
|
+
cloned.impactClosure.changedSurfaces,
|
|
642
|
+
) as PlanTaskDraft["impactClosure"]["changedSurfaces"],
|
|
643
|
+
searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
|
|
644
|
+
relatedTests: [...cloned.impactClosure.relatedTests].sort(
|
|
645
|
+
(left, right) =>
|
|
646
|
+
left.path.localeCompare(right.path) ||
|
|
647
|
+
left.disposition.localeCompare(right.disposition),
|
|
648
|
+
),
|
|
649
|
+
affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function graphFromPlan(plan: ImplementPlan): ImplementGraphBoundary {
|
|
655
|
+
const stripPhase = (phase: PlanPhaseDraft): PhaseBoundary => ({
|
|
656
|
+
read: phase.read,
|
|
657
|
+
write: phase.write,
|
|
658
|
+
delete: phase.delete,
|
|
659
|
+
verification: phase.verification,
|
|
660
|
+
verificationInputs: phase.verificationInputs,
|
|
661
|
+
...(phase.verificationLock
|
|
662
|
+
? { verificationLock: phase.verificationLock }
|
|
663
|
+
: {}),
|
|
664
|
+
});
|
|
665
|
+
return {
|
|
666
|
+
changeId: plan.changeId,
|
|
667
|
+
tasks: plan.tasks.map((task) => {
|
|
668
|
+
const {
|
|
669
|
+
affectedVerification: _affectedVerification,
|
|
670
|
+
repairVerification: _repairVerification,
|
|
671
|
+
...boundary
|
|
672
|
+
} = task;
|
|
673
|
+
return {
|
|
674
|
+
...boundary,
|
|
675
|
+
phases: {
|
|
676
|
+
red: stripPhase(task.phases.red),
|
|
677
|
+
green: stripPhase(task.phases.green),
|
|
678
|
+
...(task.phases.refactor
|
|
679
|
+
? { refactor: stripPhase(task.phases.refactor) }
|
|
680
|
+
: {}),
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}),
|
|
684
|
+
outputs: structuredClone(plan.outputs),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function deliveryVerificationDiagnostics(
|
|
689
|
+
plan: ImplementPlan,
|
|
690
|
+
consumerRoot: string,
|
|
691
|
+
): Array<Record<string, unknown>> {
|
|
692
|
+
const contracts: Array<{
|
|
693
|
+
owner: string;
|
|
694
|
+
verification: StructuredVerificationContract;
|
|
695
|
+
}> = plan.tasks.flatMap((task) => [
|
|
696
|
+
{
|
|
697
|
+
owner: `task:${task.taskId}:affected`,
|
|
698
|
+
verification: task.affectedVerification,
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
owner: `task:${task.taskId}:repair`,
|
|
702
|
+
verification: task.repairVerification,
|
|
703
|
+
},
|
|
704
|
+
]);
|
|
705
|
+
contracts.push(
|
|
706
|
+
{
|
|
707
|
+
owner: "baseline:full-suite",
|
|
708
|
+
verification: plan.verification.baseline.fullSuite,
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
owner: "change:full-suite",
|
|
712
|
+
verification: plan.verification.change.fullSuite,
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
owner: "change:post-apply",
|
|
716
|
+
verification: plan.verification.change.postApply,
|
|
717
|
+
},
|
|
718
|
+
);
|
|
719
|
+
if (plan.verification.agentsCheckpoint.verification) {
|
|
720
|
+
contracts.push({
|
|
721
|
+
owner: "agents-checkpoint",
|
|
722
|
+
verification: plan.verification.agentsCheckpoint.verification,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return contracts.flatMap(({ owner, verification }) => {
|
|
726
|
+
const capability = validateVerificationAdapterCapability(
|
|
727
|
+
consumerRoot,
|
|
728
|
+
verification,
|
|
729
|
+
{ dependencyOwner: consumerRoot },
|
|
730
|
+
);
|
|
731
|
+
if (capability.ok) return [];
|
|
732
|
+
const { message: _message, ...diagnostic } = capability.diagnostic;
|
|
733
|
+
return [{ owner, ...diagnostic }];
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function normalizeExpectedGreenVerification(
|
|
738
|
+
value: unknown,
|
|
739
|
+
): StructuredVerificationContract {
|
|
740
|
+
const verification = normalizeVerification(value);
|
|
741
|
+
if (verification.classification !== "expected-green") {
|
|
742
|
+
throw new Error("delivery-verification-classification-invalid");
|
|
743
|
+
}
|
|
744
|
+
return verification;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function normalizeAgentsManagedBlock(value: unknown): string {
|
|
748
|
+
if (
|
|
749
|
+
typeof value !== "string" ||
|
|
750
|
+
value.length === 0 ||
|
|
751
|
+
Buffer.byteLength(value) > 64 * 1024 ||
|
|
752
|
+
value.includes("\0") ||
|
|
753
|
+
value.includes("\r")
|
|
754
|
+
) {
|
|
755
|
+
throw new Error("delivery-agents-checkpoint-invalid");
|
|
756
|
+
}
|
|
757
|
+
const lines = value.endsWith("\n")
|
|
758
|
+
? value.slice(0, -1).split("\n")
|
|
759
|
+
: value.split("\n");
|
|
760
|
+
if (
|
|
761
|
+
lines[0] !== AGENTS_START ||
|
|
762
|
+
lines.at(-1) !== AGENTS_END ||
|
|
763
|
+
lines.filter((line) => line === AGENTS_START).length !== 1 ||
|
|
764
|
+
lines.filter((line) => line === AGENTS_END).length !== 1
|
|
765
|
+
) {
|
|
766
|
+
throw new Error("delivery-agents-checkpoint-invalid");
|
|
767
|
+
}
|
|
768
|
+
return lines.join("\n");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function normalizeAgentsCheckpointOperations(
|
|
772
|
+
value: unknown,
|
|
773
|
+
): PlanAgentsCheckpointOperation[] {
|
|
774
|
+
if (!Array.isArray(value) || value.length > 64) {
|
|
775
|
+
throw new Error("delivery-agents-checkpoint-invalid");
|
|
776
|
+
}
|
|
777
|
+
const targets = new Set<string>();
|
|
778
|
+
return value
|
|
779
|
+
.map((operation): PlanAgentsCheckpointOperation => {
|
|
780
|
+
if (
|
|
781
|
+
!isRecord(operation) ||
|
|
782
|
+
!exactKeys(operation, [
|
|
783
|
+
"target",
|
|
784
|
+
"impact",
|
|
785
|
+
"taskIds",
|
|
786
|
+
"managedBlock",
|
|
787
|
+
]) ||
|
|
788
|
+
!isAgentsPath(operation.target) ||
|
|
789
|
+
!["update-existing", "create-index", "remove-index"].includes(
|
|
790
|
+
String(operation.impact),
|
|
791
|
+
) ||
|
|
792
|
+
targets.has(operation.target) ||
|
|
793
|
+
!Array.isArray(operation.taskIds) ||
|
|
794
|
+
operation.taskIds.length === 0 ||
|
|
795
|
+
operation.taskIds.some(
|
|
796
|
+
(taskId) => typeof taskId !== "string" || taskId.length === 0,
|
|
797
|
+
) ||
|
|
798
|
+
new Set(operation.taskIds).size !== operation.taskIds.length
|
|
799
|
+
) {
|
|
800
|
+
throw new Error("delivery-agents-checkpoint-invalid");
|
|
801
|
+
}
|
|
802
|
+
targets.add(operation.target);
|
|
803
|
+
const impact = operation.impact as Exclude<AgentsImpact, "none">;
|
|
804
|
+
if (
|
|
805
|
+
(impact === "remove-index" && operation.managedBlock !== null) ||
|
|
806
|
+
(impact !== "remove-index" &&
|
|
807
|
+
typeof operation.managedBlock !== "string")
|
|
808
|
+
) {
|
|
809
|
+
throw new Error("delivery-agents-checkpoint-invalid");
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
target: operation.target,
|
|
813
|
+
impact,
|
|
814
|
+
taskIds: sortStrings(operation.taskIds as string[]),
|
|
815
|
+
managedBlock:
|
|
816
|
+
impact === "remove-index"
|
|
817
|
+
? null
|
|
818
|
+
: normalizeAgentsManagedBlock(operation.managedBlock),
|
|
819
|
+
};
|
|
820
|
+
})
|
|
821
|
+
.sort((left, right) => left.target.localeCompare(right.target));
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function normalizeVerificationPlan(value: unknown): PlanVerification {
|
|
825
|
+
if (
|
|
826
|
+
!isRecord(value) ||
|
|
827
|
+
!exactKeys(value, [
|
|
828
|
+
"baseline",
|
|
829
|
+
"change",
|
|
830
|
+
"artifactCorrection",
|
|
831
|
+
"repair",
|
|
832
|
+
"agentsCheckpoint",
|
|
833
|
+
]) ||
|
|
834
|
+
!isRecord(value.baseline) ||
|
|
835
|
+
!exactKeys(value.baseline, [
|
|
836
|
+
"target",
|
|
837
|
+
"affected",
|
|
838
|
+
"fullSuite",
|
|
839
|
+
"failureIdentity",
|
|
840
|
+
]) ||
|
|
841
|
+
value.baseline.target !== "task-red-contracts" ||
|
|
842
|
+
value.baseline.affected !== "task-affected-contracts" ||
|
|
843
|
+
value.baseline.failureIdentity !== "normalized-v1" ||
|
|
844
|
+
!isRecord(value.change) ||
|
|
845
|
+
!exactKeys(value.change, ["affected", "fullSuite", "postApply"]) ||
|
|
846
|
+
value.change.affected !== "task-affected-contracts" ||
|
|
847
|
+
!isRecord(value.artifactCorrection) ||
|
|
848
|
+
!exactKeys(value.artifactCorrection, ["maxAttempts"]) ||
|
|
849
|
+
!Number.isSafeInteger(value.artifactCorrection.maxAttempts) ||
|
|
850
|
+
(value.artifactCorrection.maxAttempts as number) < 2 ||
|
|
851
|
+
(value.artifactCorrection.maxAttempts as number) > 3 ||
|
|
852
|
+
!isRecord(value.repair) ||
|
|
853
|
+
!exactKeys(value.repair, [
|
|
854
|
+
"maxAttempts",
|
|
855
|
+
"inBoundaryOnly",
|
|
856
|
+
"approvalOnBoundaryExpansion",
|
|
857
|
+
"attribution",
|
|
858
|
+
]) ||
|
|
859
|
+
typeof value.repair.maxAttempts !== "number" ||
|
|
860
|
+
!Number.isSafeInteger(value.repair.maxAttempts) ||
|
|
861
|
+
value.repair.maxAttempts < 1 ||
|
|
862
|
+
value.repair.maxAttempts > 3 ||
|
|
863
|
+
value.repair.inBoundaryOnly !== true ||
|
|
864
|
+
value.repair.approvalOnBoundaryExpansion !== true ||
|
|
865
|
+
canonicalJson(value.repair.attribution) !==
|
|
866
|
+
canonicalJson([
|
|
867
|
+
"pre-existing",
|
|
868
|
+
"introduced",
|
|
869
|
+
"unresolved",
|
|
870
|
+
"environment",
|
|
871
|
+
]) ||
|
|
872
|
+
!isRecord(value.agentsCheckpoint) ||
|
|
873
|
+
!exactKeys(value.agentsCheckpoint, [
|
|
874
|
+
"required",
|
|
875
|
+
"verification",
|
|
876
|
+
"operations",
|
|
877
|
+
]) ||
|
|
878
|
+
typeof value.agentsCheckpoint.required !== "boolean" ||
|
|
879
|
+
(value.agentsCheckpoint.required &&
|
|
880
|
+
!isRecord(value.agentsCheckpoint.verification)) ||
|
|
881
|
+
(!value.agentsCheckpoint.required &&
|
|
882
|
+
value.agentsCheckpoint.verification !== null) ||
|
|
883
|
+
!Array.isArray(value.agentsCheckpoint.operations)
|
|
884
|
+
) {
|
|
885
|
+
throw new Error("delivery-verification-plan-invalid");
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
baseline: {
|
|
889
|
+
target: "task-red-contracts",
|
|
890
|
+
affected: "task-affected-contracts",
|
|
891
|
+
fullSuite: normalizeExpectedGreenVerification(value.baseline.fullSuite),
|
|
892
|
+
failureIdentity: "normalized-v1",
|
|
893
|
+
},
|
|
894
|
+
change: {
|
|
895
|
+
affected: "task-affected-contracts",
|
|
896
|
+
fullSuite: normalizeExpectedGreenVerification(value.change.fullSuite),
|
|
897
|
+
postApply: normalizeExpectedGreenVerification(value.change.postApply),
|
|
898
|
+
},
|
|
899
|
+
artifactCorrection: {
|
|
900
|
+
maxAttempts: value.artifactCorrection.maxAttempts as number,
|
|
901
|
+
},
|
|
902
|
+
repair: {
|
|
903
|
+
maxAttempts: value.repair.maxAttempts as number,
|
|
904
|
+
inBoundaryOnly: true,
|
|
905
|
+
approvalOnBoundaryExpansion: true,
|
|
906
|
+
attribution: ["pre-existing", "introduced", "unresolved", "environment"],
|
|
907
|
+
},
|
|
908
|
+
agentsCheckpoint: {
|
|
909
|
+
required: value.agentsCheckpoint.required,
|
|
910
|
+
verification: value.agentsCheckpoint.required
|
|
911
|
+
? normalizeExpectedGreenVerification(
|
|
912
|
+
value.agentsCheckpoint.verification,
|
|
913
|
+
)
|
|
914
|
+
: null,
|
|
915
|
+
operations: normalizeAgentsCheckpointOperations(
|
|
916
|
+
value.agentsCheckpoint.operations,
|
|
917
|
+
),
|
|
918
|
+
},
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function normalizeTracking(value: unknown): PlanTracking {
|
|
923
|
+
if (
|
|
924
|
+
!isRecord(value) ||
|
|
925
|
+
!exactKeys(value, ["path", "format", "taskIds", "completionOwner"]) ||
|
|
926
|
+
value.path !== "tasks.md" ||
|
|
927
|
+
value.format !== "markdown-checkbox" ||
|
|
928
|
+
!Array.isArray(value.taskIds) ||
|
|
929
|
+
!value.taskIds.every((taskId) => typeof taskId === "string") ||
|
|
930
|
+
new Set(value.taskIds).size !== value.taskIds.length ||
|
|
931
|
+
value.completionOwner !== "parent"
|
|
932
|
+
) {
|
|
933
|
+
throw new Error("delivery-tracking-invalid");
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
path: "tasks.md",
|
|
937
|
+
format: "markdown-checkbox",
|
|
938
|
+
taskIds: sortStrings(value.taskIds as string[]),
|
|
939
|
+
completionOwner: "parent",
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function normalizeDraft(value: unknown): ImplementPlan {
|
|
944
|
+
if (
|
|
945
|
+
!isRecord(value) ||
|
|
946
|
+
!exactKeys(value, [
|
|
947
|
+
"schemaVersion",
|
|
948
|
+
"changeId",
|
|
949
|
+
"tasks",
|
|
950
|
+
"outputs",
|
|
951
|
+
"verification",
|
|
952
|
+
"tracking",
|
|
953
|
+
]) ||
|
|
954
|
+
value.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
|
|
955
|
+
typeof value.changeId !== "string" ||
|
|
956
|
+
!CHANGE_NAME.test(value.changeId) ||
|
|
957
|
+
!Array.isArray(value.tasks) ||
|
|
958
|
+
!Array.isArray(value.outputs)
|
|
959
|
+
) {
|
|
960
|
+
throw new Error("delivery-plan-invalid");
|
|
961
|
+
}
|
|
962
|
+
const plan: ImplementPlan = {
|
|
963
|
+
schemaVersion: IMPLEMENT_PLAN_SCHEMA_VERSION,
|
|
964
|
+
changeId: value.changeId,
|
|
965
|
+
tasks: value.tasks
|
|
966
|
+
.map(normalizeTask)
|
|
967
|
+
.sort((left, right) => left.taskId.localeCompare(right.taskId)),
|
|
968
|
+
outputs: (structuredClone(value.outputs) as ImplementGraphOutput[]).sort(
|
|
969
|
+
(left, right) => left.id.localeCompare(right.id),
|
|
970
|
+
),
|
|
971
|
+
verification: normalizeVerificationPlan(value.verification),
|
|
972
|
+
tracking: normalizeTracking(value.tracking),
|
|
973
|
+
};
|
|
974
|
+
if (
|
|
975
|
+
canonicalJson(plan.tracking.taskIds) !==
|
|
976
|
+
canonicalJson(plan.tasks.map((task) => task.taskId).sort())
|
|
977
|
+
) {
|
|
978
|
+
throw new Error("delivery-tracking-task-mismatch");
|
|
979
|
+
}
|
|
980
|
+
const agentsTasks = plan.tasks.filter(
|
|
981
|
+
(task) => task.agents.impact !== "none" && task.agents.target,
|
|
982
|
+
);
|
|
983
|
+
const agentsByTarget = new Map<string, PlanTaskDraft[]>();
|
|
984
|
+
for (const task of agentsTasks) {
|
|
985
|
+
const target = task.agents.target as string;
|
|
986
|
+
const existing = agentsByTarget.get(target) ?? [];
|
|
987
|
+
existing.push(task);
|
|
988
|
+
agentsByTarget.set(target, existing);
|
|
989
|
+
}
|
|
990
|
+
const operations = plan.verification.agentsCheckpoint.operations;
|
|
991
|
+
const operationsByTarget = new Map(
|
|
992
|
+
operations.map((operation) => [operation.target, operation]),
|
|
993
|
+
);
|
|
994
|
+
const agentsRequired = agentsTasks.length > 0;
|
|
995
|
+
if (
|
|
996
|
+
plan.verification.agentsCheckpoint.required !== agentsRequired ||
|
|
997
|
+
operations.length !== agentsByTarget.size ||
|
|
998
|
+
[...agentsByTarget].some(([target, tasks]) => {
|
|
999
|
+
const operation = operationsByTarget.get(target);
|
|
1000
|
+
return (
|
|
1001
|
+
!operation ||
|
|
1002
|
+
tasks.some((task) => task.agents.impact !== operation.impact) ||
|
|
1003
|
+
canonicalJson(operation.taskIds) !==
|
|
1004
|
+
canonicalJson(tasks.map((task) => task.taskId).sort())
|
|
1005
|
+
);
|
|
1006
|
+
})
|
|
1007
|
+
) {
|
|
1008
|
+
throw new Error("delivery-agents-checkpoint-mismatch");
|
|
1009
|
+
}
|
|
1010
|
+
const graphValidation = validateImplementGraphBoundary(graphFromPlan(plan));
|
|
1011
|
+
if (!graphValidation.ok) {
|
|
1012
|
+
throw new Error(`delivery-plan-invalid:${graphValidation.reason}`);
|
|
1013
|
+
}
|
|
1014
|
+
return plan;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function renderTasks(plan: ImplementPlan): string {
|
|
1018
|
+
const lines = ["# Implementation tasks", ""];
|
|
1019
|
+
for (const task of plan.tasks) {
|
|
1020
|
+
lines.push(`- [ ] ${task.taskId} — ${task.objective}`);
|
|
1021
|
+
lines.push(
|
|
1022
|
+
` - Depends on: ${task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "[]"}`,
|
|
1023
|
+
);
|
|
1024
|
+
lines.push(` - Red verification: ${task.phases.red.verification.id}`);
|
|
1025
|
+
lines.push(` - Green verification: ${task.phases.green.verification.id}`);
|
|
1026
|
+
}
|
|
1027
|
+
lines.push("");
|
|
1028
|
+
return lines.join("\n");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
export function compileImplementPlan(
|
|
1032
|
+
draft: unknown,
|
|
1033
|
+
options: { consumerRoot: string },
|
|
1034
|
+
): CompiledDelivery {
|
|
1035
|
+
const plan = normalizeDraft(draft);
|
|
1036
|
+
const graph = graphFromPlan(plan);
|
|
1037
|
+
const readiness = assessImplementGraphReadiness(options.consumerRoot, graph, {
|
|
1038
|
+
completedTasks: [],
|
|
1039
|
+
blockedTasks: [],
|
|
1040
|
+
appliedPhases: [],
|
|
1041
|
+
});
|
|
1042
|
+
const diagnostics = [
|
|
1043
|
+
...readiness.closure.diagnostics,
|
|
1044
|
+
...deliveryVerificationDiagnostics(plan, options.consumerRoot),
|
|
1045
|
+
];
|
|
1046
|
+
if (diagnostics.length > 0) {
|
|
1047
|
+
throw new Error(
|
|
1048
|
+
`delivery-plan-not-executable:${canonicalJson(diagnostics)}`,
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
const canonical = canonicalJson(plan);
|
|
1052
|
+
const bytes = new TextEncoder().encode(`${canonical}\n`);
|
|
1053
|
+
const rawSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
1054
|
+
const planHash = hashCanonicalValue(plan);
|
|
1055
|
+
const closure = structuredClone(readiness.closure);
|
|
1056
|
+
return {
|
|
1057
|
+
plan,
|
|
1058
|
+
bytes,
|
|
1059
|
+
rawSha256,
|
|
1060
|
+
planHash,
|
|
1061
|
+
closure,
|
|
1062
|
+
receipt: {
|
|
1063
|
+
plan: {
|
|
1064
|
+
path: IMPLEMENT_PLAN_PATH,
|
|
1065
|
+
schemaVersion: IMPLEMENT_PLAN_SCHEMA_VERSION,
|
|
1066
|
+
rawSha256,
|
|
1067
|
+
canonicalHash: planHash,
|
|
1068
|
+
},
|
|
1069
|
+
verificationClosure: structuredClone(closure),
|
|
1070
|
+
},
|
|
1071
|
+
tasksMarkdown: renderTasks(plan),
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
|
|
1076
|
+
const text = new TextDecoder().decode(bytes);
|
|
1077
|
+
let parsed: unknown;
|
|
1078
|
+
try {
|
|
1079
|
+
parsed = JSON.parse(text);
|
|
1080
|
+
} catch {
|
|
1081
|
+
throw new Error("delivery-plan-json-invalid");
|
|
1082
|
+
}
|
|
1083
|
+
const normalized = normalizeDraft(parsed);
|
|
1084
|
+
if (`${canonicalJson(normalized)}\n` !== text) {
|
|
1085
|
+
throw new Error("delivery-plan-not-canonical");
|
|
1086
|
+
}
|
|
1087
|
+
return normalized;
|
|
1088
|
+
}
|