@abelxiaoxing/cadence 1.3.0 → 1.4.1

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 (76) hide show
  1. package/README.md +172 -13
  2. package/agents/design-explorer.md +17 -11
  3. package/agents/diagnosis-worker.md +9 -1
  4. package/agents/implementation-worker.md +3 -1
  5. package/config/plan-draft.example.json +122 -0
  6. package/config/plan-draft.multiple-tasks.example.json +345 -0
  7. package/package.json +9 -5
  8. package/prompts/abel-design.md +141 -38
  9. package/prompts/abel-diagnose.md +10 -1
  10. package/prompts/abel-implement.md +118 -20
  11. package/prompts/abel-init.md +9 -2
  12. package/src/activity-contracts.ts +82 -0
  13. package/src/apply-transaction.ts +16 -14
  14. package/src/artifact-store.ts +17 -0
  15. package/src/candidate-artifact.ts +143 -0
  16. package/src/candidate-patch.ts +33 -12
  17. package/src/candidate-workspace.ts +74 -0
  18. package/src/canonical.ts +16 -0
  19. package/src/change-contract-schema.ts +191 -0
  20. package/src/change-contract.ts +273 -0
  21. package/src/change-verification.ts +896 -0
  22. package/src/child-model.ts +129 -0
  23. package/src/child-session.ts +296 -328
  24. package/src/contracts.ts +325 -109
  25. package/src/control-contracts.ts +51 -1
  26. package/src/delivery-compiler.ts +241 -141
  27. package/src/delivery-revision.ts +42 -0
  28. package/src/design-control.ts +182 -15
  29. package/src/design-diagnostics.ts +150 -0
  30. package/src/design-journal.ts +216 -72
  31. package/src/durable-contracts.ts +210 -0
  32. package/src/durable-workflow.ts +963 -0
  33. package/src/evidence-draft.ts +122 -0
  34. package/src/implement-graph.ts +14 -16
  35. package/src/implement-plan.ts +70 -0
  36. package/src/index.ts +341 -1405
  37. package/src/isolation-backend.ts +84 -23
  38. package/src/model-source.ts +22 -0
  39. package/src/package-candidate.ts +224 -0
  40. package/src/package-delivery.ts +486 -0
  41. package/src/package-verification.ts +509 -0
  42. package/src/package-workflow.ts +253 -0
  43. package/src/packet-runtime.ts +27 -48
  44. package/src/parent-provider.ts +86 -198
  45. package/src/phase-execution.ts +2248 -0
  46. package/src/pi-adapter.ts +19 -0
  47. package/src/plan-draft-summary.ts +146 -0
  48. package/src/plan-draft.ts +493 -0
  49. package/src/route-policy.ts +1 -11
  50. package/src/run-state.ts +4 -16
  51. package/src/run-store.ts +5 -144
  52. package/src/scoped-grep-worker.mjs +23 -0
  53. package/src/scoped-tools.ts +312 -116
  54. package/src/sqlite-schema.ts +171 -0
  55. package/src/storage-schema.ts +352 -0
  56. package/src/subagent-activity.ts +46 -67
  57. package/src/submit-tool.ts +40 -61
  58. package/src/task-ledger.ts +14 -55
  59. package/src/transport-budget.ts +32 -0
  60. package/src/verification-capability.ts +298 -102
  61. package/src/verification-environment.ts +213 -0
  62. package/src/verification-identity.ts +77 -0
  63. package/src/worker-broker.ts +134 -91
  64. package/src/workflow-engine.ts +28 -8285
  65. package/src/workflow-policy.ts +1388 -0
  66. package/src/workflow-recovery-policy.ts +95 -0
  67. package/src/workflow-scheduling.ts +105 -0
  68. package/src/workflow-state-machine.ts +4086 -0
  69. package/src/workflow-status.ts +384 -0
  70. package/src/workspace-io-entry.mjs +58 -0
  71. package/src/workspace-io-worker.mjs +991 -0
  72. package/src/workspace-io.ts +84 -0
  73. package/src/workspace-store.ts +125 -19
  74. package/skills/abel-workflow/SKILL.md +0 -205
  75. package/src/empty-resource-loader.ts +0 -38
  76. package/src/parent-payload-bridge.ts +0 -523
