@abelxiaoxing/cadence 1.5.3 → 1.7.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 (38) hide show
  1. package/README.md +61 -1
  2. package/config/plan-draft.example.json +3 -0
  3. package/config/plan-draft.multiple-tasks.example.json +30 -0
  4. package/package.json +1 -1
  5. package/prompts/abel-design.md +33 -3
  6. package/prompts/abel-implement.md +36 -4
  7. package/src/apply-transaction.ts +1 -9
  8. package/src/artifact-store.ts +1 -9
  9. package/src/change-verification.ts +758 -65
  10. package/src/contracts.ts +8 -0
  11. package/src/delivery-compiler.ts +452 -9
  12. package/src/design-control.ts +2 -2
  13. package/src/design-diagnostics.ts +6 -4
  14. package/src/directory-sync.ts +36 -0
  15. package/src/durable-contracts.ts +19 -3
  16. package/src/durable-workflow.ts +47 -38
  17. package/src/implement-continuation.ts +559 -0
  18. package/src/implement-graph.ts +114 -0
  19. package/src/implement-plan.ts +2 -0
  20. package/src/index.ts +179 -1
  21. package/src/operator-cli.mjs +12 -7
  22. package/src/package-state.ts +309 -0
  23. package/src/package-verification.ts +17 -1
  24. package/src/package-workflow.ts +219 -171
  25. package/src/phase-execution.ts +7 -1
  26. package/src/plan-draft-summary.ts +385 -7
  27. package/src/plan-draft.ts +9 -0
  28. package/src/run-store.ts +39 -15
  29. package/src/storage-schema.ts +14 -6
  30. package/src/subagent-activity.ts +3 -1
  31. package/src/verification-capability.ts +17 -5
  32. package/src/verification-environment.ts +20 -2
  33. package/src/workflow-policy.ts +296 -5
  34. package/src/workflow-recovery-policy.ts +2 -0
  35. package/src/workflow-state-machine.ts +400 -131
  36. package/src/workflow-status.ts +251 -59
  37. package/src/workspace-io-worker.mjs +48 -42
  38. package/src/workspace-store.ts +1 -9
