@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.
- package/README.md +61 -1
- package/config/plan-draft.example.json +3 -0
- package/config/plan-draft.multiple-tasks.example.json +30 -0
- package/package.json +1 -1
- package/prompts/abel-design.md +33 -3
- package/prompts/abel-implement.md +36 -4
- package/src/apply-transaction.ts +1 -9
- package/src/artifact-store.ts +1 -9
- package/src/change-verification.ts +758 -65
- package/src/contracts.ts +8 -0
- package/src/delivery-compiler.ts +452 -9
- package/src/design-control.ts +2 -2
- package/src/design-diagnostics.ts +6 -4
- package/src/directory-sync.ts +36 -0
- package/src/durable-contracts.ts +19 -3
- package/src/durable-workflow.ts +47 -38
- package/src/implement-continuation.ts +559 -0
- package/src/implement-graph.ts +114 -0
- package/src/implement-plan.ts +2 -0
- package/src/index.ts +179 -1
- package/src/operator-cli.mjs +12 -7
- package/src/package-state.ts +309 -0
- package/src/package-verification.ts +17 -1
- package/src/package-workflow.ts +219 -171
- package/src/phase-execution.ts +7 -1
- package/src/plan-draft-summary.ts +385 -7
- package/src/plan-draft.ts +9 -0
- package/src/run-store.ts +39 -15
- package/src/storage-schema.ts +14 -6
- package/src/subagent-activity.ts +3 -1
- package/src/verification-capability.ts +17 -5
- package/src/verification-environment.ts +20 -2
- package/src/workflow-policy.ts +296 -5
- package/src/workflow-recovery-policy.ts +2 -0
- package/src/workflow-state-machine.ts +400 -131
- package/src/workflow-status.ts +251 -59
- package/src/workspace-io-worker.mjs +48 -42
- 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
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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: {
|
|
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
|
|
374
|
-
const
|
|
540
|
+
const aggregateFactKey = this.#aggregateBaselineFactKey(input.resources);
|
|
541
|
+
for (const factKey of [
|
|
542
|
+
aggregateFactKey,
|
|
375
543
|
this.baselineFactKey(input.resources),
|
|
376
|
-
)
|
|
377
|
-
|
|
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 (
|
|
551
|
+
if (
|
|
552
|
+
baseline.revisionId !==
|
|
553
|
+
this.#originalBaselineRevisionId(input.resources)
|
|
554
|
+
) {
|
|
383
555
|
throw new Error("workflow-verification-baseline-conflict");
|
|
384
556
|
}
|
|
385
|
-
|
|
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
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
434
|
-
|
|
1029
|
+
ok: true,
|
|
1030
|
+
observation: {
|
|
1031
|
+
...baseline.fullSuite,
|
|
1032
|
+
verificationId: input.verification.id,
|
|
1033
|
+
},
|
|
435
1034
|
};
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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),
|