@@ -1,9 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { compareCanonicalStrings } from "./canonical.ts";
3
+ import {
4
+ assertPlanWithinChangeContract,
5
+ normalizeChangeContract,
6
+ } from "./change-contract.ts";
2
7
  import {
3
8
  type AgentsImpact,
4
9
  type ImplementGraphBoundary,
5
10
  type ImplementGraphOutput,
6
- type ImplementTaskBoundary,
7
11
  isAgentsPath,
8
12
  isValidRelativePath,
9
13
  type PhaseBoundary,
@@ -11,13 +15,28 @@ import {
11
15
  validateImplementGraphBoundary,
12
16
  validateVerificationContract,
13
17
  } from "./contracts.ts";
18
+ import {
19
+ type DesignPlanDiagnostic,
20
+ DesignPlanValidationError,
21
+ } from "./design-diagnostics.ts";
14
22
  import {
15
23
  assessImplementGraphReadiness,
16
24
  canonicalJson,
17
25
  hashCanonicalValue,
18
26
  type ImplementGraphReadiness,
19
27
  } from "./implement-graph.ts";
20
- import { validateVerificationAdapterCapability } from "./verification-capability.ts";
28
+ import type {
29
+ ImplementPlan,
30
+ PlanAgentsCheckpointOperation,
31
+ PlanTaskDraft,
32
+ PlanTracking,
33
+ PlanVerification,
34
+ } from "./implement-plan.ts";
35
+ import { expandPlanDraft, preparePlanDraft } from "./plan-draft.ts";
36
+ import {
37
+ bindDraftVerificationInputs,
38
+ validateVerificationAdapterCapability,
39
+ } from "./verification-capability.ts";
21
40
 
22
41
  export const IMPLEMENT_PLAN_PATH = "implement-plan.json" as const;
23
42
  export const GATE_A_RECEIPT_PATH = "gate-a.yaml" as const;
@@ -94,40 +113,6 @@ export class DeliveryValidationError extends Error {
94
113
  }
95
114
  }
96
115
 
97
- export interface DesignPlanDiagnostic {
98
- code: string;
99
- taskId?: string;
100
- phase?: string;
101
- field?: string;
102
- category?: string;
103
- owner?: string;
104
- verificationId?: string;
105
- outputId?: string;
106
- dependencyTaskId?: string;
107
- producerTaskId?: string;
108
- producerPhase?: string;
109
- }
110
-
111
- export class DesignPlanValidationError extends Error {
112
- readonly diagnostics: readonly DesignPlanDiagnostic[];
113
-
114
- constructor(message: string, diagnostics: readonly DesignPlanDiagnostic[]) {
115
- const normalized = [
116
- ...new Map(
117
- diagnostics
118
- .map((diagnostic) => structuredClone(diagnostic))
119
- .sort((left, right) =>
120
- canonicalJson(left).localeCompare(canonicalJson(right)),
121
- )
122
- .map((diagnostic) => [canonicalJson(diagnostic), diagnostic]),
123
- ).values(),
124
- ];
125
- super(message);
126
- this.name = "DesignPlanValidationError";
127
- this.diagnostics = Object.freeze(normalized);
128
- }
129
- }
130
-
131
116
  const SAFE_PLAN_ERROR_CODE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
132
117
 
