@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.
@@ -15,6 +15,7 @@ import {
15
15
  compileImplementPlan,
16
16
  compileReadyReceipt,
17
17
  type DeliveryArtifactBinding,
18
+ DesignPlanValidationError,
18
19
  type GateApprovalProof,
19
20
  parseGateAReceipt,
20
21
  parseImplementPlan,
@@ -88,6 +89,10 @@ export type DesignControlRequest =
88
89
  operationId: string;
89
90
  gate: "gate-b";
90
91
  }
92
+ | {
93
+ operation: "validate-plan-draft";
94
+ runId: string;
95
+ }
91
96
  | {
92
97
  operation: "compile-plan";
93
98
  runId: string;
@@ -198,7 +203,10 @@ export function validateDesignControlRequest(
198
203
  ? { ok: true, value: structuredClone(value) as DesignControlRequest }
199
204
  : { ok: false, code: "invalid-design-control-request" };
200
205
  }
201
- if (value.operation === "status") {
206
+ if (
207
+ value.operation === "status" ||
208
+ value.operation === "validate-plan-draft"
209
+ ) {
202
210
  return exactKeys(value, ["operation", "runId"]) &&
203
211
  typeof value.runId === "string" &&
204
212
  IDENTIFIER.test(value.runId)
@@ -448,12 +456,21 @@ export class DesignController {
448
456
  private constructor(options: DesignControllerOptions) {
449
457
  this.consumerRoot = path.resolve(options.consumerRoot);
450
458
  this.#runs = RunStore.open(options.stateRoot);
451
- this.#journal = DesignJournal.open(options.stateRoot, {
452
- ...(options.now ? { now: options.now } : {}),
453
- ...(options.finalizationLeaseMs !== undefined
454
- ? { finalizationLeaseMs: options.finalizationLeaseMs }
455
- : {}),
456
- });
459
+ try {
460
+ this.#journal = DesignJournal.open(options.stateRoot, {
461
+ ...(options.now ? { now: options.now } : {}),
462
+ ...(options.finalizationLeaseMs !== undefined
463
+ ? { finalizationLeaseMs: options.finalizationLeaseMs }
464
+ : {}),
465
+ });
466
+ } catch (error) {
467
+ try {
468
+ this.#runs.close();
469
+ } catch {
470
+ // The Design journal initialization failure remains authoritative.
471
+ }
472
+ throw error;
473
+ }
457
474
  this.#inspectOpenSpec = options.inspectOpenSpec;
458
475
  }
459
476
 
@@ -508,6 +525,8 @@ export class DesignController {
508
525
  return this.#journal.recordDecision(request);
509
526
  case "approve-gate":
510
527
  return this.#journal.approveGate(request);
528
+ case "validate-plan-draft":
529
+ return this.#validatePlanDraft(request);
511
530
  case "compile-plan":
512
531
  return this.#compilePlan(request);
513
532
  case "finalize-delivery":
@@ -539,7 +558,7 @@ export class DesignController {
539
558
  const canApproveGate =
540
559
  !design.gates.gateA.current ||
541
560
  (planReadyForGateB && !design.gates.gateB.current);
542
- const legalActions = terminal
561
+ const legalOperations = terminal
543
562
  ? ["status"]
544
563
  : [
545
564
  "status",
@@ -549,7 +568,12 @@ export class DesignController {
549
568
  ? ["bind-change"]
550
569
  : []),
551
570
  ...(design.gates.gateA.current && design.change
552
- ? ["write-artifact", "delete-artifact", "compile-plan"]
571
+ ? [
572
+ "write-artifact",
573
+ "delete-artifact",
574
+ "validate-plan-draft",
575
+ "compile-plan",
576
+ ]
553
577
  : []),
554
578
  ...(design.change &&
555
579
  design.plan &&
@@ -557,7 +581,6 @@ export class DesignController {
557
581
  design.gates.gateB.current
558
582
  ? ["finalize-delivery"]
559
583
  : []),
560
- "finish",
561
584
  ];
562
585
  return {
563
586
  runId: run.runId,
@@ -566,25 +589,40 @@ export class DesignController {
566
589
  state: run.state,
567
590
  completed: run.state === "completed",
568
591
  ...(run.pauseCode ? { pause: { code: run.pauseCode } } : {}),
569
- legalActions,
592
+ legalOperations,
593
+ packetActions: terminal ? [] : ["finish"],
570
594
  design,
571
595
  };
572
596
  }
573
597
 
574
598
  #start(request: Extract<DesignControlRequest, { operation: "start" }>) {
575
599
  const fromRequirement = "requirement" in request;
576
- const run = this.#runs.startRun({
577
- stage: "abel-design",
578
- ...(fromRequirement
579
- ? {
580
- provisionalKey: transientHash(
581
- "abel-design-requirement-v1",
582
- request.requirement,
583
- ),
584
- }
585
- : { change: request.change }),
586
- operationId: request.operationId,
587
- });
600
+ let run: ReturnType<RunStore["startRun"]>;
601
+ try {
602
+ run = this.#runs.startRun({
603
+ stage: "abel-design",
604
+ ...(fromRequirement
605
+ ? {
606
+ provisionalKey: transientHash(
607
+ "abel-design-requirement",
608
+ request.requirement,
609
+ ),
610
+ provisionalAliases: [
611
+ transientHash(
612
+ "abel-design-requirement-v1",
613
+ request.requirement,
614
+ ),
615
+ ],
616
+ }
617
+ : { change: request.change }),
618
+ operationId: request.operationId,
619
+ });
620
+ } catch (error) {
621
+ if (error instanceof Error && error.message === "operation-id-conflict") {
622
+ throw new Error("design-operation-conflict", { cause: error });
623
+ }
624
+ throw error;
625
+ }
588
626
  if (run.state === "created") {
589
627
  this.#runs.transition({
590
628
  runId: run.runId,
@@ -772,6 +810,51 @@ export class DesignController {
772
810
  }
773
811
  }
