@abelxiaoxing/cadence 1.3.0 → 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.
Files changed (45) hide show
  1. package/README.md +115 -10
  2. package/package.json +5 -3
  3. package/prompts/abel-design.md +71 -17
  4. package/prompts/abel-diagnose.md +10 -1
  5. package/prompts/abel-implement.md +118 -20
  6. package/prompts/abel-init.md +9 -2
  7. package/src/apply-transaction.ts +16 -14
  8. package/src/artifact-store.ts +17 -0
  9. package/src/candidate-patch.ts +2 -1
  10. package/src/canonical.ts +16 -0
  11. package/src/change-contract.ts +268 -0
  12. package/src/child-session.ts +4 -3
  13. package/src/contracts.ts +141 -33
  14. package/src/control-contracts.ts +51 -1
  15. package/src/delivery-compiler.ts +148 -31
  16. package/src/design-control.ts +69 -11
  17. package/src/design-journal.ts +216 -72
  18. package/src/durable-workflow.ts +4385 -0
  19. package/src/implement-graph.ts +11 -16
  20. package/src/index.ts +380 -981
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/package-delivery.ts +486 -0
  23. package/src/package-verification.ts +509 -0
  24. package/src/route-policy.ts +1 -11
  25. package/src/run-state.ts +4 -16
  26. package/src/run-store.ts +5 -144
  27. package/src/scoped-grep-worker.mjs +23 -0
  28. package/src/scoped-tools.ts +312 -116
  29. package/src/sqlite-schema.ts +171 -0
  30. package/src/storage-schema.ts +352 -0
  31. package/src/subagent-activity.ts +14 -1
  32. package/src/submit-tool.ts +20 -2
  33. package/src/task-ledger.ts +14 -55
  34. package/src/verification-capability.ts +298 -102
  35. package/src/verification-environment.ts +213 -0
  36. package/src/verification-identity.ts +77 -0
  37. package/src/worker-broker.ts +97 -34
  38. package/src/workflow-engine.ts +28 -8285
  39. package/src/workflow-policy.ts +1427 -0
  40. package/src/workflow-state-machine.ts +4478 -0
  41. package/src/workspace-io-entry.mjs +58 -0
  42. package/src/workspace-io-worker.mjs +984 -0
  43. package/src/workspace-io.ts +84 -0
  44. package/src/workspace-store.ts +125 -19
  45. package/skills/abel-workflow/SKILL.md +0 -205
@@ -15,6 +15,7 @@ export interface IsolationRunInput {
15
15
  mounts?: IsolationMount[];
16
16
  environment?: Record<string, string>;
17
17
  signal?: AbortSignal;
18
+ outputWitness?: string;
18
19
  }
19
20
 
20
21
  export type IsolationRunResult =
@@ -24,6 +25,11 @@ export type IsolationRunResult =
24
25
  exitCode: number;
25
26
  stdout: string;
26
27
  stderr: string;
28
+ outputWitnessMatched?: boolean;
29
+ logs: {
30
+ stdout: { bytes: number; truncated: boolean };
31
+ stderr: { bytes: number; truncated: boolean };
32
+ };
27
33
  }
28
34
  | {
29
35
  ok: false;
@@ -31,14 +37,13 @@ export type IsolationRunResult =
31
37
  code:
32
38
  | "isolation-backend-unavailable"
33
39
  | "isolation-backend-launch-failed"
34
- | "isolation-execution-timeout"
35
- | "isolation-output-limit-exceeded";
40
+ | "isolation-execution-timeout";
36
41
  }
37
42
  | { ok: false; state: "cancelled"; code: "cancelled" };
38
43
 
39
44
  export const ISOLATION_RUN_LIMITS = Object.freeze({
40
45
  timeoutMs: 10 * 60_000,
41
- maxOutputBytes: 1024 * 1024,
46
+ maxOutputBytes: 256 * 1024,
42
47
  terminateGraceMs: 1_000,
43
48
  });
44
49
 
@@ -64,6 +69,59 @@ function validEnvironmentName(name: string): boolean {
64
69
  return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(name);
65
70
  }
