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