774
812
 
813
+ #readCompiledDraft(runId: string) {
814
+ const status = this.#journal.status(runId);
815
+ if (!status.change) throw new Error("design-change-required");
816
+ if (!status.gates.gateA.current) throw new Error("design-gate-a-required");
817
+ const draftPath = safeRelative(status.change, "plan-draft.json");
818
+ const draftBytes = readSafeFile(this.consumerRoot, draftPath);
819
+ let draft: unknown;
820
+ try {
821
+ draft = JSON.parse(draftBytes.toString("utf8"));
822
+ } catch {
823
+ throw new DesignPlanValidationError("design-plan-draft-invalid", [
824
+ { code: "design-plan-draft-invalid", field: "plan-draft.json" },
825
+ ]);
826
+ }
827
+ const compiled = compileImplementPlan(draft, {
828
+ consumerRoot: this.consumerRoot,
829
+ });
830
+ if (compiled.plan.changeId !== status.change) {
831
+ throw new DesignPlanValidationError("design-plan-change-mismatch", [
832
+ { code: "design-plan-change-mismatch", field: "changeId" },
833
+ ]);
834
+ }
835
+ return { status, compiled };
836
+ }
837
+
838
+ #validatePlanDraft(
839
+ request: Extract<
840
+ DesignControlRequest,
841
+ { operation: "validate-plan-draft" }
842
+ >,
843
+ ) {
844
+ const { compiled } = this.#readCompiledDraft(request.runId);
845
+ return {
846
+ operation: request.operation,
847
+ runId: request.runId,
848
+ valid: true,
849
+ plan: {
850
+ taskCount: compiled.plan.tasks.length,
851
+ outputCount: compiled.plan.outputs.length,
852
+ rawSha256: compiled.rawSha256,
853
+ canonicalHash: compiled.planHash,
854
+ },
855
+ };
856
+ }
857
+
775
858
  #compilePlan(
776
859
  request: Extract<DesignControlRequest, { operation: "compile-plan" }>,
777
860
  ) {
@@ -787,32 +870,17 @@ export class DesignController {
787
870
  });