@@ -3,7 +3,11 @@ import { mkdtempSync, rmSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { verifyCumulativeRevision } from "./apply-transaction.ts";
5
5
  import type { ArtifactStore } from "./artifact-store.ts";
6
- import type { StructuredVerificationContract } from "./contracts.ts";
6
+ import { canonicalJson } from "./canonical.ts";
7
+ import {
8
+ type StructuredVerificationContract,
9
+ verificationInputPaths,
10
+ } from "./contracts.ts";
7
11
  import type { PlanTaskDraft } from "./delivery-compiler.ts";
8
12
  import type {
9
13
  DurableAffectedResult,
@@ -22,6 +26,7 @@ import { observeSafePath } from "./safe-path.ts";
22
26
 
23
27
  import type { TaskLedger } from "./task-ledger.ts";
24
28
  import {
29
+ baselineRecoveryKey,
25
30
  type DurableVerificationScope,
26
31
  hash,
27
32
  hasVerificationLifecycle,
@@ -30,15 +35,69 @@ import {
30
35
  SHA256,
31
36
  type WorkflowAttemptOutcome,
32
37
  type WorkflowChangeVerifier,
38
+ type WorkflowVerificationPrerequisite,
33
39
  } from "./workflow-policy.ts";
34
40
  import type { WorkspaceRevision } from "./workspace-store.ts";
35
41
  export function sha256Bytes(bytes: Uint8Array): string {
36
42
  return createHash("sha256").update(bytes).digest("hex");
37
43
  }
44
+
45
+ function verificationContractIdentity(
46
+ verification: StructuredVerificationContract,
47
+ ): string {
48
+ const executionSemantics = (
49
+ contract: StructuredVerificationContract,
50
+ ): unknown => {
51
+ if (contract.kind === "steps") {
52
+ return {
53
+ kind: contract.kind,
54
+ steps: contract.steps.map((step) => executionSemantics(step)),
55
+ };
56
+ }
57
+ const {
58
+ id: _id,
59
+ classification: _classification,
60
+ expectedFailure: _expectedFailure,
61
+ ...semantics
62
+ } = contract;
63
+ return semantics;
64
+ };
65
+ return hash(
66
+ "verification-contract-v1",
67
+ canonicalJson(executionSemantics(verification)),
68
+ );
69
+ }
70
+
71
+ const BASELINE_CAPABILITY_CODES = new Set([
72
+ "dependency-path-unsafe",
73
+ "isolation-backend-launch-failed",
74
+ "isolation-backend-unavailable",
75
+ "local-executable-missing",
76
+ "runner-missing",
77
+ "sandbox-runtime-unavailable",
78
+ "verification-environment-changed",
79
+ "verification-environment-requires-trusted-mode",
80
+ "verification-environment-unavailable",
81
+ "verification-runner-launch-failed",
82
+ "workspace-dependency-missing",
83
+ ]);
84
+
85
+ function hasExactKeys(
86
+ value: Record<string, unknown>,
87
+ required: readonly string[],
88
+ optional: readonly string[] = [],
89
+ ): boolean {
90
+ const keys = Object.keys(value);
91
+ const allowed = new Set([...required, ...optional]);
92
+ return (
93
+ required.every((key) => Object.hasOwn(value, key)) &&
94
+ keys.every((key) => allowed.has(key))
95
+ );
96
+ }
97
+
38
98
  function verificationIdentities(
39
99
  result: DurableChangeVerificationResult,
40
- verification: StructuredVerificationContract,
41
- scope: DurableVerificationScope,
100
+ contractIdentity: string,
42
101
  ): string[] {
43
102
  // Keep the complete comparison set; only Worker/status summaries are bounded.
44
103
  const supplied = result.failureIdentities;
@@ -51,7 +110,7 @@ function verificationIdentities(
51
110
  return [...new Set(supplied)].sort();
52
111
  }
53
112
  if (result.ok) return [];
54
- return [hash("verification-failure", scope, verification.id, result.code)];
113
+ return [hash("verification-failure-v2", contractIdentity, result.code)];
55
114
  }
56
115
  export function verificationBaselineFact(
57
116
  baseline: DurableVerificationBaseline,
@@ -97,7 +156,10 @@ export function parseVerificationBaseline(
97
156
  observation.failureIdentities.every((identity) => SHA256.test(identity)) &&
98
157
  (observation.code === undefined || typeof observation.code === "string") &&
99
158
  (observation.attributionReliable === undefined ||
100
- typeof observation.attributionReliable === "boolean");
159
+ typeof observation.attributionReliable === "boolean") &&
160
+ (observation.contractIdentity === undefined ||
161
+ (typeof observation.contractIdentity === "string" &&
162
+ SHA256.test(observation.contractIdentity)));
101
163
  if (
102
164
  !baseline.targetContracts.every(
103
165
  (entry) =>
@@ -159,6 +221,94 @@ export class ChangeVerification {
159
221
  this.#options = options;
160
222
  this.#services = services;
161
223
  }
224
+
225
+ #originalBaselineRevisionId(resources: DurableExecutionResources): string {
226
+ return (
227
+ (
228
+ resources as DurableExecutionResources & {
229
+ originalBaselineRevisionId?: string;
230
+ }
231
+ ).originalBaselineRevisionId ?? resources.baselineRevisionId
232
+ );
233
+ }
234
+
235
+ #observedEnvironmentIdentity(resources: DurableExecutionResources): string {
236
+ const identity = resources.environmentIdentity;
237
+ if (!identity) return "unobserved";
238
+ return SHA256.test(identity)
239
+ ? identity
240
+ : hash("verification-environment-identity-v1", identity);
241
+ }
242
+
243
+ #baselinePrerequisite(input: {
244
+ resources: DurableExecutionResources;
245
+ root: string;
246
+ revisionId: string;
247
+ scope: "baseline-task-affected" | "baseline-full-suite";
248
+ verification: StructuredVerificationContract;
249
+ result: Extract<DurableChangeVerificationResult, { ok: false }>;
250
+ taskId?: string;
251
+ }): WorkflowVerificationPrerequisite {
252
+ const originalRevisionId = this.#originalBaselineRevisionId(
253
+ input.resources,
254
+ );
255
+ if (input.revisionId !== originalRevisionId) {
256
+ throw new Error("workflow-verification-baseline-conflict");
257
+ }
258
+ const base = {
259
+ kind: "verification-prerequisite" as const,
260
+ scope: input.scope,
261
+ verificationId: input.verification.id,
262
+ contractIdentity: verificationContractIdentity(input.verification),
263
+ originalRevisionId,
264
+ environmentIdentity: this.#observedEnvironmentIdentity(input.resources),
265
+ ...(input.taskId ? { taskId: input.taskId } : {}),
266
+ };
267
+ const observedInputs = verificationInputPaths(input.verification).map(
268
+ (relative) => ({
269
+ relative,
270
+ observation: observeSafePath(input.root, relative),
271
+ }),
272
+ );
273
+ const unsafe = observedInputs.find(
274
+ ({ observation }) =>
275
+ observation.kind !== "file" && observation.kind !== "absent",
276
+ );
277
+ if (unsafe) {
278
+ return {
279
+ ...base,
280
+ cause: "unsafe-input",
281
+ input: { path: unsafe.relative, kind: "unsafe" },
282
+ };
283
+ }
284
+ const absent = observedInputs.find(
285
+ ({ observation }) => observation.kind === "absent",
286
+ );
287
+ if (absent) {
288
+ const output = input.resources.plan.outputs.find(
289
+ (candidate) => candidate.path === absent.relative,
290
+ );
291
+ return output
292
+ ? {
293
+ ...base,
294
+ cause: "future-output",
295
+ input: { path: absent.relative, kind: "absent" },
296
+ producer: structuredClone(output.producer),
297
+ }
298
+ : {
299
+ ...base,
300
+ cause: "missing-input",
301
+ input: { path: absent.relative, kind: "absent" },
302
+ };
303
+ }
304
+ return {
305
+ ...base,
306
+ cause: BASELINE_CAPABILITY_CODES.has(input.result.code)
307
+ ? "capability"
308
+ : "unknown",
309
+ };
310
+ }
311
+
162
312
  async #refreshEnvironment(
163
313
  resources: DurableExecutionResources,
164
314
  signal: AbortSignal,
@@ -298,6 +448,7 @@ export class ChangeVerification {
298
448
  };
299
449
  }
300
450
  const root = mkdtempSync(path.join(input.resources.root, "verification-"));
