@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.
- package/README.md +25 -4
- package/config/.env.example +6 -0
- 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 +28 -8
- package/src/build-windows-job.mjs +95 -0
- package/src/candidate-workspace.ts +6 -0
- package/src/change-verification.ts +764 -67
- 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/durable-contracts.ts +21 -3
- package/src/durable-workflow.ts +55 -40
- package/src/execution-profile.ts +28 -2
- package/src/execution-retention.ts +77 -0
- 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 +151 -1
- package/src/isolation-backend.ts +2 -1
- package/src/operator-cli.mjs +664 -104
- package/src/operator-tools.ts +7 -1
- package/src/package-state.ts +11 -0
- package/src/package-verification.ts +66 -18
- package/src/package-workflow.ts +6 -0
- package/src/phase-execution.ts +18 -7
- package/src/plan-draft-summary.ts +385 -7
- package/src/plan-draft.ts +9 -0
- package/src/remove-path.ts +32 -0
- package/src/subagent-activity.ts +3 -1
- package/src/verification-capability.ts +18 -6
- package/src/verification-environment-io.ts +123 -14
- package/src/verification-environment.ts +50 -3
- package/src/windows-job-backend.ts +419 -0
- package/src/windows-job.c +149 -0
- package/src/workflow-policy.ts +296 -5
- package/src/workflow-recovery-policy.ts +2 -0
- package/src/workflow-state-machine.ts +314 -80
- package/src/workflow-status.ts +251 -59
- package/src/workspace-io-entry.mjs +2 -2
- package/src/workspace-io-worker.mjs +207 -110
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdtempSync
|
|
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
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
|
374
|
-
const
|
|
544
|
+
const aggregateFactKey = this.#aggregateBaselineFactKey(input.resources);
|
|
545
|
+
for (const factKey of [
|
|
546
|
+
aggregateFactKey,
|
|
375
547
|
this.baselineFactKey(input.resources),
|
|
376
|
-
)
|
|
377
|
-
|
|
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 (
|
|
555
|
+
if (
|
|
556
|
+
baseline.revisionId !==
|
|
557
|
+
this.#originalBaselineRevisionId(input.resources)
|
|
558
|
+
) {
|
|
383
559
|
throw new Error("workflow-verification-baseline-conflict");
|
|
384
560
|
}
|
|
385
|
-
|
|
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
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
434
|
-
|
|
1033
|
+
ok: true,
|
|
1034
|
+
observation: {
|
|
1035
|
+
...baseline.fullSuite,
|
|
1036
|
+
verificationId: input.verification.id,
|
|
1037
|
+
},
|
|
435
1038
|
};
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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),
|