@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
@@ -1,4 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { compareCanonicalStrings } from "./canonical.ts";
3
+ import {
4
+ assertPlanWithinChangeContract,
5
+ type ChangeContract,
6
+ normalizeChangeContract,
7
+ } from "./change-contract.ts";
2
8
  import {
3
9
  type AgentsImpact,
4
10
  type ImplementGraphBoundary,
@@ -17,7 +23,10 @@ import {
17
23
  hashCanonicalValue,
18
24
  type ImplementGraphReadiness,
19
25
  } from "./implement-graph.ts";
20
- import { validateVerificationAdapterCapability } from "./verification-capability.ts";
26
+ import {
27
+ bindDraftVerificationInputs,
28
+ validateVerificationAdapterCapability,
29
+ } from "./verification-capability.ts";
21
30
 
22
31
  export const IMPLEMENT_PLAN_PATH = "implement-plan.json" as const;
23
32
  export const GATE_A_RECEIPT_PATH = "gate-a.yaml" as const;
@@ -117,7 +126,7 @@ export class DesignPlanValidationError extends Error {
117
126
  diagnostics
118
127
  .map((diagnostic) => structuredClone(diagnostic))
119
128
  .sort((left, right) =>
120
- canonicalJson(left).localeCompare(canonicalJson(right)),
129
+ compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
121
130
  )
122
131
  .map((diagnostic) => [canonicalJson(diagnostic), diagnostic]),
123
132
  ).values(),
@@ -218,7 +227,9 @@ export function assessDeliveryTraceability(input: {
218
227
  }
219
228
  const verificationIds = input.plan.tasks
220
229
  .flatMap((task) => [
221
- task.phases.red.verification.id,
230
+ ...(task.verificationMode && task.verificationMode !== "behavior"
231
+ ? []
232
+ : [task.phases.red.verification.id]),
222
233
  task.phases.green.verification.id,
223
234
  ...(task.phases.refactor ? [task.phases.refactor.verification.id] : []),
224
235
  ])
@@ -304,6 +315,7 @@ export interface PlanTracking {
304
315
  }
305
316
 
306
317
  export interface PlanDraft {
318
+ changeContract?: ChangeContract;
307
319
  changeId: string;
308
320
  tasks: PlanTaskDraft[];
309
321
  outputs: ImplementGraphOutput[];
@@ -371,7 +383,9 @@ function normalizeArtifactBindings(
371
383
  paths.add(binding.path);
372
384
  return { path: binding.path, rawSha256: binding.rawSha256 };
373
385
  });
374
- return normalized.sort((left, right) => left.path.localeCompare(right.path));
386
+ return normalized.sort((left, right) =>
387
+ compareCanonicalStrings(left.path, right.path),
388
+ );
375
389
  }
376
390
 
377
391
  function requireReceiptIdentity(
@@ -405,7 +419,41 @@ function normalizeGateProof(value: GateApprovalProof): GateApprovalProof {
405
419
  return structuredClone(value);
406
420
  }
407
421
 
408
- function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
422
+ export interface DeliveryReadOptions {
423
+ /** Only the proof-bound delivery loader may admit historical collection ordering. */
424
+ allowLegacyOrder?: boolean;
425
+ }
426
+
427
+ const legacyPlans = new WeakMap<object, string>();
428
+
429
+ function unorderedIdentity(value: unknown): string {
430
+ if (Array.isArray(value))
431
+ return `[${value.map(unorderedIdentity).sort().join(",")}]`;
432
+ if (isRecord(value))
433
+ return `{${Object.entries(value)
434
+ .sort(([a], [b]) => compareCanonicalStrings(a, b))
435
+ .map(
436
+ ([key, entry]) => `${JSON.stringify(key)}:${unorderedIdentity(entry)}`,
437
+ )
438
+ .join(",")}}`;
439
+ return JSON.stringify(value) ?? "null";
440
+ }
441
+
442
+ function sameNormalized(
443
+ left: unknown,
444
+ right: unknown,
445
+ options: DeliveryReadOptions,
446
+ ): boolean {
447
+ return options.allowLegacyOrder
448
+ ? unorderedIdentity(left) === unorderedIdentity(right)
449
+ : canonicalJson(left) === canonicalJson(right);
450
+ }
451
+
452
+ function parseCanonicalReceipt<T>(
453
+ bytes: Uint8Array,
454
+ code: string,
455
+ options: DeliveryReadOptions = {},
456
+ ): T {
409
457
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
410
458
  let parsed: unknown;
411
459
  try {
@@ -413,7 +461,11 @@ function parseCanonicalReceipt<T>(bytes: Uint8Array, code: string): T {
413
461
  } catch {
414
462
  throw new Error(code);
415
463
  }
416
- if (`${canonicalJson(parsed)}\n` !== text) throw new Error(code);
464
+ if (
465
+ `${canonicalJson(parsed)}\n` !== text &&
466
+ (!options.allowLegacyOrder || `${JSON.stringify(parsed)}\n` !== text)
467
+ )
468
+ throw new Error(code);
417
469
  return parsed as T;
418
470
  }
419
471
 
@@ -438,10 +490,14 @@ export function compileGateAReceipt(input: {
438
490
  };
439
491
  }
440
492
 
441
- export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
493
+ export function parseGateAReceipt(
494
+ bytes: Uint8Array,
495
+ options: DeliveryReadOptions = {},
496
+ ): GateAReceipt {
442
497
  const receipt = parseCanonicalReceipt<GateAReceipt>(
443
498
  bytes,
444
499
  "gate-a-receipt-invalid",
500
+ options,
445
501
  );
446
502
  if (
447
503
  !isRecord(receipt) ||
@@ -455,8 +511,8 @@ export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
455
511
  const approval = normalizeGateProof(receipt.approval);
456
512
  const artifacts = normalizeArtifactBindings(receipt.artifacts);
457
513
  if (
458
- canonicalJson(approval) !== canonicalJson(receipt.approval) ||
459
- canonicalJson(artifacts) !== canonicalJson(receipt.artifacts)
514
+ !sameNormalized(approval, receipt.approval, options) ||
515
+ !sameNormalized(artifacts, receipt.artifacts, options)
460
516
  ) {
461
517
  throw new Error("gate-a-receipt-invalid");
462
518
  }
@@ -537,10 +593,14 @@ export function compileReadyReceipt(input: {
537
593
  };
538
594
  }
539
595
 
540
- export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
596
+ export function parseReadyReceipt(
597
+ bytes: Uint8Array,
598
+ options: DeliveryReadOptions = {},
599
+ ): ReadyReceipt {
541
600
  const receipt = parseCanonicalReceipt<ReadyReceipt>(
542
601
  bytes,
543
602
  "delivery-receipt-invalid",
603
+ options,
544
604
  );
545
605
  if (
546
606
  !isRecord(receipt) ||
@@ -592,9 +652,9 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
592
652
  const traceability = normalizeTraceability(receipt.traceability);
593
653
  if (
594
654
  gateB.contractHash !== receipt.plan.canonicalHash ||
595
- canonicalJson(gateB) !== canonicalJson(receipt.approvals.gateB) ||
596
- canonicalJson(artifacts) !== canonicalJson(receipt.artifacts) ||
597
- canonicalJson(traceability) !== canonicalJson(receipt.traceability)
655
+ !sameNormalized(gateB, receipt.approvals.gateB, options) ||
656
+ !sameNormalized(artifacts, receipt.artifacts, options) ||
657
+ !sameNormalized(traceability, receipt.traceability, options)
598
658
  ) {
599
659
  throw new Error("delivery-receipt-invalid");
600
660
  }
@@ -602,12 +662,14 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
602
662
  }
603
663
 
604
664
  function sortStrings(values: readonly string[]): string[] {
605
- return [...values].sort((left, right) => left.localeCompare(right));
665
+ return [...values].sort((left, right) =>
666
+ compareCanonicalStrings(left, right),
667
+ );
606
668
  }
607
669
 
608
670
  function sortCanonical<T>(values: readonly T[]): T[] {
609
671
  return [...values].sort((left, right) =>
610
- canonicalJson(left).localeCompare(canonicalJson(right)),
672
+ compareCanonicalStrings(canonicalJson(left), canonicalJson(right)),
611
673
  );
612
674
  }
613
675
 
@@ -628,7 +690,7 @@ function normalizeVerification(
628
690
  return cloned;
629
691
  }
630
692
 
631
- function normalizePhase(value: unknown): PlanPhaseDraft {
693
+ function normalizePhase(value: unknown, readOnly = false): PlanPhaseDraft {
632
694
  if (
633
695
  !isRecord(value) ||
634
696
  !exactKeys(value, [
@@ -651,7 +713,7 @@ function normalizePhase(value: unknown): PlanPhaseDraft {
651
713
  }
652
714
  const write = sortStrings(value.write as string[]);
653
715
  const deletions = sortStrings(value.delete as string[]);
654
- if (write.length + deletions.length === 0) {
716
+ if (!readOnly && write.length + deletions.length === 0) {
655
717
  throw new Error("delivery-plan-empty-phase-boundary");
656
718
  }
657
719
  if (write.some((candidate) => deletions.includes(candidate))) {
@@ -724,7 +786,12 @@ function normalizeTask(value: unknown): PlanTaskDraft {
724
786
  candidate: unknown,
725
787
  ) => {
726
788
  try {
727
- return normalizePhase(candidate);
789
+ return normalizePhase(
790
+ candidate,
791
+ phase === "red" &&
792
+ value.verificationMode !== undefined &&
793
+ value.verificationMode !== "behavior",
794
+ );
728
795
  } catch (error) {
729
796
  throw asPlanValidationError(error, {
730
797
  ...context,
@@ -758,8 +825,8 @@ function normalizeTask(value: unknown): PlanTaskDraft {
758
825
  searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
759
826
  relatedTests: [...cloned.impactClosure.relatedTests].sort(
760
827
  (left, right) =>
761
- left.path.localeCompare(right.path) ||
762
- left.disposition.localeCompare(right.disposition),
828
+ compareCanonicalStrings(left.path, right.path) ||
829
+ compareCanonicalStrings(left.disposition, right.disposition),
763
830
  ),
764
831
  affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
765
832
  },
@@ -936,7 +1003,7 @@ function normalizeAgentsCheckpointOperations(
936
1003
  : normalizeAgentsManagedBlock(operation.managedBlock),
937
1004
  };
938
1005
  })
939
- .sort((left, right) => left.target.localeCompare(right.target));
1006
+ .sort((left, right) => compareCanonicalStrings(left.target, right.target));
940
1007
  }
941
1008
 
942
1009
  function normalizeVerificationPlan(value: unknown): PlanVerification {
@@ -1059,6 +1126,7 @@ function normalizeTracking(value: unknown): PlanTracking {
1059
1126
  }
1060
1127
 
1061
1128
  function normalizeDraft(value: unknown): ImplementPlan {
1129
+ value = structuredClone(value);
1062
1130
  if (
1063
1131
  !isRecord(value) ||
1064
1132
  !exactKeys(value, [
@@ -1067,6 +1135,7 @@ function normalizeDraft(value: unknown): ImplementPlan {
1067
1135
  "outputs",
1068
1136
  "verification",
1069
1137
  "tracking",
1138
+ ...(value.changeContract === undefined ? [] : ["changeContract"]),
1070
1139
  ]) ||
1071
1140
  typeof value.changeId !== "string" ||
1072
1141
  !CHANGE_NAME.test(value.changeId) ||
@@ -1075,9 +1144,26 @@ function normalizeDraft(value: unknown): ImplementPlan {
1075
1144
  ) {
1076
1145
  throw new Error("delivery-plan-invalid");
1077
1146
  }
1147
+ const source = value;
1148
+ const taskDrafts = value.tasks.map((task: unknown) => {
1149
+ if (
1150
+ !isRecord(task) ||
1151
+ !["mechanical", "refactor"].includes(String(task.verificationMode))
1152
+ )
1153
+ return task;
1154
+ if (!isRecord(task.phases) || !isRecord(task.phases.green))
1155
+ throw new Error("delivery-plan-invalid-task-phases");
1156
+ if (!source.changeContract)
1157
+ throw new Error("change-contract-verification-mode-required");
1158
+ const green = task.phases.green;
1159
+ return {
1160
+ ...task,
1161
+ phases: { ...task.phases, red: { ...green, write: [], delete: [] } },
1162
+ };
1163
+ });
1078
1164
  const draftGraph = validateImplementGraphBoundary({
1079
1165
  changeId: value.changeId,
1080
- tasks: value.tasks.map((candidate) => {
1166
+ tasks: taskDrafts.map((candidate) => {
1081
1167
  if (!isRecord(candidate)) return candidate;
1082
1168
  const {
1083
1169
  affectedVerification: _affectedVerification,
@@ -1095,12 +1181,17 @@ function normalizeDraft(value: unknown): ImplementPlan {
1095
1181
  );
1096
1182
  }
1097
1183
  const plan: ImplementPlan = {
1184
+ ...(value.changeContract === undefined
1185
+ ? {}
1186
+ : { changeContract: normalizeChangeContract(value.changeContract) }),
1098
1187
  changeId: value.changeId,
1099
- tasks: value.tasks
1188
+ tasks: taskDrafts
1100
1189
  .map(normalizeTask)
1101
- .sort((left, right) => left.taskId.localeCompare(right.taskId)),
1190
+ .sort((left, right) =>
1191
+ compareCanonicalStrings(left.taskId, right.taskId),
1192
+ ),
1102
1193
  outputs: (structuredClone(value.outputs) as ImplementGraphOutput[]).sort(
1103
- (left, right) => left.id.localeCompare(right.id),
1194
+ (left, right) => compareCanonicalStrings(left.id, right.id),
1104
1195
  ),
1105
1196
  verification: normalizeVerificationPlan(value.verification),
1106
1197
  tracking: normalizeTracking(value.tracking),
@@ -1141,6 +1232,11 @@ function normalizeDraft(value: unknown): ImplementPlan {
1141
1232
  ) {
1142
1233
  throw new Error("delivery-agents-checkpoint-mismatch");
1143
1234
  }
1235
+ if (plan.changeContract)
1236
+ assertPlanWithinChangeContract(plan.changeContract, plan.tasks, [
1237
+ plan.verification.change.fullSuite,
1238
+ plan.verification.change.postApply,
1239
+ ]);
1144
1240
  const graphValidation = validateImplementGraphBoundary(graphFromPlan(plan));
1145
1241
  if (!graphValidation.ok) {
1146
1242
  throw new DesignPlanValidationError(
@@ -1158,7 +1254,12 @@ function renderTasks(plan: ImplementPlan): string {
1158
1254
  lines.push(
1159
1255
  ` - Depends on: ${task.dependsOn.length > 0 ? task.dependsOn.join(", ") : "[]"}`,
1160
1256
  );
1161
- lines.push(` - Red verification: ${task.phases.red.verification.id}`);
1257
+ if (!task.verificationMode || task.verificationMode === "behavior")
1258
+ lines.push(` - Red verification: ${task.phases.red.verification.id}`);
1259
+ else
1260
+ lines.push(
1261
+ ` - Evidence mode: ${task.verificationMode}; baseline and postconditions required`,
1262
+ );
1162
1263
  lines.push(` - Green verification: ${task.phases.green.verification.id}`);
1163
1264
  }
1164
1265
  lines.push("");
@@ -1167,10 +1268,16 @@ function renderTasks(plan: ImplementPlan): string {
1167
1268
 
1168
1269
  export function compileImplementPlan(
1169
1270
  draft: unknown,
1170
- options: { consumerRoot: string },
1271
+ options: { consumerRoot: string; bindExecutionInputs?: boolean },
1171
1272
  ): CompiledDelivery {
1172
1273
  try {
1173
- const plan = normalizeDraft(draft);
1274
+ if (options.bindExecutionInputs)
1275
+ draft = bindDraftVerificationInputs(options.consumerRoot, draft);
1276
+ const normalized = normalizeDraft(draft);
1277
+ const plan =
1278
+ isRecord(draft) && legacyPlans.get(draft) === JSON.stringify(draft)
1279
+ ? (draft as unknown as ImplementPlan)
1280
+ : normalized;
1174
1281
  const graph = graphFromPlan(plan);
1175
1282
  const readiness = assessImplementGraphReadiness(
1176
1283
  options.consumerRoot,
@@ -1217,7 +1324,10 @@ export function compileImplementPlan(
1217
1324
  }
1218
1325
  }
1219
1326
 
1220
- export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
1327
+ export function parseImplementPlan(
1328
+ bytes: Uint8Array,
1329
+ options: DeliveryReadOptions = {},
1330
+ ): ImplementPlan {
1221
1331
  const text = new TextDecoder().decode(bytes);
1222
1332
  let parsed: unknown;
1223
1333
  try {
@@ -1226,8 +1336,15 @@ export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {
1226
1336
  throw new Error("delivery-plan-json-invalid");
1227
1337
  }
1228
1338
  const normalized = normalizeDraft(parsed);
1229
- if (`${canonicalJson(normalized)}\n` !== text) {
1339
+ if (`${canonicalJson(normalized)}\n` === text) return normalized;
1340
+ if (
1341
+ !options.allowLegacyOrder ||
1342
+ `${canonicalJson(parsed)}\n` !== text ||
1343
+ !sameNormalized(normalized, parsed, options)
1344
+ ) {
1230
1345
  throw new Error("delivery-plan-not-canonical");
1231
1346
  }
1232
- return normalized;
1347
+ // Keep proof-bound historical bytes unchanged; a mutation invalidates this association.
1348
+ legacyPlans.set(parsed as object, JSON.stringify(parsed));
1349
+ return parsed as ImplementPlan;
1233
1350
  }
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import {
3
4
  lstatSync,
@@ -8,6 +9,10 @@ import {
8
9
  writeFileSync,
9
10
  } from "node:fs";
10
11
  import path from "node:path";
12
+ import {
13
+ type ChangeContract,
14
+ normalizeChangeContract,
15
+ } from "./change-contract.ts";
11
16
  import { isValidRelativePath } from "./contracts.ts";
12
17
  import {
13
18
  assessDeliveryTraceability,
@@ -28,6 +33,7 @@ import {
28
33
  type DesignStatusProjection,
29
34
  } from "./design-journal.ts";
30
35
  import { OpenSpecCliError, type OpenSpecDiagnostic } from "./openspec-cli.ts";
36
+ import { canonicalJson } from "./run-state.ts";
31
37
  import { RunStore } from "./run-store.ts";
32
38
  import { observeSafePath } from "./safe-path.ts";
33
39
  import type { ResolvedStateRoot } from "./state-root.ts";
@@ -39,6 +45,20 @@ const MAX_DESIGN_FILE_BYTES = 16 * 1024 * 1024;
39
45
  const MAX_DESIGN_REQUIREMENT_BYTES = 64 * 1024;
40
46
  const MAX_DESIGN_CONTRACT_BYTES = 256 * 1024;
41
47
 
48
+ function validGateContract(value: unknown): boolean {
49
+ if (typeof value === "string")
50
+ return (
51
+ value.trim().length > 0 &&
52
+ Buffer.byteLength(value, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
53
+ );
54
+ try {
55
+ normalizeChangeContract(value);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
42
62
  export interface DesignOpenSpecInspection {
43
63
  change: string;
44
64
  schema: string;
@@ -82,7 +102,7 @@ export type DesignControlRequest =
82
102
  runId: string;
83
103
  operationId: string;
84
104
  gate: "gate-a";
85
- contract: string;
105
+ contract: string | ChangeContract;
86
106
  }
87
107
  | {
88
108
  operation: "approve-gate";
@@ -275,9 +295,7 @@ export function validateDesignControlRequest(
275
295
  "gate",
276
296
  "contract",
277
297
  ]) &&
278
- typeof value.contract === "string" &&
279
- value.contract.trim().length > 0 &&
280
- Buffer.byteLength(value.contract, "utf8") <= MAX_DESIGN_CONTRACT_BYTES
298
+ validGateContract(value.contract)
281
299
  ? { ok: true, value: structuredClone(value) as DesignControlRequest }
282
300
  : { ok: false, code: "invalid-design-control-request" };
283
301
  }
@@ -459,6 +477,7 @@ export class DesignController {
459
477
  readonly #runs: RunStore;
460
478
  readonly #inspectOpenSpec: DesignControllerOptions["inspectOpenSpec"];
461
479
  #closed = false;
480
+ readonly #authority = new AsyncLocalStorage<() => void>();
462
481
 
463
482
  private constructor(options: DesignControllerOptions) {
464
483
  this.consumerRoot = path.resolve(options.consumerRoot);
@@ -485,7 +504,22 @@ export class DesignController {
485
504
  return new DesignController(options);
486
505
  }
487
506
 
507
+ executeWithAuthority(
508
+ value: unknown,
509
+ assertAuthority: () => void,
510
+ ): Promise<Record<string, unknown>> {
511
+ return this.#authority.run(assertAuthority, () => this.execute(value));
512
+ }
513
+
514
+ #assertFinalizationLease(
515
+ lease: Parameters<DesignJournal["assertFinalizationLease"]>[0],
516
+ ): void {
517
+ this.#authority.getStore()?.();
518
+ this.#journal.assertFinalizationLease(lease);
519
+ }
520
+
488
521
  #assertOpen(): void {
522
+ this.#authority.getStore()?.();
489
523
  if (this.#closed) throw new Error("design-controller-closed");
490
524
  }
491
525
 
@@ -631,6 +665,7 @@ export class DesignController {
631
665
  throw error;
632
666
  }
633
667
  if (run.state === "created") {
668
+ this.#journal.inheritFinalizedAuthority(run.runId);
634
669
  this.#runs.transition({
635
670
  runId: run.runId,
636
671
  to: "paused",
@@ -831,7 +866,21 @@ export class DesignController {
831
866
  { code: "design-plan-draft-invalid", field: "plan-draft.json" },
832
867
  ]);
833
868
  }
869
+ if (status.changeContract) {
870
+ if (!draft || typeof draft !== "object" || Array.isArray(draft))
871
+ throw new Error("change-contract-invalid");
872
+ const record = draft as Record<string, unknown>;
873
+ if (
874
+ record.changeContract !== undefined &&
875
+ canonicalJson(normalizeChangeContract(record.changeContract)) !==
876
+ canonicalJson(status.changeContract)
877
+ )
878
+ throw new Error("change-contract-mismatch");
879
+ record.changeContract = status.changeContract;
880
+ } else if (draft && typeof draft === "object" && "changeContract" in draft)
881
+ throw new Error("change-contract-gate-a-required");
834
882
  const compiled = compileImplementPlan(draft, {
883
+ bindExecutionInputs: true,
835
884
  consumerRoot: this.consumerRoot,
836
885
  });
837
886
  if (compiled.plan.changeId !== status.change) {
@@ -882,9 +931,15 @@ export class DesignController {
882
931
  if (!change) throw new Error("design-change-required");
883
932
  const readyPath = safeRelative(change, "ready.yaml");
884
933
  const gateAPath = safeRelative(change, "gate-a.yaml");
885
- removeSafeFile(this.consumerRoot, readyPath);
886
- removeSafeFile(this.consumerRoot, gateAPath);
887
- this.#journal.assertFinalizationLease(lease);
934
+ const unchanged =
935
+ status.gates.gateB.current &&
936
+ status.plan?.rawSha256 === compiled.rawSha256 &&
937
+ status.plan.canonicalHash === compiled.planHash;
938
+ if (!unchanged) {
939
+ removeSafeFile(this.consumerRoot, readyPath);
940
+ removeSafeFile(this.consumerRoot, gateAPath);
941
+ }
942
+ this.#assertFinalizationLease(lease);
888
943
  atomicWrite(
889
944
  this.consumerRoot,
890
945
  safeRelative(change, "implement-plan.json"),
@@ -897,6 +952,7 @@ export class DesignController {
897
952
  rawSha256: compiled.rawSha256,
898
953
  canonicalHash: compiled.planHash,
899
954
  lease,
955
+ certify: true,
900
956
  });
901
957
  } finally {
902
958
  this.#journal.releaseFinalizationLease(lease);
@@ -970,7 +1026,9 @@ export class DesignController {
970
1026
  }
971
1027
  let compiled: ReturnType<typeof compileImplementPlan> | undefined;
972
1028
  try {
973
- const plan = parseImplementPlan(installedPlanBytes);
1029
+ const plan = parseImplementPlan(installedPlanBytes, {
1030
+ allowLegacyOrder: true,
1031
+ });
974
1032
  compiled = compileImplementPlan(plan, {
975
1033
  consumerRoot: this.consumerRoot,
976
1034
  });
@@ -1067,14 +1125,14 @@ export class DesignController {
1067
1125
  traceability: traceability.value,
1068
1126
  });
1069
1127
  readyHash = readyReceipt.rawSha256;
1070
- this.#journal.assertFinalizationLease(lease);
1128
+ this.#assertFinalizationLease(lease);
1071
1129
  atomicWrite(this.consumerRoot, gateAPath, gateAReceipt.bytes);
1072
1130
  atomicWrite(this.consumerRoot, readyPath, readyReceipt.bytes);
1073
1131
  readyInstalled = true;
1074
1132
  const installedGateA = readSafeFile(this.consumerRoot, gateAPath);
1075
1133
  const installedReady = readSafeFile(this.consumerRoot, readyPath);
1076
- parseGateAReceipt(installedGateA);
1077
- parseReadyReceipt(installedReady);
1134
+ parseGateAReceipt(installedGateA, { allowLegacyOrder: true });
1135
+ parseReadyReceipt(installedReady, { allowLegacyOrder: true });
1078
1136
  if (
1079
1137
  !Buffer.from(installedGateA).equals(Buffer.from(gateAReceipt.bytes)) ||
1080
1138
  !Buffer.from(installedReady).equals(Buffer.from(readyReceipt.bytes))