451
+ const contractIdentity = verificationContractIdentity(input.verification);
301
452
  try {
302
453
  await input.resources.workspaces.materializeAsync(
303
454
  input.revisionId,
@@ -320,11 +471,8 @@ export class ChangeVerification {
320
471
  observation: {
321
472
  status: "passed",
322
473
  verificationId: input.verification.id,
323
- failureIdentities: verificationIdentities(
324
- result,
325
- input.verification,
326
- input.scope,
327
- ),
474
+ failureIdentities: verificationIdentities(result, contractIdentity),
475
+ contractIdentity,
328
476
  },
329
477
  };
330
478
  }
@@ -334,13 +482,10 @@ export class ChangeVerification {
334
482
  observation: {
335
483
  status: "failed",
336
484
  verificationId: input.verification.id,
337
- failureIdentities: verificationIdentities(
338
- result,
339
- input.verification,
340
- input.scope,
341
- ),
485
+ failureIdentities: verificationIdentities(result, contractIdentity),
342
486
  code: result.code,
343
487
  attributionReliable: result.attributionReliable,
488
+ contractIdentity,
344
489
  },
345
490
  };
346
491
  }
@@ -356,9 +501,30 @@ export class ChangeVerification {
356
501
  outcome: { kind: "operation-cancelled", code: "cancelled" },
357
502
  };
358
503
  }
504
+ const baselineScope =
505
+ input.scope === "baseline-task-affected" ||
506
+ input.scope === "baseline-full-suite"
507
+ ? input.scope
508
+ : undefined;
359
509
  return {
360
510
  ok: false,
361
- outcome: { kind: "paused", code: result.code },
511
+ outcome: {
512
+ kind: "paused",
513
+ code: result.code,
514
+ ...(baselineScope
515
+ ? {
516
+ prerequisite: this.#baselinePrerequisite({
517
+ resources: input.resources,
518
+ root,
519
+ revisionId: input.revisionId,
520
+ scope: baselineScope,
521
+ verification: input.verification,
522
+ result,
523
+ ...(input.taskId ? { taskId: input.taskId } : {}),
524
+ }),
525
+ }
526
+ : {}),
527
+ },
362
528
  };
363
529
  } finally {
364
530
  rmSync(root, { recursive: true, force: true });
@@ -369,48 +535,97 @@ export class ChangeVerification {
369
535
  resources: DurableExecutionResources;
370
536
  ledger: TaskLedger;
371
537
  signal: AbortSignal;
538
+ taskId?: string;
372
539
  }): Promise<DurableBaselineResult> {
373
- const environmentIdentity = input.resources.environmentIdentity;
374
- const stored = input.ledger.durableFact(
540
+ const aggregateFactKey = this.#aggregateBaselineFactKey(input.resources);
541
+ for (const factKey of [
542
+ aggregateFactKey,
375
543
  this.baselineFactKey(input.resources),
376
- );
377
- if (stored !== undefined) {
544
+ ]) {
545
+ const stored = input.ledger.durableFact(factKey);
546
+ if (stored === undefined) continue;
378
547
  const baseline = parseVerificationBaseline(
379
548
  stored,
380
549
  input.resources.artifacts,
381
550
  );
382
- if (baseline.revisionId !== input.resources.baselineRevisionId) {
551
+ if (
552
+ baseline.revisionId !==
553
+ this.#originalBaselineRevisionId(input.resources)
554
+ ) {
383
555
  throw new Error("workflow-verification-baseline-conflict");
384
556
  }
385
- return { ok: true, baseline };
557
+ // Legacy aggregates remain inspectable, but do not bind the complete
558
+ // verification contract and therefore cannot authorize evidence reuse.
559
+ if (factKey !== aggregateFactKey) continue;
560
+ const compatible = this.#compatibleAggregateBaseline(
561
+ baseline,
562
+ input.resources.plan,
563
+ );
564
+ if (!compatible) continue;
565
+ if (!input.taskId) return { ok: true, baseline };
566
+ const affected = baseline.affected.filter(
567
+ (entry) => entry.taskId === input.taskId,
568
+ );
569
+ if (affected.length === 1) {
570
+ return { ok: true, baseline: { ...baseline, affected } };
571
+ }
386
572
  }
387
573
  const plan = input.resources.plan;
388
- const affected: DurableVerificationBaseline["affected"] = [];
389
- for (const task of plan.tasks) {
390
- const observed = await this.#observeVerification({
574
+ const tasks = input.taskId
575
+ ? plan.tasks.filter((task) => task.taskId === input.taskId)
576
+ : plan.tasks;
577
+ if (tasks.length !== (input.taskId ? 1 : plan.tasks.length)) {
578
+ throw new Error("workflow-task-baseline-unavailable");
579
+ }
580
+ const pendingAffected = tasks.map(async (task) => {
581
+ const observed = await this.#captureBaselineObservation({
391
582
  resources: input.resources,
392
- revisionId: input.resources.baselineRevisionId,
583
+ ledger: input.ledger,
584
+ verification: task.baselineVerification ?? task.affectedVerification,
393
585
  scope: "baseline-task-affected",
394
- verification: task.affectedVerification,
395
586
  taskId: task.taskId,
396
587
  signal: input.signal,
397
588
  });
398
- if (!observed.ok) return observed;
399
- affected.push({
400
- taskId: task.taskId,
401
- observation: observed.observation,
402
- });
403
- }
404
- const fullSuite = await this.#observeVerification({
589
+ return { taskId: task.taskId, observed };
590
+ });
591
+ const pendingFullSuite = this.#captureBaselineObservation({
405
592
  resources: input.resources,
406
- revisionId: input.resources.baselineRevisionId,
407
- scope: "baseline-full-suite",
593
+ ledger: input.ledger,
408
594
  verification: plan.verification.baseline.fullSuite,