788
871
  try {
789
872
  this.#journal.assertNoPendingArtifactOperations(request.runId);
790
- const status = this.#journal.status(request.runId);
791
- if (!status.change) throw new Error("design-change-required");
792
- if (!status.gates.gateA.current)
793
- throw new Error("design-gate-a-required");
794
- const readyPath = safeRelative(status.change, "ready.yaml");
795
- const gateAPath = safeRelative(status.change, "gate-a.yaml");
873
+ const { status, compiled } = this.#readCompiledDraft(request.runId);
874
+ const change = status.change;
875
+ if (!change) throw new Error("design-change-required");
876
+ const readyPath = safeRelative(change, "ready.yaml");
877
+ const gateAPath = safeRelative(change, "gate-a.yaml");
796
878
  removeSafeFile(this.consumerRoot, readyPath);
797
879
  removeSafeFile(this.consumerRoot, gateAPath);
798
- const draftPath = safeRelative(status.change, "plan-draft.json");
799
- const draftBytes = readSafeFile(this.consumerRoot, draftPath);
800
- let draft: unknown;
801
- try {
802
- draft = JSON.parse(draftBytes.toString("utf8"));
803
- } catch {
804
- throw new Error("design-plan-draft-invalid");
805
- }
806
- const compiled = compileImplementPlan(draft as never, {
807
- consumerRoot: this.consumerRoot,
808
- });
809
- if (compiled.plan.changeId !== status.change) {
810
- throw new Error("design-plan-change-mismatch");
811
- }
812
880
  this.#journal.assertFinalizationLease(lease);
813
881
  atomicWrite(
814
882
  this.consumerRoot,
815
- safeRelative(status.change, "implement-plan.json"),
883
+ safeRelative(change, "implement-plan.json"),
816
884
  compiled.bytes,
817
885
  );
818
886
  return this.#journal.recordCompiledPlan({
@@ -317,12 +317,21 @@ export class DesignJournal {
317
317
  }
318
318
  const stateRoot = prepareStateRoot(unresolved);
319
319
  const database = new DatabaseSync(stateRoot.databasePath);
320
- database.exec("PRAGMA foreign_keys = ON");
321
- database.exec("PRAGMA journal_mode = WAL");
322
- database.exec("PRAGMA synchronous = FULL");
323
- database.exec("PRAGMA busy_timeout = 5000");
324
- database.exec(SCHEMA);
325
- return new DesignJournal(stateRoot, database, options);
320
+ try {
321
+ database.exec("PRAGMA foreign_keys = ON");
322
+ database.exec("PRAGMA journal_mode = WAL");
323
+ database.exec("PRAGMA synchronous = FULL");
324
+ database.exec("PRAGMA busy_timeout = 5000");
325
+ database.exec(SCHEMA);
326
+ return new DesignJournal(stateRoot, database, options);
327
+ } catch (error) {
328
+ try {
329
+ database.close();
330
+ } catch {
331
+ // The Design journal initialization failure remains authoritative.
332
+ }
333
+ throw error;
334
+ }
326
335
  }
327
336
 
328
337
  #assertOpen(): void {
@@ -337,7 +346,11 @@ export class DesignJournal {
337
346
  this.#database.exec("COMMIT");
338
347
  return result;
339
348
  } catch (error) {
340
- this.#database.exec("ROLLBACK");
349
+ try {
350
+ this.#database.exec("ROLLBACK");
351
+ } catch {
352
+ // The original operation or commit failure remains authoritative.
353
+ }
341
354
  throw error;
342
355
  }
343
356
  }
@@ -899,7 +912,7 @@ export class DesignJournal {
899
912
  }): Record<string, unknown> {
900
913
  requireIdentifier(input.decisionId, "design-decision-id");
901
914
  const decisionContractHash = contractHash(
902
- "abel-design-decision-v1",
915
+ "abel-design-decision",
903
916
  input.contract,
904
917
  );
905
918
  if (input.category !== "behavior" && input.category !== "technical") {
@@ -1024,7 +1037,7 @@ export class DesignJournal {
1024
1037
  }
1025
1038
  const gateAContractHash =
1026
1039
  input.gate === "gate-a"
1027
- ? contractHash("abel-design-gate-a-v1", input.contract)
1040
+ ? contractHash("abel-design-gate-a", input.contract)
1028
1041
  : undefined;
1029
1042
  return this.#transaction(() => {
1030
1043
  this.#run(input.runId, true);
@@ -56,7 +56,7 @@ function updateManifest(
56
56
 
57
57
  function recursiveDirectoryManifest(abs: string): string {
58
58
  const hash = createHash("sha256");
59
- updateManifest(hash, "cadence-directory-manifest-v2");
59
+ updateManifest(hash, "cadence-directory-manifest");
60
60
  const visit = (directory: string, prefix: string): void => {
61
61
  const names = readdirSync(directory).sort();
62
62
  for (const name of names) {