@abelxiaoxing/cadence 1.2.2 → 1.2.4

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.
@@ -19,9 +19,7 @@ import {
19
19
  } from "./implement-graph.ts";
20
20
  import { validateVerificationAdapterCapability } from "./verification-capability.ts";
21
21
 
22
- export const IMPLEMENT_PLAN_SCHEMA_VERSION = 3 as const;
23
22
  export const IMPLEMENT_PLAN_PATH = "implement-plan.json" as const;
24
- export const DELIVERY_RECEIPT_VERSION = 4 as const;
25
23
  export const GATE_A_RECEIPT_PATH = "gate-a.yaml" as const;
26
24
  export const READY_RECEIPT_PATH = "ready.yaml" as const;
27
25
 
@@ -42,7 +40,6 @@ export interface GateApprovalProof {
42
40
  }
43
41
 
44
42
  export interface GateAReceipt {
45
- receiptVersion: typeof DELIVERY_RECEIPT_VERSION;
46
43
  change: string;
47
44
  schema: string;
48
45
  approval: GateApprovalProof;
@@ -58,7 +55,6 @@ export interface DeliveryTraceability {
58
55
  }
59
56
 
60
57
  export interface ReadyReceipt {
61
- receiptVersion: typeof DELIVERY_RECEIPT_VERSION;
62
58
  change: string;
63
59
  schema: string;
64
60
  deliveryRevision: number;
@@ -98,6 +94,79 @@ export class DeliveryValidationError extends Error {
98
94
  }
99
95
  }
100
96
 
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
+ const SAFE_PLAN_ERROR_CODE = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
132
+
133
+ function planErrorCode(error: unknown): string {
134
+ const message = error instanceof Error ? error.message : "";
135
+ return SAFE_PLAN_ERROR_CODE.test(message) ? message : "delivery-plan-invalid";
136
+ }
137
+
138
+ function planDiagnosticField(code: string): string | undefined {
139
+ const fields: Record<string, string> = {
140
+ "delivery-plan-invalid-task": "task",
141
+ "delivery-plan-invalid-task-phases": "phases",
142
+ "delivery-plan-invalid-phase": "phases",
143
+ "delivery-plan-empty-phase-boundary": "phases",
144
+ "delivery-plan-write-delete-overlap": "phases",
145
+ "delivery-verification-invalid": "verification",
146
+ "delivery-task-verification-invalid": "affectedVerification",
147
+ "delivery-agents-checkpoint-mismatch": "verification.agentsCheckpoint",
148
+ };
149
+ return fields[code];
150
+ }
151
+
152
+ function asPlanValidationError(
153
+ error: unknown,
154
+ context: Omit<DesignPlanDiagnostic, "code"> = {},
155
+ ): DesignPlanValidationError {
156
+ if (error instanceof DesignPlanValidationError) {
157
+ const diagnostics = error.diagnostics.map((diagnostic) => ({
158
+ ...context,
159
+ ...diagnostic,
160
+ }));
161
+ return new DesignPlanValidationError(error.message, diagnostics);
162
+ }
163
+ const code = planErrorCode(error);
164
+ const field = planDiagnosticField(code);
165
+ return new DesignPlanValidationError(code, [
166
+ { code, ...context, ...(field ? { field } : {}) },
167
+ ]);
168
+ }
169
+
101
170
  export function assessDeliveryTraceability(input: {
102
171
  tasksMarkdown: string;
103
172
  specs: readonly { path: string; text: string }[];
@@ -197,7 +266,7 @@ export interface PlanVerification {
197
266
  target: "task-red-contracts";
198
267
  affected: "task-affected-contracts";
199
268
  fullSuite: StructuredVerificationContract;
200
- failureIdentity: "normalized-v1";
269
+ failureIdentity: "normalized";
201
270
  };
202
271
  change: {
203
272
  affected: "task-affected-contracts";
@@ -235,7 +304,6 @@ export interface PlanTracking {
235
304
  }
236
305
 
237
306
  export interface PlanDraft {
238
- schemaVersion: typeof IMPLEMENT_PLAN_SCHEMA_VERSION;
239
307
  changeId: string;
240
308
  tasks: PlanTaskDraft[];
241
309
  outputs: ImplementGraphOutput[];
@@ -243,9 +311,7 @@ export interface PlanDraft {
243
311
  tracking: PlanTracking;
244
312
  }
245
313
 
246
- export interface ImplementPlan extends PlanDraft {
247
- schemaVersion: typeof IMPLEMENT_PLAN_SCHEMA_VERSION;
248
- }
314
+ export type ImplementPlan = PlanDraft;
249
315
 
250
316
  export interface CompiledDelivery {
251
317
  plan: ImplementPlan;
@@ -256,7 +322,6 @@ export interface CompiledDelivery {
256
322
  receipt: {
257
323
  plan: {
258
324
  path: typeof IMPLEMENT_PLAN_PATH;
259
- schemaVersion: typeof IMPLEMENT_PLAN_SCHEMA_VERSION;
260
325
  rawSha256: string;
261
326
  canonicalHash: string;
262
327
  };
@@ -360,7 +425,6 @@ export function compileGateAReceipt(input: {
360
425
  }): { receipt: GateAReceipt; bytes: Uint8Array; rawSha256: string } {
361
426
  requireReceiptIdentity(input.change, input.schema);
362
427
  const receipt: GateAReceipt = {
363
- receiptVersion: DELIVERY_RECEIPT_VERSION,
364
428
  change: input.change,
365
429
  schema: input.schema,
366
430
  approval: normalizeGateProof(input.approval),
@@ -381,14 +445,7 @@ export function parseGateAReceipt(bytes: Uint8Array): GateAReceipt {
381
445
  );
382
446
  if (
383
447
  !isRecord(receipt) ||
384
- !exactKeys(receipt, [
385
- "receiptVersion",
386
- "change",
387
- "schema",
388
- "approval",
389
- "artifacts",
390
- ]) ||
391
- receipt.receiptVersion !== DELIVERY_RECEIPT_VERSION ||
448
+ !exactKeys(receipt, ["change", "schema", "approval", "artifacts"]) ||
392
449
  !isRecord(receipt.approval) ||
393
450
  !exactKeys(receipt.approval, ["revision", "contractHash", "recordHash"])
394
451
  ) {
@@ -454,7 +511,6 @@ export function compileReadyReceipt(input: {
454
511
  throw new Error("delivery-receipt-invalid");
455
512
  }
456
513
  const receipt: ReadyReceipt = {
457
- receiptVersion: DELIVERY_RECEIPT_VERSION,
458
514
  change: input.change,
459
515
  schema: input.schema,
460
516
  deliveryRevision: input.deliveryRevision,
@@ -489,7 +545,6 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
489
545
  if (
490
546
  !isRecord(receipt) ||
491
547
  !exactKeys(receipt, [
492
- "receiptVersion",
493
548
  "change",
494
549
  "schema",
495
550
  "deliveryRevision",
@@ -500,7 +555,6 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
500
555
  "traceability",
501
556
  "openspec",
502
557
  ]) ||
503
- receipt.receiptVersion !== DELIVERY_RECEIPT_VERSION ||
504
558
  !Number.isSafeInteger(receipt.deliveryRevision) ||
505
559
  receipt.deliveryRevision < 1 ||
506
560
  !isRecord(receipt.approvals) ||
@@ -516,14 +570,8 @@ export function parseReadyReceipt(bytes: Uint8Array): ReadyReceipt {
516
570
  "recordHash",
517
571
  ]) ||
518
572
  !isRecord(receipt.plan) ||
519
- !exactKeys(receipt.plan, [
520
- "path",
521
- "schemaVersion",
522
- "rawSha256",
523
- "canonicalHash",
524
- ]) ||
573
+ !exactKeys(receipt.plan, ["path", "rawSha256", "canonicalHash"]) ||
525
574
  receipt.plan.path !== IMPLEMENT_PLAN_PATH ||
526
- receipt.plan.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
527
575
  !SHA256.test(receipt.plan.rawSha256) ||
528
576
  !SHA256.test(receipt.plan.canonicalHash) ||
529
577
  !isRecord(receipt.verificationClosure) ||
@@ -603,6 +651,9 @@ function normalizePhase(value: unknown): PlanPhaseDraft {
603
651
  }
604
652
  const write = sortStrings(value.write as string[]);
605
653
  const deletions = sortStrings(value.delete as string[]);
654
+ if (write.length + deletions.length === 0) {
655
+ throw new Error("delivery-plan-empty-phase-boundary");
656
+ }
606
657
  if (write.some((candidate) => deletions.includes(candidate))) {
607
658
  throw new Error("delivery-plan-write-delete-overlap");
608
659
  }
@@ -625,60 +676,97 @@ function normalizePhase(value: unknown): PlanPhaseDraft {
625
676
  }
626
677
 
627
678
  function normalizeTask(value: unknown): PlanTaskDraft {
628
- if (
629
- !isRecord(value) ||
630
- !isRecord(value.phases) ||
631
- !isRecord(value.affectedVerification) ||
632
- !isRecord(value.repairVerification)
633
- ) {
634
- throw new Error("delivery-plan-invalid-task");
635
- }
636
- const phases = value.phases;
637
- if (!Object.hasOwn(phases, "red") || !Object.hasOwn(phases, "green")) {
638
- throw new Error("delivery-plan-invalid-task-phases");
639
- }
640
- const cloned = structuredClone(value) as unknown as PlanTaskDraft;
641
- const affectedVerification = normalizeVerification(
642
- value.affectedVerification,
643
- );
644
- const repairVerification = normalizeVerification(value.repairVerification);
645
- if (
646
- affectedVerification.classification !== "expected-green" ||
647
- repairVerification.classification !== "expected-green"
648
- ) {
649
- throw new Error("delivery-task-verification-invalid");
679
+ const taskId =
680
+ isRecord(value) && typeof value.taskId === "string"
681
+ ? value.taskId
682
+ : undefined;
683
+ const context = taskId ? { taskId } : {};
684
+ try {
685
+ if (
686
+ !isRecord(value) ||
687
+ !isRecord(value.phases) ||
688
+ !isRecord(value.affectedVerification) ||
689
+ !isRecord(value.repairVerification)
690
+ ) {
691
+ throw new Error("delivery-plan-invalid-task");
692
+ }
693
+ const phases = value.phases;
694
+ if (!Object.hasOwn(phases, "red") || !Object.hasOwn(phases, "green")) {
695
+ throw new Error("delivery-plan-invalid-task-phases");
696
+ }
697
+ const cloned = structuredClone(value) as unknown as PlanTaskDraft;
698
+ let affectedVerification: StructuredVerificationContract;
699
+ try {
700
+ affectedVerification = normalizeVerification(value.affectedVerification);
701
+ } catch (error) {
702
+ throw asPlanValidationError(error, {
703
+ ...context,
704
+ field: "affectedVerification",
705
+ });
706
+ }
707
+ let repairVerification: StructuredVerificationContract;
708
+ try {
709
+ repairVerification = normalizeVerification(value.repairVerification);
710
+ } catch (error) {
711
+ throw asPlanValidationError(error, {
712
+ ...context,
713
+ field: "repairVerification",
714
+ });
715
+ }
716
+ if (
717
+ affectedVerification.classification !== "expected-green" ||
718
+ repairVerification.classification !== "expected-green"
719
+ ) {
720
+ throw new Error("delivery-task-verification-invalid");
721
+ }
722
+ const normalizeTaskPhase = (
723
+ phase: "red" | "green" | "refactor",
724
+ candidate: unknown,
725
+ ) => {
726
+ try {
727
+ return normalizePhase(candidate);
728
+ } catch (error) {
729
+ throw asPlanValidationError(error, {
730
+ ...context,
731
+ phase,
732
+ field: `phases.${phase}`,
733
+ });
734
+ }
735
+ };
736
+ return {
737
+ ...cloned,
738
+ dependsOn: sortStrings(cloned.dependsOn),
739
+ roots: sortStrings(cloned.roots),
740
+ phases: {
741
+ red: normalizeTaskPhase("red", phases.red),
742
+ green: normalizeTaskPhase("green", phases.green),
743
+ ...(phases.refactor !== undefined
744
+ ? { refactor: normalizeTaskPhase("refactor", phases.refactor) }
745
+ : {}),
746
+ },
747
+ scheduling: {
748
+ conflicts: sortStrings(cloned.scheduling.conflicts),
749
+ resources: sortStrings(cloned.scheduling.resources),
750
+ },
751
+ approvedDependencies: sortStrings(cloned.approvedDependencies),
752
+ affectedVerification,
753
+ repairVerification,
754
+ impactClosure: {
755
+ changedSurfaces: sortStrings(
756
+ cloned.impactClosure.changedSurfaces,
757
+ ) as PlanTaskDraft["impactClosure"]["changedSurfaces"],
758
+ searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
759
+ relatedTests: [...cloned.impactClosure.relatedTests].sort(
760
+ (left, right) =>
761
+ left.path.localeCompare(right.path) ||
762
+ left.disposition.localeCompare(right.disposition),
763
+ ),
764
+ affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
765
+ },
766
+ };
767
+ } catch (error) {
768
+ throw asPlanValidationError(error, context);
650
769
  }
651
- return {
652
- ...cloned,
653
- dependsOn: sortStrings(cloned.dependsOn),
654
- roots: sortStrings(cloned.roots),
655
- phases: {
656
- red: normalizePhase(phases.red),
657
- green: normalizePhase(phases.green),
658
- ...(phases.refactor !== undefined
659
- ? { refactor: normalizePhase(phases.refactor) }
660
- : {}),
661
- },
662
- scheduling: {
663
- conflicts: sortStrings(cloned.scheduling.conflicts),
664
- resources: sortStrings(cloned.scheduling.resources),
665
- },
666
- approvedDependencies: sortStrings(cloned.approvedDependencies),
667
- affectedVerification,
668
- repairVerification,
669
- impactClosure: {
670
- changedSurfaces: sortStrings(
671
- cloned.impactClosure.changedSurfaces,
672
- ) as PlanTaskDraft["impactClosure"]["changedSurfaces"],
673
- searchEvidence: sortStrings(cloned.impactClosure.searchEvidence),
674
- relatedTests: [...cloned.impactClosure.relatedTests].sort(
675
- (left, right) =>
676
- left.path.localeCompare(right.path) ||
677
- left.disposition.localeCompare(right.disposition),
678
- ),
679
- affectedSuite: sortStrings(cloned.impactClosure.affectedSuite),
680
- },
681
- };
682
770
  }
683
771
 
684
772
  function graphFromPlan(plan: ImplementPlan): ImplementGraphBoundary {
@@ -870,7 +958,7 @@ function normalizeVerificationPlan(value: unknown): PlanVerification {
870
958
  ]) ||
871
959
  value.baseline.target !== "task-red-contracts" ||
872
960
  value.baseline.affected !== "task-affected-contracts" ||
873
- value.baseline.failureIdentity !== "normalized-v1" ||
961
+ value.baseline.failureIdentity !== "normalized" ||
874
962
  !isRecord(value.change) ||
875
963
  !exactKeys(value.change, ["affected", "fullSuite", "postApply"]) ||
876
964
  value.change.affected !== "task-affected-contracts" ||
@@ -919,7 +1007,7 @@ function normalizeVerificationPlan(value: unknown): PlanVerification {
919
1007
  target: "task-red-contracts",
920
1008
  affected: "task-affected-contracts",
921
1009
  fullSuite: normalizeExpectedGreenVerification(value.baseline.fullSuite),
922
- failureIdentity: "normalized-v1",
1010
+ failureIdentity: "normalized",
923
1011
  },
924
1012
  change: {
925
1013
  affected: "task-affected-contracts",
@@ -974,14 +1062,12 @@ function normalizeDraft(value: unknown): ImplementPlan {
974
1062
  if (
975
1063
  !isRecord(value) ||
976
1064
  !exactKeys(value, [
977
- "schemaVersion",
978
1065
  "changeId",
979
1066
  "tasks",
980
1067
  "outputs",
981
1068
  "verification",
982
1069
  "tracking",
983
1070
  ]) ||
984
- value.schemaVersion !== IMPLEMENT_PLAN_SCHEMA_VERSION ||
985
1071
  typeof value.changeId !== "string" ||
986
1072
  !CHANGE_NAME.test(value.changeId) ||
987
1073
  !Array.isArray(value.tasks) ||
@@ -989,8 +1075,26 @@ function normalizeDraft(value: unknown): ImplementPlan {
989
1075
  ) {
990
1076
  throw new Error("delivery-plan-invalid");
991
1077
  }
1078
+ const draftGraph = validateImplementGraphBoundary({
1079
+ changeId: value.changeId,
1080
+ tasks: value.tasks.map((candidate) => {
1081
+ if (!isRecord(candidate)) return candidate;
1082
+ const {
1083
+ affectedVerification: _affectedVerification,
1084
+ repairVerification: _repairVerification,
1085
+ ...boundary
1086
+ } = candidate;
1087
+ return boundary;
1088
+ }),
1089
+ outputs: value.outputs,
1090
+ });
1091
+ if (!draftGraph.ok) {
1092
+ throw new DesignPlanValidationError(
1093
+ `delivery-plan-invalid:${draftGraph.reason}`,
1094
+ [draftGraph.diagnostic],
1095
+ );
1096
+ }
992
1097
  const plan: ImplementPlan = {
993
- schemaVersion: IMPLEMENT_PLAN_SCHEMA_VERSION,
994
1098
  changeId: value.changeId,
995
1099
  tasks: value.tasks
996
1100
  .map(normalizeTask)
@@ -1039,7 +1143,10 @@ function normalizeDraft(value: unknown): ImplementPlan {
1039
1143
  }
1040
1144
  const graphValidation = validateImplementGraphBoundary(graphFromPlan(plan));
1041
1145
  if (!graphValidation.ok) {
1042
- throw new Error(`delivery-plan-invalid:${graphValidation.reason}`);
1146
+ throw new DesignPlanValidationError(
1147
+ `delivery-plan-invalid:${graphValidation.reason}`,
1148
+ [graphValidation.diagnostic],
1149
+ );
1043
1150
  }
1044
1151
  return plan;
1045
1152
  }
@@ -1062,44 +1169,52 @@ export function compileImplementPlan(
1062
1169
  draft: unknown,
1063
1170
  options: { consumerRoot: string },
1064
1171
  ): CompiledDelivery {
1065
- const plan = normalizeDraft(draft);
1066
- const graph = graphFromPlan(plan);
1067
- const readiness = assessImplementGraphReadiness(options.consumerRoot, graph, {
1068
- completedTasks: [],
1069
- blockedTasks: [],
1070
- appliedPhases: [],
1071
- });
1072
- const diagnostics = [
1073
- ...readiness.closure.diagnostics,
1074
- ...deliveryVerificationDiagnostics(plan, options.consumerRoot),
1075
- ];
1076
- if (diagnostics.length > 0) {
1077
- throw new Error(
1078
- `delivery-plan-not-executable:${canonicalJson(diagnostics)}`,
1172
+ try {
1173
+ const plan = normalizeDraft(draft);
1174
+ const graph = graphFromPlan(plan);
1175
+ const readiness = assessImplementGraphReadiness(
1176
+ options.consumerRoot,
1177
+ graph,
1178
+ {
1179
+ completedTasks: [],
1180
+ blockedTasks: [],
1181
+ appliedPhases: [],
1182
+ },
1079
1183
  );
1080
- }
1081
- const canonical = canonicalJson(plan);
1082
- const bytes = new TextEncoder().encode(`${canonical}\n`);
1083
- const rawSha256 = createHash("sha256").update(bytes).digest("hex");
1084
- const planHash = hashCanonicalValue(plan);
1085
- const closure = structuredClone(readiness.closure);
1086
- return {
1087
- plan,
1088
- bytes,
1089
- rawSha256,
1090
- planHash,
1091
- closure,
1092
- receipt: {
1093
- plan: {
1094
- path: IMPLEMENT_PLAN_PATH,
1095
- schemaVersion: IMPLEMENT_PLAN_SCHEMA_VERSION,
1096
- rawSha256,
1097
- canonicalHash: planHash,
1184
+ const diagnostics = [
1185
+ ...readiness.closure.diagnostics,
1186
+ ...deliveryVerificationDiagnostics(plan, options.consumerRoot),
1187
+ ] as DesignPlanDiagnostic[];
1188
+ if (diagnostics.length > 0) {
1189
+ throw new DesignPlanValidationError(
1190
+ `delivery-plan-not-executable:${canonicalJson(diagnostics)}`,
1191
+ diagnostics,
1192
+ );
1193
+ }
1194
+ const canonical = canonicalJson(plan);
1195
+ const bytes = new TextEncoder().encode(`${canonical}\n`);
1196
+ const rawSha256 = createHash("sha256").update(bytes).digest("hex");
1197
+ const planHash = hashCanonicalValue(plan);
1198
+ const closure = structuredClone(readiness.closure);
1199
+ return {
1200
+ plan,
1201
+ bytes,
1202
+ rawSha256,
1203
+ planHash,
1204
+ closure,
1205
+ receipt: {
1206
+ plan: {
1207
+ path: IMPLEMENT_PLAN_PATH,
1208
+ rawSha256,
1209
+ canonicalHash: planHash,
1210
+ },
1211
+ verificationClosure: structuredClone(closure),
1098
1212
  },
1099
- verificationClosure: structuredClone(closure),
1100
- },
1101
- tasksMarkdown: renderTasks(plan),
1102
- };
1213
+ tasksMarkdown: renderTasks(plan),
1214
+ };
1215
+ } catch (error) {
1216
+ throw asPlanValidationError(error);
1217
+ }
1103
1218
  }
1104
1219
 
1105
1220
  export function parseImplementPlan(bytes: Uint8Array): ImplementPlan {