595
+ scope: "baseline-full-suite",
409
596
  signal: input.signal,
410
597
  });
598
+ const settled = await Promise.allSettled([
599
+ ...pendingAffected,
600
+ pendingFullSuite,
601
+ ]);
602
+ const rejected = settled.find(
603
+ (result): result is PromiseRejectedResult => result.status === "rejected",
604
+ );
605
+ if (rejected) throw rejected.reason;
606
+ const fulfilled = settled as Array<
607
+ PromiseFulfilledResult<
608
+ | Awaited<(typeof pendingAffected)[number]>
609
+ | Awaited<typeof pendingFullSuite>
610
+ >
611
+ >;
612
+ const affectedResults = fulfilled
613
+ .slice(0, pendingAffected.length)
614
+ .map((result) => result.value) as Awaited<
615
+ (typeof pendingAffected)[number]
616
+ >[];
617
+ const fullSuiteResult = fulfilled.at(-1);
618
+ if (!fullSuiteResult) {
619
+ throw new Error("workflow-verification-baseline-unavailable");
620
+ }
621
+ const fullSuite = fullSuiteResult.value as Awaited<typeof pendingFullSuite>;
622
+ const affectedFailure = affectedResults.find((entry) => !entry.observed.ok);
623
+ if (affectedFailure && !affectedFailure.observed.ok) {
624
+ return affectedFailure.observed;
625
+ }
411
626
  if (!fullSuite.ok) return fullSuite;
412
627
  const baseline: DurableVerificationBaseline = {
413
- revisionId: input.resources.baselineRevisionId,
628
+ revisionId: this.#originalBaselineRevisionId(input.resources),
414
629
  targetContracts: plan.tasks
415
630
  .filter(
416
631
  (task) =>
@@ -425,25 +640,503 @@ export class ChangeVerification {
425
640
  task.phases.red.verification.id)
426
641
  : task.phases.red.verification.id,
427
642
  })),
428
- affected,
643
+ affected: affectedResults.map((entry) => {
644
+ if (!entry.observed.ok) {
645
+ throw new Error("workflow-task-baseline-unavailable");
646
+ }
647
+ return {
648
+ taskId: entry.taskId,
649
+ observation: entry.observed.observation,
650
+ };
651
+ }),
429
652
  fullSuite: fullSuite.observation,
430
653
  };