66
71
 
72
+ /** Retains independent head/tail copies; discarded chunks never stay referenced. */
73
+ class BoundedLog {
74
+ bytes = 0;
75
+ #head = Buffer.alloc(0);
76
+ #tail = Buffer.alloc(0);
77
+ readonly limit: number;
78
+ readonly #witness?: Buffer;
79
+ #witnessTail = Buffer.alloc(0);
80
+ witnessMatched = false;
81
+ constructor(limit: number, witness?: string) {
82
+ this.limit = limit;
83
+ if (witness) this.#witness = Buffer.from(witness);
84
+ }
85
+ append(chunk: Buffer | string): void {
86
+ const input = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
87
+ this.bytes += input.byteLength;
88
+ if (this.#witness && !this.witnessMatched) {
89
+ const searchable = Buffer.concat([this.#witnessTail, input]);
90
+ this.witnessMatched = searchable.includes(this.#witness);
91
+ this.#witnessTail = this.witnessMatched
92
+ ? Buffer.alloc(0)
93
+ : Buffer.from(
94
+ searchable.subarray(
95
+ Math.max(0, searchable.length - this.#witness.length + 1),
96
+ ),
97
+ );
98
+ }
99
+ const headLimit = Math.ceil(this.limit / 2);
100
+ const take = Math.min(input.length, headLimit - this.#head.length);
101
+ if (take > 0)
102
+ this.#head = Buffer.concat([this.#head, input.subarray(0, take)]);
103
+ const rest = input.subarray(take);
104
+ const tailLimit = this.limit - headLimit;
105
+ if (rest.length && tailLimit) {
106
+ this.#tail =
107
+ rest.length >= tailLimit
108
+ ? Buffer.from(rest.subarray(-tailLimit))
109
+ : Buffer.concat([
110
+ this.#tail.subarray(
111
+ Math.max(0, this.#tail.length + rest.length - tailLimit),
112
+ ),
113
+ rest,
114
+ ]);
115
+ }
116
+ }
117
+ text(): string {
118
+ return Buffer.concat([this.#head, this.#tail]).toString("utf8");
119
+ }
120
+ metadata() {
121
+ return { bytes: this.bytes, truncated: this.bytes > this.limit };
122
+ }
123
+ }
124
+
67
125
  /** Linux isolation capability. Lack of Bubblewrap is a pause, never a fallback. */
68
126
  export class BubblewrapIsolationBackend {
69
127
  readonly #bwrapPath: string;
@@ -103,6 +161,13 @@ export class BubblewrapIsolationBackend {
103
161
  }
104
162
 
105
163
  async run(input: IsolationRunInput): Promise<IsolationRunResult> {
164
+ if (
165
+ input.outputWitness !== undefined &&
166
+ (typeof input.outputWitness !== "string" ||
167
+ input.outputWitness.length < 1 ||
168
+ input.outputWitness.length > 512)
169
+ )
170
+ throw new Error("isolation-output-witness-invalid");
106
171
  if (!(await this.available())) {
107
172
  return {
108
173
  ok: false,
@@ -165,9 +230,8 @@ export class BubblewrapIsolationBackend {
165
230
 
166
231
  return new Promise((resolve) => {
167
232
  let settled = false;
168
- const stdout: Buffer[] = [];
169
- const stderr: Buffer[] = [];
170
- let capturedBytes = 0;
233
+ const stdout = new BoundedLog(this.#maxOutputBytes, input.outputWitness);
234
+ const stderr = new BoundedLog(this.#maxOutputBytes, input.outputWitness);
171
235
  let child: ReturnType<typeof spawn>;
172
236
  let executionTimer: ReturnType<typeof setTimeout> | undefined;
173
237
  let killTimer: ReturnType<typeof setTimeout> | undefined;
@@ -207,21 +271,11 @@ export class BubblewrapIsolationBackend {
207
271
  });
208
272
  return;
209
273
  }
210
- const capture = (destination: Buffer[], chunk: Buffer | string): void => {
211
- if (settled || terminationResult) return;
212
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
213
- const remaining = this.#maxOutputBytes - capturedBytes;
214
- if (remaining > 0) destination.push(bytes.subarray(0, remaining));
215
- if (bytes.byteLength > remaining) {
216
- capturedBytes = this.#maxOutputBytes;
217
- terminate({
218
- ok: false,
219
- state: "paused",
220
- code: "isolation-output-limit-exceeded",
221
- });
222
- return;
223
- }
224
- capturedBytes += bytes.byteLength;
274
+ const capture = (
275
+ destination: BoundedLog,
276
+ chunk: Buffer | string,
277
+ ): void => {
278
+ if (!settled && !terminationResult) destination.append(chunk);
225
279
  };
226
280
  child.stdout?.on("data", (chunk: Buffer | string) => {
227
281
  capture(stdout, chunk);
@@ -244,8 +298,15 @@ export class BubblewrapIsolationBackend {
244
298
  ok: true,
245
299
  state: "completed",
246
300
  exitCode: code ?? 1,
247
- stdout: Buffer.concat(stdout).toString("utf8"),
248
- stderr: Buffer.concat(stderr).toString("utf8"),
301
+ stdout: stdout.text(),
302
+ stderr: stderr.text(),
303
+ logs: { stdout: stdout.metadata(), stderr: stderr.metadata() },
304
+ ...(input.outputWitness
305
+ ? {
306
+ outputWitnessMatched:
307
+ stdout.witnessMatched || stderr.witnessMatched,
308
+ }
309
+ : {}),
249
310
  },
250
311
  );
251
312
  });
@@ -0,0 +1,486 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstatSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { isValidRelativePath } from "./contracts.ts";
5
+ import {
6
+ assessDeliveryTraceability,
7
+ compileImplementPlan,
8
+ DeliveryValidationError,
9
+ type GateApprovalProof,
10
+ parseGateAReceipt,
11
+ parseImplementPlan,
12
+ parseReadyReceipt,
13
+ } from "./delivery-compiler.ts";
14
+ import { canonicalJson, hashCanonicalValue } from "./implement-graph.ts";
15
+ import {
16
+ inspectOpenSpecDelivery,
17
+ type OpenSpecCliOptions,
18
+ type OpenSpecDeliveryInspection,
19
+ } from "./openspec-cli.ts";
20
+ import { observeSafePath } from "./safe-path.ts";
21
+ import type {
22
+ WorkflowAvailableDelivery,
23
+ WorkflowDeliverySource,
24
+ } from "./workflow-engine.ts";
25
+
26
+ function sha256(value: Uint8Array | string): string {
27
+ return createHash("sha256").update(value).digest("hex");
28
+ }
29
+
30
+ export interface PackageDeliverySourceOptions {
31
+ inspectOpenSpec?: (
32
+ consumerRoot: string,
33
+ change: string,
34
+ options?: OpenSpecCliOptions,
35
+ ) => Promise<OpenSpecDeliveryInspection>;
36
+ verifyGateProof?: (input: {
37
+ change: string;
38
+ gate: "gate-a" | "gate-b";
39
+ proof: GateApprovalProof;
40
+ }) => boolean;
41
+ verifyFinalizedDelivery?: (input: {
42
+ change: string;
43
+ deliveryRevision: number;
44
+ receiptHash: string;
45
+ gateA: GateApprovalProof;
46
+ gateB: GateApprovalProof;
47
+ planCanonicalHash: string;
48
+ }) => boolean;
49
+ }
50
+
51
+ export interface PackageWorkflowDeliverySource extends WorkflowDeliverySource {
52
+ discoverLatest(input: {
53
+ stage: "abel-implement";
54
+ change: string;
55
+ }): Promise<WorkflowAvailableDelivery | undefined>;
56
+ }
57
+
58
+ function proofVerified(verify: () => boolean): boolean {
59
+ try {
60
+ return verify() === true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ function normalizedTrackingArtifact(
67
+ bytes: Uint8Array,
68
+ taskIds: readonly string[],
69
+ ): Buffer | undefined {
70
+ let text: string;
71
+ try {
72
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ const identities = taskIds.map((taskId) => ({
77
+ taskId,
78
+ pattern: new RegExp(
79
+ `(?:^|[^A-Za-z0-9._:-])${taskId.replace(
80
+ /[.*+?^${}()|[\]\\]/gu,
81
+ "\\$&",
82
+ )}(?![A-Za-z0-9._:-])`,
83
+ "u",
84
+ ),
85
+ matches: 0,
86
+ }));
87
+ const normalized = text
88
+ .split(/(?<=\n)/u)
89
+ .map((segment) => {
90
+ const line = segment.endsWith("\n") ? segment.slice(0, -1) : segment;
91
+ if (!/^\s*-\s+\[[ xX]\]/u.test(line)) return segment;
92
+ const matching = identities.filter(({ pattern }) => pattern.test(line));
93
+ if (matching.length !== 1) return segment;
94
+ matching[0].matches += 1;
95
+ return segment.replace(/^(\s*-\s+)\[[xX]\]/u, "$1[ ]");
96
+ })
97
+ .join("");
98
+ if (identities.some(({ matches }) => matches !== 1)) return undefined;
99
+ return Buffer.from(normalized, "utf8");
100
+ }
101
+
102
+ function readPackageDeliveryFile(
103
+ root: string,
104
+ relative: string,
105
+ maximumBytes = PACKAGE_DELIVERY_MAX_BYTES,
106
+ ): Buffer {
107
+ const observation = observeSafePath(root, relative);
108
+ if (observation.kind !== "file") {
109
+ throw new Error("delivery-file-unavailable");
110
+ }
111
+ const target = path.join(root, ...relative.split("/"));
112
+ const stat = lstatSync(target);
113
+ if (stat.size < 1 || stat.size > maximumBytes) {
114
+ throw new Error("delivery-file-size-invalid");
115
+ }
116
+ return readFileSync(target);
117
+ }
118
+
119
+ export function packageDeliverySource(
120
+ consumerRoot: string,
121
+ options: PackageDeliverySourceOptions = {},
122
+ ): PackageWorkflowDeliverySource {
123
+ const inspect = options.inspectOpenSpec ?? inspectOpenSpecDelivery;
124
+ const verifyGateProof = options.verifyGateProof;
125
+ const verifyFinalizedDelivery = options.verifyFinalizedDelivery;
126
+ return {
127
+ async discoverLatest(input) {
128
+ if (
129
+ input.stage !== "abel-implement" ||
130
+ !verifyGateProof ||
131
+ !verifyFinalizedDelivery
132
+ ) {
133
+ return undefined;
134
+ }
135
+ try {
136
+ const changeRoot = `openspec/changes/${input.change}`;
137
+ const receiptBytes = readPackageDeliveryFile(
138
+ consumerRoot,
139
+ `${changeRoot}/ready.yaml`,
140
+ 4 * 1024 * 1024,
141
+ );
142
+ const receipt = parseReadyReceipt(receiptBytes, {
143
+ allowLegacyOrder: true,
144
+ });
145
+ if (receipt.change !== input.change) return undefined;
146
+ const gateABytes = readPackageDeliveryFile(
147
+ consumerRoot,
148
+ `${changeRoot}/${receipt.approvals.gateA.path}`,
149
+ 4 * 1024 * 1024,
150
+ );
151
+ if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
152
+ return undefined;
153
+ }
154
+ const gateA = parseGateAReceipt(gateABytes, { allowLegacyOrder: true });
155
+ if (
156
+ gateA.change !== receipt.change ||
157
+ gateA.schema !== receipt.schema ||
158
+ !proofVerified(() =>
159
+ verifyGateProof({
160
+ change: input.change,
161
+ gate: "gate-a",
162
+ proof: gateA.approval,
163
+ }),
164
+ ) ||
165
+ !proofVerified(() =>
166
+ verifyGateProof({
167
+ change: input.change,
168
+ gate: "gate-b",
169
+ proof: receipt.approvals.gateB,
170
+ }),
171
+ )
172
+ ) {
173
+ return undefined;
174
+ }
175
+ const receiptHash = sha256(receiptBytes);
176
+ if (
177
+ !proofVerified(() =>
178
+ verifyFinalizedDelivery({
179
+ change: input.change,
180
+ deliveryRevision: receipt.deliveryRevision,
181
+ receiptHash,
182
+ gateA: gateA.approval,
183
+ gateB: receipt.approvals.gateB,
184
+ planCanonicalHash: receipt.plan.canonicalHash,
185
+ }),
186
+ )
187
+ ) {
188
+ return undefined;
189
+ }
190
+ return {
191
+ deliveryRevision: receipt.deliveryRevision,
192
+ receiptHash,
193
+ };
194
+ } catch {
195
+ return undefined;
196
+ }
197
+ },
198
+ async load(input) {
199
+ if (input.stage !== "abel-implement") {
200
+ throw new DeliveryValidationError(["delivery-stage-invalid"]);
201
+ }
202
+ const changeRoot = `openspec/changes/${input.change}`;
203
+ const receiptRelative = `${changeRoot}/ready.yaml`;
204
+ const planRelative = `${changeRoot}/implement-plan.json`;
205
+ let receiptBytes: Buffer;
206
+ try {
207
+ receiptBytes = readPackageDeliveryFile(
208
+ consumerRoot,
209
+ receiptRelative,
210
+ 4 * 1024 * 1024,
211
+ );
212
+ } catch {
213
+ throw new DeliveryValidationError(["delivery-receipt-unavailable"]);
214
+ }
215
+ let receipt: ReturnType<typeof parseReadyReceipt>;
216
+ try {
217
+ receipt = parseReadyReceipt(receiptBytes, { allowLegacyOrder: true });
218
+ } catch {
219
+ throw new DeliveryValidationError([
220
+ "delivery-receipt-invalid",
221
+ "delivery-recompile-required",
222
+ ]);
223
+ }
224
+ const diagnostics = new Set<string>();
225
+ if (receipt.change !== input.change) {
226
+ diagnostics.add("delivery-change-mismatch");
227
+ }
228
+ const receiptHash = sha256(receiptBytes);
229
+ if (
230
+ (input.deliveryRevision !== undefined &&
231
+ input.deliveryRevision !== receipt.deliveryRevision) ||
232
+ (input.receiptHash !== undefined && input.receiptHash !== receiptHash)
233
+ ) {
234
+ diagnostics.add("delivery-revision-mismatch");
235
+ }
236
+
237
+ let inspection: OpenSpecDeliveryInspection | undefined;
238
+ try {
239
+ inspection = await inspect(consumerRoot, input.change, {
240
+ signal: input.signal,
241
+ });
242
+ } catch {
243
+ diagnostics.add("delivery-openspec-unavailable");
244
+ }
245
+ input.signal?.throwIfAborted();
246
+ if (inspection) {
247
+ if (!inspection.strictValid) {
248
+ diagnostics.add("delivery-openspec-strict-invalid");
249
+ }
250
+ if (!inspection.planningComplete) {
251
+ diagnostics.add("delivery-openspec-incomplete");
252
+ }
253
+ if (inspection.schema !== receipt.schema) {
254
+ diagnostics.add("delivery-schema-mismatch");
255
+ }
256
+ }
257
+
258
+ let gateABytes: Buffer | undefined;
259
+ let gateA: ReturnType<typeof parseGateAReceipt> | undefined;
260
+ try {
261
+ gateABytes = readPackageDeliveryFile(
262
+ consumerRoot,
263
+ `${changeRoot}/${receipt.approvals.gateA.path}`,
264
+ 4 * 1024 * 1024,
265
+ );
266
+ if (sha256(gateABytes) !== receipt.approvals.gateA.rawSha256) {
267
+ diagnostics.add("delivery-gate-a-hash-mismatch");
268
+ }
269
+ gateA = parseGateAReceipt(gateABytes, { allowLegacyOrder: true });
270
+ if (
271
+ gateA.change !== receipt.change ||
272
+ gateA.schema !== receipt.schema
273
+ ) {
274
+ diagnostics.add("delivery-gate-a-binding-mismatch");
275
+ }
276
+ } catch {
277
+ diagnostics.add("delivery-gate-a-invalid");
278
+ }
279
+
280
+ const artifactBytes = new Map<string, Buffer>();
281
+ const artifactHashes = new Map(
282
+ receipt.artifacts.map((artifact) => [
283
+ artifact.path,
284
+ artifact.rawSha256,
285
+ ]),
286
+ );
287
+ for (const artifact of receipt.artifacts) {
288
+ try {
289
+ const bytes = readPackageDeliveryFile(
290
+ consumerRoot,
291
+ `${changeRoot}/${artifact.path}`,
292
+ );
293
+ artifactBytes.set(artifact.path, bytes);
294
+ if (
295
+ artifact.path !== receipt.traceability.taskPath &&
296
+ sha256(bytes) !== artifact.rawSha256
297
+ ) {
298
+ diagnostics.add(`delivery-artifact-hash-mismatch:${artifact.path}`);
299
+ }
300
+ } catch {
301
+ diagnostics.add(`delivery-artifact-unavailable:${artifact.path}`);
302
+ }
303
+ }
304
+ if (inspection) {
305
+ const covered = new Set(receipt.artifacts.map((entry) => entry.path));
306
+ const expected = new Set(inspection.artifactPaths);
307
+ for (const relative of expected) {
308
+ if (!isValidRelativePath(relative) || !covered.has(relative)) {
309
+ diagnostics.add(`delivery-artifact-unbound:${relative}`);
310
+ }
311
+ }
312
+ for (const relative of covered) {
313
+ if (!expected.has(relative)) {
314
+ diagnostics.add(`delivery-artifact-not-in-openspec:${relative}`);
315
+ }
316
+ }
317
+ }
318
+ if (gateA) {
319
+ if (!verifyGateProof) {
320
+ diagnostics.add("delivery-gate-proof-verifier-unavailable");
321
+ } else if (
322
+ !proofVerified(() =>
323
+ verifyGateProof({
324
+ change: input.change,
325
+ gate: "gate-a",
326
+ proof: gateA.approval,
327
+ }),
328
+ )
329
+ ) {
330
+ diagnostics.add("delivery-gate-a-proof-invalid");
331
+ }
332
+ for (const artifact of gateA.artifacts) {
333
+ if (artifactHashes.get(artifact.path) !== artifact.rawSha256) {
334
+ diagnostics.add(
335
+ `delivery-gate-a-artifact-mismatch:${artifact.path}`,
336
+ );
337
+ }
338
+ }
339
+ }
340
+ if (
341
+ verifyGateProof &&
342
+ !proofVerified(() =>
343
+ verifyGateProof({
344
+ change: input.change,
345
+ gate: "gate-b",
346
+ proof: receipt.approvals.gateB,
347
+ }),
348
+ )
349
+ ) {
350
+ diagnostics.add("delivery-gate-b-proof-invalid");
351
+ }
352
+ if (gateA) {
353
+ if (!verifyFinalizedDelivery) {
354
+ diagnostics.add("delivery-finalization-verifier-unavailable");
355
+ } else if (
356
+ !proofVerified(() =>
357
+ verifyFinalizedDelivery({
358
+ change: input.change,
359
+ deliveryRevision: receipt.deliveryRevision,
360
+ receiptHash,
361
+ gateA: gateA.approval,
362
+ gateB: receipt.approvals.gateB,
363
+ planCanonicalHash: receipt.plan.canonicalHash,
364
+ }),
365
+ )
366
+ ) {
367
+ diagnostics.add("delivery-finalization-proof-invalid");
368
+ }
369
+ }
370
+
371
+ let plan: ReturnType<typeof parseImplementPlan> | undefined;
372
+ let planBytes: Buffer | undefined;
373
+ try {
374
+ planBytes = readPackageDeliveryFile(consumerRoot, planRelative);
375
+ plan = parseImplementPlan(planBytes, { allowLegacyOrder: true });
376
+ } catch {
377
+ diagnostics.add("delivery-plan-invalid");
378
+ }
379
+ if (plan && planBytes) {
380
+ if (plan.changeId !== input.change) {
381
+ diagnostics.add("delivery-plan-change-mismatch");
382
+ }
383
+ let compiled: ReturnType<typeof compileImplementPlan> | undefined;
384
+ try {
385
+ compiled = compileImplementPlan(plan, { consumerRoot });
386
+ } catch {
387
+ diagnostics.add("delivery-verification-closure-invalid");
388
+ }
389
+ if (
390
+ receipt.plan.path !== "implement-plan.json" ||
391
+ receipt.plan.rawSha256 !== sha256(planBytes) ||
392
+ receipt.plan.canonicalHash !== hashCanonicalValue(plan)
393
+ ) {
394
+ diagnostics.add("delivery-plan-binding-invalid");
395
+ }
396
+ if (
397
+ compiled &&
398
+ canonicalJson(compiled.closure) !==
399
+ canonicalJson(receipt.verificationClosure)
400
+ ) {
401
+ diagnostics.add("delivery-verification-closure-mismatch");
402
+ }
403
+ }
404
+
405
+ if (plan) {
406
+ const tasksBytes = artifactBytes.get(receipt.traceability.taskPath);
407
+ const expectedTasksHash = artifactHashes.get(
408
+ receipt.traceability.taskPath,
409
+ );
410
+ if (
411
+ tasksBytes &&
412
+ expectedTasksHash &&
413
+ sha256(tasksBytes) !== expectedTasksHash
414
+ ) {
415
+ const normalized = normalizedTrackingArtifact(
416
+ tasksBytes,
417
+ plan.tracking.taskIds,
418
+ );
419
+ if (!normalized || sha256(normalized) !== expectedTasksHash) {
420
+ diagnostics.add(
421
+ `delivery-artifact-hash-mismatch:${receipt.traceability.taskPath}`,
422
+ );
423
+ }
424
+ }
425
+ const specs = [...artifactBytes]
426
+ .filter(
427
+ ([relative]) =>
428
+ relative.startsWith("specs/") && relative.endsWith("/spec.md"),
429
+ )
430
+ .flatMap(([relative, bytes]) => {
431
+ try {
432
+ return [
433
+ {
434
+ path: relative,
435
+ text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
436
+ },
437
+ ];
438
+ } catch {
439
+ diagnostics.add(`delivery-artifact-encoding-invalid:${relative}`);
440
+ return [];
441
+ }
442
+ });
443
+ if (!tasksBytes || specs.length === 0) {
444
+ diagnostics.add("delivery-traceability-input-unavailable");
445
+ } else {
446
+ try {
447
+ const traceability = assessDeliveryTraceability({
448
+ tasksMarkdown: new TextDecoder("utf-8", { fatal: true }).decode(
449
+ tasksBytes,
450
+ ),
451
+ specs,
452
+ plan,
453
+ });
454
+ if (!traceability.ok) {
455
+ for (const diagnostic of traceability.diagnostics) {
456
+ diagnostics.add(diagnostic);
457
+ }
458
+ } else if (
459
+ canonicalJson(traceability.value) !==
460
+ canonicalJson(receipt.traceability)
461
+ ) {
462
+ diagnostics.add("delivery-traceability-binding-mismatch");
463
+ }
464
+ } catch {
465
+ diagnostics.add("delivery-traceability-invalid");
466
+ }
467
+ }
468
+ }
469
+ if (!plan || !gateA || diagnostics.size > 0) {
470
+ throw new DeliveryValidationError([...diagnostics]);
471
+ }
472
+ return {
473
+ gate: "gate-b",
474
+ revision: receipt.deliveryRevision,
475
+ receiptHash,
476
+ plan,
477
+ approvalProofs: {
478
+ gateA: structuredClone(gateA.approval),
479
+ gateB: structuredClone(receipt.approvals.gateB),
480
+ },
481
+ };
482
+ },
483
+ };
484
+ }
485
+
486
+ const PACKAGE_DELIVERY_MAX_BYTES = 16 * 1024 * 1024;