133
118
  function planErrorCode(error: unknown): string {
@@ -173,6 +158,22 @@ export function assessDeliveryTraceability(input: {
173
158
  plan: ImplementPlan;
174
159
  }): DeliveryTraceabilityAssessment {
175
160
  const diagnostics = new Set<string>();
161
+ let authorMarkdown = input.tasksMarkdown;
162
+ let bindings: string | undefined;
163
+ try {
164
+ const region = taskVerificationRegion(input.tasksMarkdown);
165
+ if (region) {
166
+ authorMarkdown =
167
+ input.tasksMarkdown.slice(0, region.start) +
168
+ input.tasksMarkdown.slice(region.end);
169
+ bindings = region.text;
170
+ if (bindings !== renderTasks(input.plan)) {
171
+ diagnostics.add("traceability-managed-region-stale");
172
+ }
173
+ }
174
+ } catch {
175
+ return { ok: false, diagnostics: ["traceability-managed-region-invalid"] };
176
+ }
176
177
  const headings = new Set<string>();
177
178
  for (const spec of input.specs) {
178
179
  if (!isValidRelativePath(spec.path) || !spec.path.endsWith("/spec.md")) {
@@ -197,7 +198,7 @@ export function assessDeliveryTraceability(input: {
197
198
  }
198
199
  }
199
200
  const references = [
200
- ...input.tasksMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
201
+ ...authorMarkdown.matchAll(/`(specs\/[^`\n]+\/spec\.md#[^`\n]+)`/gu),
201
202
  ].flatMap((match) => (match[1] ? [match[1].trim()] : []));
202
203
  if (references.length === 0) diagnostics.add("traceability-reference-empty");
203
204
  const referenceSet = new Set(references);
@@ -211,21 +212,22 @@ export function assessDeliveryTraceability(input: {
211
212
  diagnostics.add("traceability-scenario-unowned");
212
213
  }
213
214
  const taskIds = input.plan.tasks.map((task) => task.taskId).sort();
214
- if (
215
- taskIds.some((taskId) => !input.tasksMarkdown.includes(`\`${taskId}\``))
216
- ) {
215
+ if (taskIds.some((taskId) => !authorMarkdown.includes(`\`${taskId}\``))) {
217
216
  diagnostics.add("traceability-task-unmapped");
218
217
  }
219
218
  const verificationIds = input.plan.tasks
220
219
  .flatMap((task) => [
221
- task.phases.red.verification.id,
220
+ ...(task.verificationMode && task.verificationMode !== "behavior"
221
+ ? []
222
+ : [task.phases.red.verification.id]),
222
223
  task.phases.green.verification.id,
223
224
  ...(task.phases.refactor ? [task.phases.refactor.verification.id] : []),
224
225
  ])
225
226
  .sort();
226
227
  if (
227
228
  verificationIds.some(
228
- (verificationId) => !input.tasksMarkdown.includes(verificationId),
229
+ (verificationId) =>
230
+ !(bindings ?? authorMarkdown).includes(verificationId),
229
231
  )
230
232
  ) {
231
233
  diagnostics.add("traceability-verification-unmapped");
@@ -249,69 +251,16 @@ export function assessDeliveryTraceability(input: {
249
251
  };
250
252
  }
251
253
 
252
- export type PlanPhaseDraft = PhaseBoundary;
253
-
254
- export interface PlanTaskDraft extends Omit<ImplementTaskBoundary, "phases"> {
255
- phases: {
256
- red: PlanPhaseDraft;
257
- green: PlanPhaseDraft;
258
- refactor?: PlanPhaseDraft;
259
- };
260
- affectedVerification: StructuredVerificationContract;
261
- repairVerification: StructuredVerificationContract;
262
- }
263
-
264
- export interface PlanVerification {
265
- baseline: {
266
- target: "task-red-contracts";
267
- affected: "task-affected-contracts";
268
- fullSuite: StructuredVerificationContract;
269
- failureIdentity: "normalized";
270
- };
271
- change: {
272
- affected: "task-affected-contracts";
273
- fullSuite: StructuredVerificationContract;
274
- postApply: StructuredVerificationContract;
275
- };
276
- artifactCorrection: {
277
- maxAttempts: number;
278
- };
279
- repair: {
280
- maxAttempts: number;
281
- inBoundaryOnly: true;
282
- approvalOnBoundaryExpansion: true;
283
- attribution: ["pre-existing", "introduced", "unresolved", "environment"];
284
- };
285
- agentsCheckpoint: {
286
- required: boolean;
287
- verification: StructuredVerificationContract | null;
288
- operations: PlanAgentsCheckpointOperation[];
289
- };
290
- }
291
-
292
- export interface PlanAgentsCheckpointOperation {
293
- target: string;
294
- impact: Exclude<AgentsImpact, "none">;
295
- taskIds: string[];
296
- managedBlock: string | null;
297
- }
298
-
299
- export interface PlanTracking {
300
- path: "tasks.md";
301
- format: "markdown-checkbox";
302
- taskIds: string[];
303
- completionOwner: "parent";
304
- }
305
-
306
- export interface PlanDraft {
307
- changeId: string;
308
- tasks: PlanTaskDraft[];
309
- outputs: ImplementGraphOutput[];
310
- verification: PlanVerification;
311
- tracking: PlanTracking;
312
- }
313
-
314
- export type ImplementPlan = PlanDraft;
254
+ export type { DesignPlanDiagnostic } from "./design-diagnostics.ts";
255
+ export { DesignPlanValidationError } from "./design-diagnostics.ts";
256
+ export type {
257
+ ImplementPlan,
258
+ PlanAgentsCheckpointOperation,
259
+ PlanTaskDraft,
260
+ PlanTracking,
261
+ PlanVerification,
262
+ } from "./implement-plan.ts";
263
+ export type { PlanDraft, PlanPhaseDraft, PlanTaskInput } from "./plan-draft.ts";
315
264
 
316
265
  export interface CompiledDelivery {
317
266
  plan: ImplementPlan;
@@ -371,7 +320,9 @@ function normalizeArtifactBindings(
371
320
  paths.add(binding.path);
372
321
  return { path: binding.path, rawSha256: binding.rawSha256 };
373
322
  });
374
- return normalized.sort((left, right) => left.path.localeCompare(right.path));
323
+ return normalized.sort((left, right) =>
324
+ compareCanonicalStrings(left.path, right.path),
325
+ );
375
326
  }
376
327
 
377
328
  function requireReceiptIdentity(
@@ -405,7 +356,41 @@ function normalizeGateProof(value: GateApprovalProof): GateApprovalProof {
405
356
  return structuredClone(value);
406
357
  }
407
358
 
408
- function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
359
+ export interface DeliveryReadOptions {
360
+ /** Only the proof-bound delivery loader may admit historical collection ordering. */
361
+ allowLegacyOrder?: boolean;
362
+ }
363
+
364
+ const legacyPlans = new WeakMap<object, string>();
365
+
366
+ function unorderedIdentity(value: unknown): string {
367
+ if (Array.isArray(value))
368
+ return `[${value.map(unorderedIdentity).sort().join(",")}]`;
369
+ if (isRecord(value))
370
+ return `{${Object.entries(value)
371
+ .sort(([a], [b]) => compareCanonicalStrings(a, b))
372
+ .map(
373
+ ([key, entry]) => `${JSON.stringify(key)}:${unorderedIdentity(entry)}`,
374
+ )
375
+ .join(",")}}`;
376
+ return JSON.stringify(value) ?? "null";
377
+ }
378
+
379
+ function sameNormalized(
380
+ left: unknown,
381
+ right: unknown,
382
+ options: DeliveryReadOptions,
383
+ ): boolean {
384
+ return options.allowLegacyOrder
385
+ ? unorderedIdentity(left) === unorderedIdentity(right)
386
+ : canonicalJson(left) === canonicalJson(right);
387
+ }
388
+
389
+ function parseCanonicalReceipt<T>(
390
+ bytes: Uint8Array,
391
+ code: string,
392
+ options: DeliveryReadOptions = {},
393
+ ): T {
409
394
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
410
395
  let parsed: unknown;
411
396
  try {
@@ -413,7 +398,11 @@ function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
413
398
  } catch {
414
399
  throw new Error(code);
415
400
  }
416
- if (`${canonicalJson(parsed)}\n` !== text) throw new Error(code);
401
+ if (
402
+ `${canonicalJson(parsed)}\n` !== text &&
403
+ (!options.allowLegacyOrder || `${JSON.stringify(parsed)}\n` !== text)
404
+ )
405
+ throw new Error(code);
417
406
  return parsed as T;
418
407
  }
419
408
 
@@ -438,10 +427,14 @@ export function compileGateAReceipt(input: {
438
427
  };
439
428
  }
440
429
 
441
- export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
430
+ export function parseGateAReceipt(
431
+ bytes: Uint8Array,
432
+ options: DeliveryReadOptions = {},
433
+ ): GateAReceipt {
442
434
  const receipt = parseCanonicalReceipt<GateAReceipt>(
443
435
  bytes,
444
436
  "gate-a-receipt-invalid",
437
+ options,
445
438
  );
446
439
  if (
447
440
  !isRecord(receipt) ||
@@ -455,8 +448,8 @@ export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
455
448
  const approval = normalizeGateProof(receipt.approval);
456
449
  const artifacts = normalizeArtifactBindings(receipt.artifacts);
457
450
  if (
458
- canonicalJson(approval) !== canonicalJson(receipt.approval) ||
459
- canonicalJson(artifacts) !== canonicalJson(receipt.artifacts)
451
+ !sameNormalized(approval, receipt.approval, options) ||
452
+ !sameNormalized(artifacts, receipt.artifacts, options)
460
453
  ) {
461
454
  throw new Error("gate-a-receipt-invalid");
462
455
  }
@@ -537,10 +530,14 @@ export function compileReadyReceipt(input: {
537
530
  };
538
531
  }
539
532
 
540
- export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
533
+ export function parseReadyReceipt(
534
+ bytes: Uint8Array,
535
+ options: DeliveryReadOptions = {},
536
+ ): ReadyReceipt {
541
537
  const receipt = parseCanonicalReceipt<ReadyReceipt>(
542
538
  bytes,
543
539
  "delivery-receipt-invalid",
540
+ options,
544
541
  );
545
542
  if (
546
543
  !isRecord(receipt) ||
@@ -592,9 +589,9 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
592
589
  const traceability = normalizeTraceability(receipt.traceability);
593
590
  if (
594
591
  gateB.contractHash !== receipt.plan.canonicalHash ||
595
- canonicalJson(gateB) !== canonicalJson(receipt.approvals.gateB) ||
596
- canonicalJson(artifacts) !== canonicalJson(receipt.artifacts) ||
597
- canonicalJson(traceability) !== canonicalJson(receipt.traceability)
592
+ !sameNormalized(gateB, receipt.approvals.gateB, options) ||
593
+ !sameNormalized(artifacts, receipt.artifacts, options) ||
594
+ !sameNormalized(traceability, receipt.traceability, options)
598
595
  ) {
599
596
  throw new Error("delivery-receipt-invalid");
600
597
  }
@@ -602,12 +599,14 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
602
599
  }
603
600
 
604
601
  function sortStrings(values: readonly string[]): string[] {
605
- return [...values].sort((left, right) => left.localeCompare(right));
602
+ return [...values].sort((left, right) =>
603
+ compareCanonicalStrings(left, right),
604
+ );
606
605
  }
607
606
 
608
607
  function sortCanonical<T>(values: readonly T[]): T[] {
609
608
  return [...values].sort((left, right) =>
610
- canonicalJson(left).localeCompare(canonicalJson(right)),
609
+ compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
611
610
  );
612
611
  }
613
612
 
@@ -628,7 +627,7 @@ function normalizeVerification(
628
627
  return cloned;
629
628
  }
630
629
 
631
- function normalizePhase(value: unknown): PlanPhaseDraft {
630
+ function normalizePhase(value: unknown, readOnly = false): PhaseBoundary {
632
631
  if (
633
632
  !isRecord(value) ||
634
633
  !exactKeys(value, [
@@ -651,7 +650,7 @@ function normalizePhase(value: unknown): PlanPhaseDraft {
651
650
  }
652
651
  const write = sortStrings(value.write as string[]);
653
652
  const deletions = sortStrings(value.delete as string[]);
654
- if (write.length + deletions.length === 0) {
653
+ if (!readOnly && write.length + deletions.length === 0) {
655
654
  throw new Error("delivery-plan-empty-phase-boundary");
656
655
  }
657
656
  if (write.some((candidate) => deletions.includes(candidate))) {
@@ -724,7 +723,12 @@ function normalizeTask(value: unknown): PlanTaskDraft {
724
723
  candidate: unknown,
725
724
  ) => {
726
725
  try {
727
- return normalizePhase(candidate);
726
+ return normalizePhase(
727
+ candidate,
728
+ phase === "red" &&
729
+ value.verificationMode !== undefined &&
730
+ value.verificationMode !== "behavior",
731
+ );
728
732
  } catch (error) {
729
733
  throw asPlanValidationError(error, {
730
734
  ...context,
@@ -758,8 +762,8 @@ function normalizeTask(value: unknown): PlanTaskDraft {
758
762
  searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
759
763
  relatedTests: [...cloned.impactClosure.relatedTests].sort(
760
764
  (left, right) =>
761
- left.path.localeCompare(right.path) ||
762
- left.disposition.localeCompare(right.disposition),
765
+ compareCanonicalStrings(left.path, right.path) ||
766
+ compareCanonicalStrings(left.disposition, right.disposition),
763
767
  ),
764
768
  affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
765
769
  },
@@ -770,7 +774,7 @@ function normalizeTask(value: unknown): PlanTaskDraft {
770
774
  }
771
775
 
772
776
  function graphFromPlan(plan: ImplementPlan): ImplementGraphBoundary {
773
- const stripPhase = (phase: PlanPhaseDraft): PhaseBoundary => ({
777
+ const stripPhase = (phase: PhaseBoundary): PhaseBoundary => ({
774
778
  read: phase.read,
775
779
  write: phase.write,
776
780
  delete: phase.delete,
@@ -936,7 +940,7 @@ function normalizeAgentsCheckpointOperations(
936
940
  : normalizeAgentsManagedBlock(operation.managedBlock),
937
941
  };
938
942
  })
939
- .sort((left, right) => left.target.localeCompare(right.target));
943
+ .sort((left, right) => compareCanonicalStrings(left.target, right.target));
940
944
  }
941
945
 
942
946
  function normalizeVerificationPlan(value: unknown): PlanVerification {
@@ -1059,6 +1063,7 @@ function normalizeTracking(value: unknown): PlanTracking {
1059
1063
  }
1060
1064
 
1061
1065
  function normalizeDraft(value: unknown): ImplementPlan {
1066
+ value = structuredClone(value);
1062
1067
  if (
1063
1068
  !isRecord(value) ||
1064
1069
  !exactKeys(value, [
@@ -1067,6 +1072,7 @@ function normalizeDraft(value: unknown): ImplementPlan {
1067
1072
  "outputs",
1068
1073
  "verification",
1069
1074
  "tracking",
1075
+ ...(value.changeContract === undefined ? [] : ["changeContract"]),
1070
1076
  ]) ||
1071
1077
  typeof value.changeId !== "string" ||
1072
1078
  !CHANGE_NAME.test(value.changeId) ||
@@ -1075,9 +1081,26 @@ function normalizeDraft(value: unknown): ImplementPlan {
1075
1081
  ) {
1076
1082
  throw new Error("delivery-plan-invalid");
1077
1083
  }
1084
+ const source = value;
1085
+ const taskDrafts = value.tasks.map((task: unknown) => {
1086
+ if (
1087
+ !isRecord(task) ||
1088
+ !["mechanical", "refactor"].includes(String(task.verificationMode))
1089
+ )
1090
+ return task;
1091
+ if (!isRecord(task.phases) || !isRecord(task.phases.green))
1092
+ throw new Error("delivery-plan-invalid-task-phases");
1093
+ if (!source.changeContract)
1094
+ throw new Error("change-contract-verification-mode-required");
1095
+ const green = task.phases.green;
1096
+ return {
1097
+ ...task,
1098
+ phases: { ...task.phases, red: { ...green, write: [], delete: [] } },
1099
+ };
1100
+ });
1078
1101
  const draftGraph = validateImplementGraphBoundary({
1079
1102
  changeId: value.changeId,
1080
- tasks: value.tasks.map((candidate) => {
1103
+ tasks: taskDrafts.map((candidate) => {
1081
1104
  if (!isRecord(candidate)) return candidate;
1082
1105
  const {
1083
1106
  affectedVerification: _affectedVerification,
@@ -1091,16 +1114,21 @@ function normalizeDraft(value: unknown): ImplementPlan {
1091
1114
  if (!draftGraph.ok) {
1092
1115
  throw new DesignPlanValidationError(
1093
1116
  `delivery-plan-invalid:${draftGraph.reason}`,
1094
- [draftGraph.diagnostic],
1117
+ draftGraph.diagnostics ?? [draftGraph.diagnostic],
1095
1118
  );
1096
1119
  }
1097
1120
  const plan: ImplementPlan = {
1121
+ ...(value.changeContract === undefined
1122
+ ? {}
1123
+ : { changeContract: normalizeChangeContract(value.changeContract) }),
1098
1124
  changeId: value.changeId,
1099
- tasks: value.tasks
1125
+ tasks: taskDrafts
1100
1126
  .map(normalizeTask)
1101
- .sort((left, right) => left.taskId.localeCompare(right.taskId)),
1127
+ .sort((left, right) =>
1128
+ compareCanonicalStrings(left.taskId, right.taskId),
1129
+ ),
1102
1130
  outputs: (structuredClone(value.outputs) as ImplementGraphOutput[]).sort(
1103
- (left, right) => left.id.localeCompare(right.id),
1131
+ (left, right) => compareCanonicalStrings(left.id, right.id),
1104
1132
  ),
1105
1133
  verification: normalizeVerificationPlan(value.verification),
1106
1134
  tracking: normalizeTracking(value.tracking),
@@ -1141,6 +1169,11 @@ function normalizeDraft(value: unknown): ImplementPlan {
1141
1169
  ) {
1142
1170
  throw new Error("delivery-agents-checkpoint-mismatch");
1143
1171
  }
1172
+ if (plan.changeContract)
1173
+ assertPlanWithinChangeContract(plan.changeContract, plan.tasks, [
1174
+ plan.verification.change.fullSuite,
1175
+ plan.verification.change.postApply,
1176
+ ]);
1144
1177
  const graphValidation = validateImplementGraphBoundary(graphFromPlan(plan));
1145
1178
  if (!graphValidation.ok) {
1146
1179
  throw new DesignPlanValidationError(
@@ -1151,26 +1184,83 @@ function normalizeDraft(value: unknown): ImplementPlan {
1151
1184
  return plan;
1152
1185
  }
1153
1186
 
1187
+ const TASK_BINDINGS_START = "<!-- ABEL:VERIFICATION-BINDINGS:START -->";
1188
+ const TASK_BINDINGS_END = "<!-- ABEL:VERIFICATION-BINDINGS:END -->";
1189
+
1190
+ function taskVerificationRegion(markdown: string) {
1191
+ const markers = markdown.match(/<!-- ABEL:VERIFICATION-BINDINGS/gu) ?? [];
1192
+ if (markers.length === 0) return undefined;
1193
+ const match =
1194
+ /^<!-- ABEL:VERIFICATION-BINDINGS:START -->\r?\n[\s\S]*?^<!-- ABEL:VERIFICATION-BINDINGS:END -->(?=\r?$)/mu.exec(
1195
+ markdown,
1196
+ );
1197
+ if (markers.length !== 2 || !match) {
1198
+ throw new DesignPlanValidationError("traceability-managed-region-invalid", [
1199
+ { code: "traceability-managed-region-invalid", path: "tasks.md" },
1200
+ ]);
1201
+ }
1202
+ return {
1203
+ start: match.index,
1204
+ end: match.index + match[0].length,
1205
+ text: match[0],
1206
+ };
1207
+ }
1208
+
1209
+ // Only identity bindings are generated. Author task checkboxes and Scenario
1210
+ // ownership remain outside this region and are validated independently.
1154
1211
  function renderTasks(plan: ImplementPlan): string {
1155
- const lines = ["# Implementation tasks", ""];
1156
- for (const task of plan.tasks) {
1157
- lines.push(`- [ ] ${task.taskId} — ${task.objective}`);
1212
+ const lines = [TASK_BINDINGS_START, "## Compiled verification bindings", ""];
1213
+ for (const task of [...plan.tasks].sort((a, b) =>
1214
+ compareCanonicalStrings(a.taskId, b.taskId),
1215
+ )) {
1158
1216
  lines.push(
1159
- ` - Depends on: ${task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "[]"}`,
1217
+ `- Task \`${task.taskId}\` (${task.verificationMode ?? "behavior"})`,
1160
1218
  );
1161
- lines.push(` - Red verification: ${task.phases.red.verification.id}`);
1162
- lines.push(` - Green verification: ${task.phases.green.verification.id}`);
1219
+ if (!task.verificationMode || task.verificationMode === "behavior") {
1220
+ lines.push(` - Red: \`${task.phases.red.verification.id}\``);
1221
+ }
1222
+ lines.push(` - Green: \`${task.phases.green.verification.id}\``);
1223
+ if (task.phases.refactor) {
1224
+ lines.push(` - Refactor: \`${task.phases.refactor.verification.id}\``);
1225
+ }
1163
1226
  }
1164
- lines.push("");
1227
+ lines.push(TASK_BINDINGS_END);
1165
1228
  return lines.join("\n");
1166
1229
  }
1167
1230
 
1231
+ export function bindTaskVerifications(
1232
+ markdown: string,
1233
+ plan: ImplementPlan,
1234
+ ): string {
1235
+ const region = taskVerificationRegion(markdown);
1236
+ const bindings = renderTasks(plan);
1237
+ if (region) {
1238
+ return (
1239
+ markdown.slice(0, region.start) + bindings + markdown.slice(region.end)
1240
+ );
1241
+ }
1242
+ return `${markdown}${markdown.endsWith("\n") ? "\n" : "\n\n"}${bindings}\n`;
1243
+ }
1244
+
1168
1245
  export function compileImplementPlan(
1169
1246
  draft: unknown,
1170
- options: { consumerRoot: string },
1247
+ options: { consumerRoot: string; bindExecutionInputs?: boolean },
1171
1248
  ): CompiledDelivery {
1172
1249
  try {
1173
- const plan = normalizeDraft(draft);
1250
+ // Resolve omitted manifest command bytes before named definitions are
1251
+ // validated and purpose identities are hashed. The second pass below
1252
+ // binds execution inputs on the expanded contracts with their final IDs.
1253
+ if (options.bindExecutionInputs)
1254
+ draft = bindDraftVerificationInputs(options.consumerRoot, draft);
1255
+ draft = expandPlanDraft(draft);
1256
+ if (options.bindExecutionInputs)
1257
+ draft = bindDraftVerificationInputs(options.consumerRoot, draft);
1258
+ draft = preparePlanDraft(draft);
1259
+ const normalized = normalizeDraft(draft);
1260
+ const plan =
1261
+ isRecord(draft) && legacyPlans.get(draft) === JSON.stringify(draft)
1262
+ ? (draft as unknown as ImplementPlan)
1263
+ : normalized;
1174
1264
  const graph = graphFromPlan(plan);
1175
1265
  const readiness = assessImplementGraphReadiness(
1176
1266
  options.consumerRoot,
@@ -1217,7 +1307,10 @@ export function compileImplementPlan(
1217
1307
  }
1218
1308
  }
1219
1309
 
1220
- export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
1310
+ export function parseImplementPlan(
1311
+ bytes: Uint8Array,
1312
+ options: DeliveryReadOptions = {},
1313
+ ): ImplementPlan {
1221
1314
  const text = new TextDecoder().decode(bytes);
1222
1315
  let parsed: unknown;
1223
1316
  try {
@@ -1226,8 +1319,15 @@ export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
1226
1319
  throw new Error("delivery-plan-json-invalid");
1227
1320
  }
1228
1321
  const normalized = normalizeDraft(parsed);
1229
- if (`${canonicalJson(normalized)}\n` !== text) {
1322
+ if (`${canonicalJson(normalized)}\n` === text) return normalized;
1323
+ if (
1324
+ !options.allowLegacyOrder ||
1325
+ `${canonicalJson(parsed)}\n` !== text ||
1326
+ !sameNormalized(normalized, parsed, options)
1327
+ ) {
1230
1328
  throw new Error("delivery-plan-not-canonical");
1231
1329
  }
1232
- return normalized;
1330
+ // Keep proof-bound historical bytes unchanged; a mutation invalidates this association.
1331
+ legacyPlans.set(parsed as object, JSON.stringify(parsed));
1332
+ return parsed as ImplementPlan;
1233
1333
  }
@@ -0,0 +1,42 @@
1
+ import type { ImplementPlan } from "./implement-plan.ts";
2
+ import {
3
+ deliveryBoundPaths,
4
+ type EngineTaskRow,
5
+ taskApprovalBoundary,
6
+ } from "./workflow-policy.ts";
7
+ /** Compare authority only; retained bytes still require executor currentness and replay. */
8
+ export function compareDeliveryRevision(
9
+ priorPlan: ImplementPlan | undefined,
10
+ nextPlan: ImplementPlan,
11
+ priorRows: readonly Pick<EngineTaskRow, "task_id">[],
12
+ ) {
13
+ const priorTasks = new Map(
14
+ (priorPlan?.tasks ?? []).map((task) => [task.taskId, task]),
15
+ );
16
+ const nextTasks = new Map(nextPlan.tasks.map((task) => [task.taskId, task]));
17
+ const priorBoundPaths = new Set(
18
+ priorPlan ? deliveryBoundPaths(priorPlan) : [],
19
+ );
20
+ const boundaryExpanded = deliveryBoundPaths(nextPlan).some(
21
+ (relative) => !priorBoundPaths.has(relative),
22
+ );
23
+ const compatible = new Set<string>();
24
+ if (priorPlan) {
25
+ for (const [taskId, priorTask] of priorTasks) {
26
+ const nextTask = nextTasks.get(taskId);
27
+ if (
28
+ nextTask &&
29
+ taskApprovalBoundary(priorPlan, priorTask) ===
30
+ taskApprovalBoundary(nextPlan, nextTask)
31
+ ) {
32
+ compatible.add(taskId);
33
+ }
34
+ }
35
+ }
36
+ const invalidated = new Set(
37
+ priorRows
38
+ .filter((row) => !compatible.has(row.task_id))
39
+ .map((row) => row.task_id),
40
+ );
41
+ return { compatible, invalidated, boundaryExpanded };
42
+ }