431
- if (environmentIdentity !== input.resources.environmentIdentity)
654
+ if (!input.taskId) {
655
+ input.ledger.putDurableFact(
656
+ this.#aggregateBaselineFactKey(input.resources),
657
+ verificationBaselineFact(baseline, input.resources.artifacts),
658
+ );
659
+ }
660
+ return { ok: true, baseline };
661
+ }
662
+
663
+ #compatibleAggregateBaseline(
664
+ baseline: DurableVerificationBaseline,
665
+ plan: DurableExecutionResources["plan"],
666
+ ): boolean {
667
+ const matches = (
668
+ observation: DurableVerificationObservation,
669
+ contract: StructuredVerificationContract,
670
+ ): boolean =>
671
+ observation.verificationId === contract.id &&
672
+ observation.contractIdentity === verificationContractIdentity(contract);
673
+ return (
674
+ matches(baseline.fullSuite, plan.verification.baseline.fullSuite) &&
675
+ baseline.affected.length === plan.tasks.length &&
676
+ plan.tasks.every((task) => {
677
+ const entries = baseline.affected.filter(
678
+ (entry) => entry.taskId === task.taskId,
679
+ );
680
+ const [entry] = entries;
681
+ return (
682
+ entries.length === 1 &&
683
+ entry !== undefined &&
684
+ matches(
685
+ entry.observation,
686
+ task.baselineVerification ?? task.affectedVerification,
687
+ )
688
+ );
689
+ })
690
+ );
691
+ }
692
+
693
+ #aggregateBaselineFactKey(resources: DurableExecutionResources): string {
694
+ return `${this.baselineFactKey(resources)}-all-${hash(
695
+ this.#originalBaselineRevisionId(resources),
696
+ canonicalJson({
697
+ tasks: resources.plan.tasks.map((task) => ({
698
+ taskId: task.taskId,
699
+ verification: task.baselineVerification ?? task.affectedVerification,
700
+ })),
701
+ fullSuite: resources.plan.verification.baseline.fullSuite,
702
+ }),
703
+ ).slice(0, 40)}`;
704
+ }
705
+
706
+ #baselineOwnerIdentity(
707
+ resources: DurableExecutionResources,
708
+ scope: "baseline-task-affected" | "baseline-full-suite",
709
+ taskId?: string,
710
+ ): string {
711
+ if (scope === "baseline-full-suite") {
712
+ if (taskId !== undefined)
713
+ throw new Error("workflow-verification-baseline-failure-invalid");
714
+ return hash("baseline-observation-owner-v1", "global");
715
+ }
716
+ if (!taskId)
717
+ throw new Error("workflow-verification-baseline-failure-invalid");
718
+ const tasks = resources.plan.tasks.filter((task) => task.taskId === taskId);
719
+ const task = tasks[0];
720
+ if (!task || tasks.length !== 1)
721
+ throw new Error("workflow-verification-baseline-failure-invalid");
722
+ return baselineRecoveryKey(task, "red");
723
+ }
724
+
725
+ #baselineObservationFactKey(
726
+ resources: DurableExecutionResources,
727
+ verification: StructuredVerificationContract,
728
+ scope: "baseline-task-affected" | "baseline-full-suite",
729
+ taskId?: string,
730
+ ): string {
731
+ return `${this.baselineFactKey(resources)}-o-${hash(
732
+ this.#originalBaselineRevisionId(resources),
733
+ verificationContractIdentity(verification),
734
+ scope,
735
+ this.#baselineOwnerIdentity(resources, scope, taskId),
736
+ ).slice(0, 40)}`;
737
+ }
738
+
739
+ #baselineFailureFactKey(input: {
740
+ resources: DurableExecutionResources;
741
+ verification: StructuredVerificationContract;
742
+ scope: "baseline-task-affected" | "baseline-full-suite";
743
+ taskId?: string;
744
+ }): string {
745
+ return `verification-baseline-failure-${hash(
746
+ this.#originalBaselineRevisionId(input.resources),
747
+ verificationContractIdentity(input.verification),
748
+ input.scope,
749
+ this.#baselineOwnerIdentity(input.resources, input.scope, input.taskId),
750
+ this.#observedEnvironmentIdentity(input.resources),
751
+ this.#options.verificationPolicy ?? "legacy",
752
+ ).slice(0, 40)}`;
753
+ }
754
+
755
+ #legacyBaselineFailureFactKey(input: {
756
+ resources: DurableExecutionResources;
757
+ verification: StructuredVerificationContract;
758
+ scope: "baseline-task-affected" | "baseline-full-suite";
759
+ taskId?: string;
760
+ }): string {
761
+ return `verification-baseline-failure-${hash(
762
+ this.#originalBaselineRevisionId(input.resources),
763
+ verificationContractIdentity(input.verification),
764
+ input.scope,
765
+ input.taskId ?? "global",
766
+ this.#observedEnvironmentIdentity(input.resources),
767
+ this.#options.verificationPolicy ?? "legacy",
768
+ ).slice(0, 40)}`;
769
+ }
770
+
771
+ #projectBaselineFailure(
772
+ code: string,
773
+ observation: { path: string; kind: "absent" | "unsafe" } | undefined,
774
+ input: {
775
+ resources: DurableExecutionResources;
776
+ verification: StructuredVerificationContract;
777
+ scope: "baseline-task-affected" | "baseline-full-suite";
778
+ taskId?: string;
779
+ },
780
+ ): Extract<DurableObservedVerification, { ok: false }> {
781
+ const prerequisite: WorkflowVerificationPrerequisite = {
782
+ kind: "verification-prerequisite",
783
+ scope: input.scope,
784
+ cause: BASELINE_CAPABILITY_CODES.has(code) ? "capability" : "unknown",
785
+ verificationId: input.verification.id,
786
+ contractIdentity: verificationContractIdentity(input.verification),
787
+ originalRevisionId: this.#originalBaselineRevisionId(input.resources),
788
+ environmentIdentity: this.#observedEnvironmentIdentity(input.resources),
789
+ ...(input.taskId ? { taskId: input.taskId } : {}),
790
+ };
791
+ if (observation?.kind === "unsafe") {
792
+ prerequisite.cause = "unsafe-input";
793
+ prerequisite.input = observation;
794
+ } else if (observation?.kind === "absent") {
795
+ const outputs = input.resources.plan.outputs.filter(
796
+ (output) => output.path === observation.path,
797
+ );
798
+ if (outputs.length > 1)
799
+ throw new Error("workflow-verification-baseline-failure-invalid");
800
+ prerequisite.cause =
801
+ outputs.length === 1 ? "future-output" : "missing-input";
802
+ prerequisite.input = observation;
803
+ if (outputs[0])
804
+ prerequisite.producer = structuredClone(outputs[0].producer);
805
+ }
806
+ return {
807
+ ok: false,
808
+ outcome: { kind: "paused", code, prerequisite },
809
+ };
810
+ }
811
+
812
+ #storedBaselineFailure(
813
+ value: unknown,
814
+ input: {
815
+ resources: DurableExecutionResources;
816
+ verification: StructuredVerificationContract;
817
+ scope: "baseline-task-affected" | "baseline-full-suite";
818
+ taskId?: string;
819
+ },
820
+ ): Extract<DurableObservedVerification, { ok: false }> {
821
+ if (!isRecord(value))
822
+ throw new Error("workflow-verification-baseline-failure-invalid");
823
+ const contractIdentity = verificationContractIdentity(input.verification);
824
+ const originalRevisionId = this.#originalBaselineRevisionId(
825
+ input.resources,
826
+ );
827
+ const environmentIdentity = this.#observedEnvironmentIdentity(
828
+ input.resources,
829
+ );
830
+ const ownerIdentity = this.#baselineOwnerIdentity(
831
+ input.resources,
832
+ input.scope,
833
+ input.taskId,
834
+ );
835
+ const declaredPaths = verificationInputPaths(input.verification);
836
+ let observation: { path: string; kind: "absent" | "unsafe" } | undefined;
837
+ if (value.kind === "baseline-observation-failure-v2") {
838
+ if (
839
+ !hasExactKeys(
840
+ value,
841
+ [
842
+ "kind",
843
+ "policyIdentity",
844
+ "ownerIdentity",
845
+ "scope",
846
+ "contractIdentity",
847
+ "originalRevisionId",
848
+ "environmentIdentity",
849
+ "code",
850
+ ],
851
+ ["observation"],
852
+ ) ||
853
+ value.policyIdentity !==
854
+ hash(
855
+ "verification-failure-policy-v2",
856
+ this.#options.verificationPolicy ?? "legacy",
857
+ ) ||
858
+ value.ownerIdentity !== ownerIdentity ||
859
+ value.scope !== input.scope ||
860
+ value.contractIdentity !== contractIdentity ||
861
+ value.originalRevisionId !== originalRevisionId ||
862
+ value.environmentIdentity !== environmentIdentity
863
+ ) {
864
+ throw new Error("workflow-verification-baseline-failure-invalid");
865
+ }
866
+ if (value.observation !== undefined) {
867
+ if (
868
+ !isRecord(value.observation) ||
869
+ !hasExactKeys(value.observation, ["path", "kind"]) ||
870
+ typeof value.observation.path !== "string" ||
871
+ !declaredPaths.includes(value.observation.path) ||
872
+ !["absent", "unsafe"].includes(String(value.observation.kind))
873
+ )
874
+ throw new Error("workflow-verification-baseline-failure-invalid");
875
+ observation = {
876
+ path: value.observation.path,
877
+ kind: value.observation.kind as "absent" | "unsafe",
878
+ };
879
+ }
880
+ } else if (value.kind === "baseline-observation-failure-v1") {
881
+ const prerequisite = value.prerequisite;
882
+ if (
883
+ !hasExactKeys(value, [
884
+ "kind",
885
+ "policyIdentity",
886
+ "code",
887
+ "prerequisite",
888
+ ]) ||
889
+ value.policyIdentity !==
890
+ hash(
891
+ "verification-failure-policy-v1",
892
+ this.#options.verificationPolicy ?? "legacy",
893
+ ) ||
894
+ !isRecord(prerequisite) ||
895
+ !hasExactKeys(
896
+ prerequisite,
897
+ [
898
+ "kind",
899
+ "scope",
900
+ "cause",
901
+ "verificationId",
902
+ "contractIdentity",
903
+ "originalRevisionId",
904
+ "environmentIdentity",
905
+ ],
906
+ ["taskId", "input", "producer"],
907
+ ) ||
908
+ prerequisite.kind !== "verification-prerequisite" ||
909
+ prerequisite.scope !== input.scope ||
910
+ typeof prerequisite.verificationId !== "string" ||
911
+ !IDENTIFIER.test(prerequisite.verificationId) ||
912
+ prerequisite.contractIdentity !== contractIdentity ||
913
+ prerequisite.originalRevisionId !== originalRevisionId ||
914
+ prerequisite.environmentIdentity !== environmentIdentity ||
915
+ prerequisite.taskId !== input.taskId ||
916
+ ![
917
+ "future-output",
918
+ "missing-input",
919
+ "unsafe-input",
920
+ "capability",
921
+ "unknown",
922
+ ].includes(String(prerequisite.cause))
923
+ )
924
+ throw new Error("workflow-verification-baseline-failure-invalid");
925
+ if (prerequisite.input !== undefined) {
926
+ if (
927
+ !isRecord(prerequisite.input) ||
928
+ !hasExactKeys(prerequisite.input, ["path", "kind"]) ||
929
+ typeof prerequisite.input.path !== "string" ||
930
+ !declaredPaths.includes(prerequisite.input.path) ||
931
+ !["absent", "unsafe"].includes(String(prerequisite.input.kind))
932
+ )
933
+ throw new Error("workflow-verification-baseline-failure-invalid");
934
+ observation = {
935
+ path: prerequisite.input.path,
936
+ kind: prerequisite.input.kind as "absent" | "unsafe",
937
+ };
938
+ }
939
+ const cause = String(prerequisite.cause);
940
+ if (
941
+ ((cause === "future-output" || cause === "missing-input") &&
942
+ observation?.kind !== "absent") ||
943
+ (cause === "unsafe-input" && observation?.kind !== "unsafe") ||
944
+ ((cause === "capability" || cause === "unknown") &&
945
+ (observation !== undefined ||
946
+ prerequisite.producer !== undefined ||
947
+ (cause === "capability") !==
948
+ BASELINE_CAPABILITY_CODES.has(String(value.code))))
949
+ )
950
+ throw new Error("workflow-verification-baseline-failure-invalid");
951
+ if (prerequisite.producer !== undefined) {
952
+ if (
953
+ cause !== "future-output" ||
954
+ !isRecord(prerequisite.producer) ||
955
+ !hasExactKeys(prerequisite.producer, ["taskId", "phase"]) ||
956
+ typeof prerequisite.producer.taskId !== "string" ||
957
+ !IDENTIFIER.test(prerequisite.producer.taskId) ||
958
+ !["red", "green", "refactor"].includes(
959
+ String(prerequisite.producer.phase),
960
+ )
961
+ )
962
+ throw new Error("workflow-verification-baseline-failure-invalid");
963
+ } else if (cause === "future-output") {
964
+ throw new Error("workflow-verification-baseline-failure-invalid");
965
+ }
966
+ } else {
967
+ throw new Error("workflow-verification-baseline-failure-invalid");
968
+ }
969
+ if (
970
+ typeof value.code !== "string" ||
971
+ value.code.length < 1 ||
972
+ value.code.length > 256
973
+ )
974
+ throw new Error("workflow-verification-baseline-failure-invalid");
975
+ return this.#projectBaselineFailure(value.code, observation, input);
976
+ }
977
+
978
+ async #captureBaselineObservation(input: {
979
+ resources: DurableExecutionResources;
980
+ ledger: TaskLedger;
981
+ verification: StructuredVerificationContract;
982
+ scope: "baseline-task-affected" | "baseline-full-suite";
983
+ signal: AbortSignal;
984
+ taskId?: string;
985
+ }): Promise<DurableObservedVerification> {
986
+ const contractIdentity = verificationContractIdentity(input.verification);
987
+ const ledger = input.resources.baselineLedger ?? input.ledger;
988
+ const factKey = this.#baselineObservationFactKey(
989
+ input.resources,
990
+ input.verification,
991
+ input.scope,
992
+ input.taskId,
993
+ );
994
+ const failureFactKey = this.#baselineFailureFactKey(input);
995
+ const legacyFailureFactKey = this.#legacyBaselineFailureFactKey(input);
996
+ const stored = ledger.durableFact(factKey);
997
+ const storedFailures = [
998
+ ledger.durableFact(failureFactKey),
999
+ ledger.durableFact(legacyFailureFactKey),
1000
+ ...(input.ledger === ledger
1001
+ ? []
1002
+ : [input.ledger.durableFact(legacyFailureFactKey)]),
1003
+ ].filter((value) => value !== undefined);
1004
+ if (storedFailures.length > 1)
1005
+ throw new Error("workflow-verification-baseline-fact-conflict");
1006
+ const storedFailure = storedFailures[0];
1007
+ if (stored !== undefined && storedFailure !== undefined) {
1008
+ throw new Error("workflow-verification-baseline-fact-conflict");
1009
+ }
1010
+ if (stored !== undefined) {
1011
+ const baseline = parseVerificationBaseline(
1012
+ stored,
1013
+ input.resources.artifacts,
1014
+ );
1015
+ if (
1016
+ baseline.revisionId !==
1017
+ this.#originalBaselineRevisionId(input.resources)
1018
+ ) {
1019
+ throw new Error("workflow-verification-baseline-conflict");
1020
+ }
1021
+ if (
1022
+ baseline.fullSuite.contractIdentity !== contractIdentity ||
1023
+ baseline.targetContracts.length !== 0 ||
1024
+ baseline.affected.length !== 0
1025
+ ) {
1026
+ throw new Error("workflow-verification-baseline-conflict");
1027
+ }
432
1028
  return {
433
- ok: false,
434
- outcome: { kind: "paused", code: "verification-environment-changed" },
1029
+ ok: true,
1030
+ observation: {
1031
+ ...baseline.fullSuite,
1032
+ verificationId: input.verification.id,
1033
+ },
435
1034
  };
436
- input.ledger.putDurableFact(
437
- this.baselineFactKey(input.resources),
438
- verificationBaselineFact(baseline, input.resources.artifacts),
1035
+ }
1036
+ if (storedFailure !== undefined) {
1037
+ return this.#storedBaselineFailure(storedFailure, input);
1038
+ }
1039
+ const pendingKey = `baseline-observation:${factKey}`;
1040
+ let pending = input.resources.baselinePromises.get(pendingKey);
1041
+ if (!pending) {
1042
+ pending = this.#observeVerification({
1043
+ resources: input.resources,
1044
+ revisionId: this.#originalBaselineRevisionId(input.resources),
1045
+ scope: input.scope,
1046
+ verification: input.verification,
1047
+ ...(input.taskId ? { taskId: input.taskId } : {}),
1048
+ signal: input.signal,
1049
+ }).then(
1050
+ (observed): DurableBaselineResult =>
1051
+ observed.ok
1052
+ ? {
1053
+ ok: true,
1054
+ baseline: {
1055
+ revisionId: this.#originalBaselineRevisionId(input.resources),
1056
+ targetContracts: [],
1057
+ affected: [],
1058
+ fullSuite: observed.observation,
1059
+ },
1060
+ }
1061
+ : observed,
1062
+ );
1063
+ const owned = pending;
1064
+ pending = owned.then(
1065
+ (result) => {
1066
+ if (!result.ok) input.resources.baselinePromises.delete(pendingKey);
1067
+ return result;
1068
+ },
1069
+ (error: unknown) => {
1070
+ input.resources.baselinePromises.delete(pendingKey);
1071
+ throw error;
1072
+ },
1073
+ );
1074
+ input.resources.baselinePromises.set(pendingKey, pending);
1075
+ }
1076
+ const result = await pending;
1077
+ if (!result.ok) {
1078
+ if (result.outcome.kind === "paused") {
1079
+ if (!result.outcome.prerequisite) {
1080
+ throw new Error("workflow-verification-baseline-failure-invalid");
1081
+ }
1082
+ const prerequisite = result.outcome.prerequisite;
1083
+ const storedFact = ledger.putDurableFact(failureFactKey, {
1084
+ kind: "baseline-observation-failure-v2",
1085
+ policyIdentity: hash(
1086
+ "verification-failure-policy-v2",
1087
+ this.#options.verificationPolicy ?? "legacy",
1088
+ ),
1089
+ ownerIdentity: this.#baselineOwnerIdentity(
1090
+ input.resources,
1091
+ input.scope,
1092
+ input.taskId,
1093
+ ),
1094
+ scope: input.scope,
1095
+ contractIdentity,
1096
+ originalRevisionId: this.#originalBaselineRevisionId(input.resources),
1097
+ environmentIdentity: this.#observedEnvironmentIdentity(
1098
+ input.resources,
1099
+ ),
1100
+ code: result.outcome.code,
1101
+ ...(prerequisite.input
1102
+ ? { observation: structuredClone(prerequisite.input) }
1103
+ : {}),
1104
+ });
1105
+ return this.#storedBaselineFailure(storedFact, input);
1106
+ }
1107
+ return result;
1108
+ }
1109
+ const fact = verificationBaselineFact(
1110
+ result.baseline,
1111
+ input.resources.artifacts,
439
1112
  );
440
- return { ok: true, baseline };
1113
+ const storedFact = ledger.putDurableFact(factKey, fact);
1114
+ const baseline = parseVerificationBaseline(
1115
+ storedFact,
1116
+ input.resources.artifacts,
1117
+ );
1118
+ if (
1119
+ baseline.revisionId !==
1120
+ this.#originalBaselineRevisionId(input.resources) ||
1121
+ baseline.fullSuite.contractIdentity !== contractIdentity ||
1122
+ baseline.targetContracts.length !== 0 ||
1123
+ baseline.affected.length !== 0
1124
+ )
1125
+ throw new Error("workflow-verification-baseline-conflict");
1126
+ return {
1127
+ ok: true,
1128
+ observation: {
1129
+ ...baseline.fullSuite,
1130
+ verificationId: input.verification.id,
1131
+ },
1132
+ };
441
1133
  }
442
1134
 
443
1135
  async ensureVerificationBaseline(input: {
444
1136
  resources: DurableExecutionResources;
445
1137
  ledger: TaskLedger;
446
1138
  signal: AbortSignal;
1139
+ taskId?: string;
447
1140
  }): Promise<DurableBaselineResult> {
448
1141
  try {
449
1142
  await this.#refreshEnvironment(input.resources, input.signal);
@@ -456,23 +1149,7 @@ export class ChangeVerification {
456
1149
  },
457
1150
  };
458
1151
  }
459
- const existing = input.resources.baselinePromises.get(
460
- input.resources.deliveryRevision,
461
- );
462
- if (existing) return existing;
463
- const pending = this.#captureVerificationBaseline(input).then((result) => {
464
- if (!result.ok) {
465
- input.resources.baselinePromises.delete(
466
- input.resources.deliveryRevision,
467
- );
468
- }
469
- return result;
470
- });
471
- input.resources.baselinePromises.set(
472
- input.resources.deliveryRevision,
473
- pending,
474
- );
475
- return pending;
1152
+ return this.#captureVerificationBaseline(input);
476
1153
  }
477
1154
 
478
1155
  async verifyTaskAffected(input: {
@@ -498,12 +1175,18 @@ export class ChangeVerification {
498
1175
  if (current.observation.status === "passed") {
499
1176
  return { kind: "verified", attribution: "none" };
500
1177
  }
1178
+ const comparable =
1179
+ typeof baseline.observation.contractIdentity === "string" &&
1180
+ baseline.observation.contractIdentity ===
1181
+ current.observation.contractIdentity;
501
1182
  if (
502
1183
  current.observation.attributionReliable === false &&
503
1184
  baseline.observation.status === "failed"
504
1185
  )
505
1186
  return { kind: "paused", code: "verification-attribution-unresolved" };
506
- const prior = new Set(baseline.observation.failureIdentities);
1187
+ const prior = new Set(
1188
+ comparable ? baseline.observation.failureIdentities : [],
1189
+ );
507
1190
  const introduced = current.observation.failureIdentities.filter(
508
1191
  (identity) => !prior.has(identity),
509
1192
  );
@@ -742,6 +1425,10 @@ export class ChangeVerification {
742
1425
  };
743
1426
  }
744
1427
  if (current.observation.status === "failed") {
1428
+ const comparable =
1429
+ typeof baseline.observation.contractIdentity === "string" &&
1430
+ baseline.observation.contractIdentity ===
1431
+ current.observation.contractIdentity;
745
1432
  if (
746
1433
  current.observation.attributionReliable === false &&
747
1434
  baseline.observation.status === "failed"
@@ -750,7 +1437,9 @@ export class ChangeVerification {
750
1437
  kind: "paused",
751
1438
  code: "verification-attribution-unresolved",
752
1439
  };
753
- const prior = new Set(baseline.observation.failureIdentities);
1440
+ const prior = new Set(
1441
+ comparable ? baseline.observation.failureIdentities : [],
1442
+ );
754
1443
  const introduced = current.observation.failureIdentities.filter(
755
1444
  (identity) => !prior.has(identity),
756
1445
  );
@@ -789,6 +1478,10 @@ export class ChangeVerification {
789
1478
  };
790
1479
  }
791
1480
  if (fullSuite.observation.status === "failed") {
1481
+ const comparable =
1482
+ typeof captured.baseline.fullSuite.contractIdentity === "string" &&
1483
+ captured.baseline.fullSuite.contractIdentity ===
1484
+ fullSuite.observation.contractIdentity;
792
1485
  if (
793
1486
  fullSuite.observation.attributionReliable === false &&
794
1487
  captured.baseline.fullSuite.status === "failed"
@@ -798,7 +1491,7 @@ export class ChangeVerification {
798
1491
  code: "verification-attribution-unresolved",
799
1492
  };
800
1493
  const baselineFailures = new Set(
801
- captured.baseline.fullSuite.failureIdentities,
1494
+ comparable ? captured.baseline.fullSuite.failureIdentities : [],
802
1495
  );
803
1496
  const introduced = fullSuite.observation.failureIdentities.filter(
804
1497
  (identity) => !baselineFailures.has(identity),