@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.10
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/AGENTS.md +41 -75
- package/CONTRIBUTING.md +19 -26
- package/architecture/action-taxonomy.json +24 -0
- package/architecture/agent-change-map.md +266 -10
- package/architecture/capability-state-machine-manifest.json +1 -4
- package/architecture/ci-lane-change-budget.json +190 -807
- package/architecture/core-mechanisms.json +3 -6
- package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
- package/architecture/decisions/0006-business-attempt-journal.md +89 -0
- package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
- package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
- package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
- package/architecture/internal-capabilities.json +362 -7
- package/architecture/maintainability-debt.json +50 -34
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +5670 -0
- package/architecture/platform-stage-checkpoints.json +31 -7
- package/architecture/product-upstreams.json +20 -0
- package/architecture/release-topology.json +328 -74
- package/architecture/stage-capsule-qualification.json +3 -1
- package/architecture/universal-workflow-bootstrap.json +15 -1
- package/architecture/universal-workflow-capability-policy.json +4 -4
- package/architecture/universal-workflow-fault-campaign.json +3 -1
- package/architecture/workflow-taxonomy.json +77 -328
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/site/buildchain-contract.json +16 -68
- package/dist/site/buildchain-site.json +40 -25
- package/dist/site/capability-registry.json +4 -4
- package/dist/site/controller-registry.json +1 -47
- package/dist/site/kfd-claims.json +499 -494
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +54 -54
- package/dist/site/page-registry.json +33 -18
- package/dist/site/public-surface-audit.json +286 -349
- package/dist/site/publication-authority-registry.json +203 -523
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +590 -940
- package/docs/code-organization.md +9 -6
- package/docs/node-api-reference.md +102 -102
- package/docs/release-governance.md +10 -12
- package/docs/runtime-entry.md +73 -5
- package/docs/runtime-train-validation.md +39 -11
- package/docs/stable-candidate-patrol.md +6 -5
- package/docs/workflow-catalog.md +9 -32
- package/package.json +4 -3
- package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
- package/packages/core/build/standalone/build.js +7 -1
- package/packages/core/consumer/buildchain-config.js +2 -21
- package/packages/core/consumer/contract/entries.js +85 -0
- package/packages/core/consumer/contract/examples.js +96 -0
- package/packages/core/consumer/contract/identity.js +65 -0
- package/packages/core/consumer/contract/inspection.js +45 -0
- package/packages/core/consumer/contract/plan.js +149 -0
- package/packages/core/consumer/contract/products.js +119 -0
- package/packages/core/consumer/contract/reader.js +47 -0
- package/packages/core/consumer/contract/shape.js +46 -0
- package/packages/core/dev-delivery/candidate/admission.js +9 -5
- package/packages/core/dev-delivery/candidate/reservation.js +11 -0
- package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
- package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
- package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
- package/packages/core/dev-delivery/native/actions.js +9 -1
- package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
- package/packages/core/dev-delivery/native/heartbeat.js +6 -4
- package/packages/core/dev-delivery/native/process-entry.js +19 -0
- package/packages/core/dev-delivery/queue/landing-action.js +53 -0
- package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
- package/packages/core/dev-delivery/warrant/service.js +7 -0
- package/packages/core/governance/buildchain-publication-authority.js +8 -23
- package/packages/core/governance/github-governance-authority.js +1 -0
- package/packages/core/observability/controller-evidence.js +0 -8
- package/packages/core/paper/operations/bootstrap.js +2 -0
- package/packages/core/paper/paper-npm-bootstrap.js +2 -2
- package/packages/core/providers/github/attempt-index.js +60 -0
- package/packages/core/providers/github/attempt-journal.js +154 -0
- package/packages/core/providers/github/discussions/materials.js +19 -6
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
- package/packages/core/providers/github/pipeline-events.js +131 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +240 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +198 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
- package/packages/core/providers/github/pipeline-run-entry.js +86 -0
- package/packages/core/providers/github/pipeline-runs.js +132 -0
- package/packages/core/providers/github/pipeline-source.js +195 -0
- package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
- package/packages/core/providers/github/pipeline-stable-source.js +372 -0
- package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
- package/packages/core/providers/github/pipeline-version-readback.js +117 -0
- package/packages/core/providers/github/pipeline-version.js +199 -0
- package/packages/core/providers/github/pipeline-worker.js +108 -0
- package/packages/core/providers/github/release-evidence-assets.js +97 -0
- package/packages/core/publication/candidate/registry-hydration.js +2 -2
- package/packages/core/publication/npm/pack-preview.js +2 -2
- package/packages/core/publication/npm/pack-result.js +31 -0
- package/packages/core/publication/npm/package.js +3 -6
- package/packages/core/publication/npm/pipeline-channel.js +36 -0
- package/packages/core/publication/npm/pipeline-provider.js +137 -0
- package/packages/core/publication/npm/registry.js +12 -9
- package/packages/core/publication/pipeline/actions.js +116 -0
- package/packages/core/publication/pipeline/apply.js +189 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +92 -0
- package/packages/core/publication/pipeline/context.js +39 -0
- package/packages/core/publication/pipeline/development-anchor.js +91 -0
- package/packages/core/publication/pipeline/development-current.js +63 -0
- package/packages/core/publication/pipeline/development-pr.js +112 -0
- package/packages/core/publication/pipeline/development-proof.js +76 -0
- package/packages/core/publication/pipeline/development-transition.js +112 -0
- package/packages/core/publication/pipeline/distribution.js +130 -0
- package/packages/core/publication/pipeline/documents.js +192 -0
- package/packages/core/publication/pipeline/effects.js +192 -0
- package/packages/core/publication/pipeline/files.js +66 -0
- package/packages/core/publication/pipeline/imported-materials.js +32 -0
- package/packages/core/publication/pipeline/journal.js +122 -0
- package/packages/core/publication/pipeline/next-development.js +117 -0
- package/packages/core/publication/pipeline/pack.js +180 -0
- package/packages/core/publication/pipeline/package-policy.js +22 -0
- package/packages/core/publication/pipeline/plan.js +136 -0
- package/packages/core/publication/pipeline/prepare.js +139 -0
- package/packages/core/publication/pipeline/qualification.js +175 -0
- package/packages/core/publication/pipeline/qualify.js +104 -0
- package/packages/core/publication/pipeline/recovery-admission.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
- package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
- package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
- package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
- package/packages/core/publication/pipeline/recovery-import.js +80 -0
- package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
- package/packages/core/publication/pipeline/recovery-materials.js +104 -0
- package/packages/core/publication/pipeline/recovery-plan.js +46 -0
- package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
- package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
- package/packages/core/publication/pipeline/recovery-readback.js +56 -0
- package/packages/core/publication/pipeline/recovery-signing.js +58 -0
- package/packages/core/publication/pipeline/sealed-products.js +79 -0
- package/packages/core/publication/pipeline/settle.js +42 -0
- package/packages/core/publication/pipeline/signing.js +140 -0
- package/packages/core/publication/pipeline/source-plan.js +83 -0
- package/packages/core/publication/pipeline/stable-entry.js +219 -0
- package/packages/core/publication/pipeline/stable-products.js +317 -0
- package/packages/core/publication/pipeline/stable-wait.js +170 -0
- package/packages/core/publication/pipeline/stable.js +82 -0
- package/packages/core/publication/pipeline/version-actions.js +93 -0
- package/packages/core/publication/pipeline/version-build.js +130 -0
- package/packages/core/publication/pipeline/version-context.js +143 -0
- package/packages/core/publication/pipeline/version-material.js +36 -0
- package/packages/core/publication/pipeline/version-preparation.js +69 -0
- package/packages/core/publication/pipeline/version-regeneration.js +162 -0
- package/packages/core/publication/pipeline/version-source-guard.js +69 -0
- package/packages/core/publication/pipeline/version.js +81 -0
- package/packages/core/publication/pipeline/worker.js +72 -0
- package/packages/core/publication/publication-reproducibility.js +2 -2
- package/packages/core/release/next-development-transition.js +15 -15
- package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
- package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
- package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
- package/packages/core/release/stable-release-gate.js +11 -0
- package/packages/core/runtime/buildchain-domain.wasm +0 -0
- package/packages/core/runtime/domain-wasm-artifact.js +2 -2
- package/packages/core/runtime/entry/actions.js +27 -25
- package/packages/core/runtime/entry/attempt.js +25 -0
- package/packages/core/runtime/entry/selection.js +14 -0
- package/packages/core/runtime/entry/source.js +62 -0
- package/packages/core/workflow/attempt/identity.js +170 -0
- package/packages/core/workflow/attempt/journal.js +89 -0
- package/packages/core/workflow/attempt/materials.js +72 -0
- package/packages/core/workflow/attempt/reader-entry.js +12 -0
- package/packages/core/workflow/attempt/reader.js +149 -0
- package/packages/core/workflow/attempt/records.js +153 -0
- package/packages/core/workflow/attempt/store.js +75 -0
- package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
- package/packages/core/workflow/pipeline/action-output.js +11 -0
- package/packages/core/workflow/pipeline/actions.js +52 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +210 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +78 -0
- package/packages/core/workflow/pipeline/build.js +142 -0
- package/packages/core/workflow/pipeline/cancellation.js +169 -0
- package/packages/core/workflow/pipeline/channel-control.js +157 -0
- package/packages/core/workflow/pipeline/channel-source.js +53 -0
- package/packages/core/workflow/pipeline/controller.js +141 -0
- package/packages/core/workflow/pipeline/delivery-control.js +249 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
- package/packages/core/workflow/pipeline/delivery-request.js +198 -0
- package/packages/core/workflow/pipeline/events.js +84 -0
- package/packages/core/workflow/pipeline/fence.js +42 -0
- package/packages/core/workflow/pipeline/group-control.js +59 -0
- package/packages/core/workflow/pipeline/guard-build.js +74 -0
- package/packages/core/workflow/pipeline/guard.js +158 -0
- package/packages/core/workflow/pipeline/host.js +144 -0
- package/packages/core/workflow/pipeline/integration-status.js +113 -0
- package/packages/core/workflow/pipeline/materials.js +58 -0
- package/packages/core/workflow/pipeline/notifications.js +36 -0
- package/packages/core/workflow/pipeline/parent-notification.js +27 -0
- package/packages/core/workflow/pipeline/platforms.js +21 -0
- package/packages/core/workflow/pipeline/progress.js +108 -0
- package/packages/core/workflow/pipeline/projection.js +20 -0
- package/packages/core/workflow/pipeline/reconcile.js +194 -0
- package/packages/core/workflow/pipeline/record-action.js +27 -0
- package/packages/core/workflow/pipeline/recorder-status.js +69 -0
- package/packages/core/workflow/pipeline/recovery-action.js +30 -0
- package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
- package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
- package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
- package/packages/core/workflow/pipeline/recovery-build.js +121 -0
- package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
- package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
- package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
- package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
- package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
- package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
- package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
- package/packages/core/workflow/pipeline/recovery-session.js +129 -0
- package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
- package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
- package/packages/core/workflow/pipeline/runtime-source.js +111 -0
- package/packages/core/workflow/pipeline/selection.js +111 -0
- package/packages/core/workflow/pipeline/session.js +72 -0
- package/packages/core/workflow/pipeline/settlement.js +137 -0
- package/packages/core/workflow/pipeline/wake.js +38 -0
- package/packages/core/workflow/pipeline/web-status.js +108 -0
- package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
- package/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-inventory.mjs +7 -70
- package/scripts/check-pipeline-publication-topology.mjs +50 -0
- package/scripts/check-public-dogfood-contract.mjs +38 -51
- package/scripts/check-release-topology.mjs +15 -8
- package/scripts/check-self-consumer-contract.mjs +133 -0
- package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/generate-site-bundle.mjs +1 -1
- package/scripts/inventory/binary.mjs +9 -19
- package/scripts/inventory/promotion.mjs +2 -13
- package/scripts/prepare-product-toolchain.mjs +10 -0
- package/scripts/verify-golden-path.mjs +5 -1
- package/scripts/verify-product-platform.mjs +65 -0
- package/scripts/verify-standalone-product.mjs +42 -0
- package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/binary/package.json +6 -0
- package/templates/minimal-consumer/binary/src/build.mjs +5 -0
- package/templates/minimal-consumer/binary/src/hello.c +2 -0
- package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
- package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/npm/package.json +6 -0
- package/templates/minimal-consumer/npm/src/build.mjs +5 -0
- package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
- package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
- package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
- package/templates/minimal-consumer/paper/package.json +6 -0
- package/templates/minimal-consumer/paper/src/build.mjs +12 -0
- package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { retainedRecoveryEvidence } from "./recovery-session.js";
|
|
3
|
+
import { pipelinePlatforms } from "./platforms.js";
|
|
4
|
+
import { buildReadbackPlatforms } from "./build-qualification.js";
|
|
5
|
+
import { qualifyRecoveryRuntime } from "./recovery-runtime.js";
|
|
6
|
+
|
|
7
|
+
export async function recoveryBuildEvidence(session, host) {
|
|
8
|
+
const { plan, evidence } = await retainedRecoveryEvidence(session, host);
|
|
9
|
+
const build = evidence.build;
|
|
10
|
+
if (!build) throw new Error("Recovery admission has no qualified build plan");
|
|
11
|
+
const { root, ...body } = build;
|
|
12
|
+
const current = session.observed.history.at(-1);
|
|
13
|
+
const source = await host.source.source(
|
|
14
|
+
current.generation.source.commit,
|
|
15
|
+
current.generation.source.configPath,
|
|
16
|
+
);
|
|
17
|
+
if (
|
|
18
|
+
root !== recordDigest(body) ||
|
|
19
|
+
body.schema !== "buildchain.pipeline-recovery-build/v1" ||
|
|
20
|
+
body.predecessor !== plan.predecessor ||
|
|
21
|
+
recordDigest(body.source) !== recordDigest(current.generation.source) ||
|
|
22
|
+
recordDigest(source.identity) !== recordDigest(body.source) ||
|
|
23
|
+
recordDigest(pipelinePlatforms(source.plan)) !==
|
|
24
|
+
recordDigest(body.platforms) ||
|
|
25
|
+
recordDigest(host.runtime) !== recordDigest(plan.runtime)
|
|
26
|
+
)
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Recovery build plan changed its source, platform inventory or executor",
|
|
29
|
+
);
|
|
30
|
+
const selected = new Set();
|
|
31
|
+
for (const segment of build.segments) {
|
|
32
|
+
const old = segment.readback;
|
|
33
|
+
const inventory = buildReadbackPlatforms(old);
|
|
34
|
+
const fresh = await host.runs.build(
|
|
35
|
+
old.runId,
|
|
36
|
+
old.runAttempt,
|
|
37
|
+
build.source,
|
|
38
|
+
inventory,
|
|
39
|
+
);
|
|
40
|
+
const runtime = await qualifyRecoveryRuntime(
|
|
41
|
+
segment.runtime,
|
|
42
|
+
host.runtime,
|
|
43
|
+
"build",
|
|
44
|
+
host.request,
|
|
45
|
+
);
|
|
46
|
+
if (recordDigest(fresh) !== recordDigest(old) || !runtime.compatible)
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Recovered platform changed during execution requalification",
|
|
49
|
+
);
|
|
50
|
+
for (const platform of segment.platforms) {
|
|
51
|
+
const job = old.jobs[inventory.indexOf(platform)];
|
|
52
|
+
if (
|
|
53
|
+
selected.has(platform) ||
|
|
54
|
+
job?.status !== "completed" ||
|
|
55
|
+
job.conclusion !== "success"
|
|
56
|
+
)
|
|
57
|
+
throw new Error(
|
|
58
|
+
"Recovery cannot reuse duplicate or unsuccessful platform results",
|
|
59
|
+
);
|
|
60
|
+
selected.add(platform);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (
|
|
64
|
+
recordDigest(build.scheduled) !==
|
|
65
|
+
recordDigest(build.platforms.filter((item) => !selected.has(item.platform)))
|
|
66
|
+
)
|
|
67
|
+
throw new Error(
|
|
68
|
+
"Recovery scheduling differs from the qualified remaining platform set",
|
|
69
|
+
);
|
|
70
|
+
return { plan, build };
|
|
71
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { pipelinePlatforms } from "./platforms.js";
|
|
3
|
+
import { qualifyRecoveryRuntime } from "./recovery-runtime.js";
|
|
4
|
+
import { unrecordedRecoveryBuild } from "./recovery-unrecorded-build.js";
|
|
5
|
+
import {
|
|
6
|
+
PIPELINE_BUILD_QUALIFICATION,
|
|
7
|
+
buildReadbackPlatforms,
|
|
8
|
+
verifyPipelineBuildQualification,
|
|
9
|
+
qualifyPipelineBuild,
|
|
10
|
+
} from "./build-qualification.js";
|
|
11
|
+
|
|
12
|
+
async function retainedSegments(session, source, platforms, host) {
|
|
13
|
+
const phase = session.observed.history.at(-1).phases.build;
|
|
14
|
+
const references = (phase?.payload.materials || []).filter(
|
|
15
|
+
(item) =>
|
|
16
|
+
item.id.startsWith("build/provider-") ||
|
|
17
|
+
item.id.startsWith("build/qualified-"),
|
|
18
|
+
);
|
|
19
|
+
if (!references.length && phase?.payload.state !== "success")
|
|
20
|
+
return unrecordedRecoveryBuild(session, source, platforms, host);
|
|
21
|
+
if (references.length !== 1)
|
|
22
|
+
throw new Error(
|
|
23
|
+
"Recovery cannot qualify the unique retained build receipt",
|
|
24
|
+
);
|
|
25
|
+
const value = await host.materialStore(session).read(references[0]);
|
|
26
|
+
if (value.schema === PIPELINE_BUILD_QUALIFICATION) {
|
|
27
|
+
await verifyPipelineBuildQualification(value, source, platforms, host.runs);
|
|
28
|
+
return value.segments;
|
|
29
|
+
}
|
|
30
|
+
const inventory = buildReadbackPlatforms(value);
|
|
31
|
+
if (
|
|
32
|
+
recordDigest(value.source) !== recordDigest(source) ||
|
|
33
|
+
recordDigest([...inventory].sort()) !== recordDigest([...platforms].sort())
|
|
34
|
+
)
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Recovery build receipt changed its original platform inventory",
|
|
37
|
+
);
|
|
38
|
+
const fresh = await host.runs.build(
|
|
39
|
+
value.runId,
|
|
40
|
+
value.runAttempt,
|
|
41
|
+
source,
|
|
42
|
+
inventory,
|
|
43
|
+
);
|
|
44
|
+
if (recordDigest(fresh) !== recordDigest(value))
|
|
45
|
+
throw new Error("Recovery build receipt changed during provider readback");
|
|
46
|
+
const segments = [{ readback: value, platforms, runtime: phase.runtime }];
|
|
47
|
+
qualifyPipelineBuild({
|
|
48
|
+
source,
|
|
49
|
+
platforms,
|
|
50
|
+
segments,
|
|
51
|
+
runId: value.runId,
|
|
52
|
+
runAttempt: value.runAttempt,
|
|
53
|
+
});
|
|
54
|
+
return segments;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Every retained segment is independently observed, even when its old overall
|
|
58
|
+
// workflow failed elsewhere. Only successful jobs with an unchanged execution
|
|
59
|
+
// implementation qualify for reuse. Missing evidence is not invented.
|
|
60
|
+
export async function qualifyRecoveryBuild(session, admission, host) {
|
|
61
|
+
const source = session.observed.history.at(-1).generation.source;
|
|
62
|
+
if (recordDigest(admission.identity) !== recordDigest(source))
|
|
63
|
+
throw new Error("Recovery build admission changed its exact source");
|
|
64
|
+
const platforms = pipelinePlatforms(admission.plan);
|
|
65
|
+
const prior = await retainedSegments(
|
|
66
|
+
session,
|
|
67
|
+
source,
|
|
68
|
+
platforms.map((item) => item.platform),
|
|
69
|
+
host,
|
|
70
|
+
);
|
|
71
|
+
const segments = [],
|
|
72
|
+
runtimes = [],
|
|
73
|
+
selected = new Set();
|
|
74
|
+
for (const segment of prior) {
|
|
75
|
+
const runtime = await qualifyRecoveryRuntime(
|
|
76
|
+
segment.runtime,
|
|
77
|
+
host.runtime,
|
|
78
|
+
"build",
|
|
79
|
+
host.request,
|
|
80
|
+
);
|
|
81
|
+
runtimes.push(runtime);
|
|
82
|
+
const inventory = buildReadbackPlatforms(segment.readback);
|
|
83
|
+
const reusable = segment.platforms.filter((platform) => {
|
|
84
|
+
const job = segment.readback.jobs[inventory.indexOf(platform)];
|
|
85
|
+
return (
|
|
86
|
+
runtime.compatible &&
|
|
87
|
+
job.status === "completed" &&
|
|
88
|
+
job.conclusion === "success"
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
if (reusable.length) {
|
|
92
|
+
for (const platform of reusable) {
|
|
93
|
+
if (
|
|
94
|
+
selected.has(platform) ||
|
|
95
|
+
!platforms.some((item) => item.platform === platform)
|
|
96
|
+
)
|
|
97
|
+
throw new Error(
|
|
98
|
+
"Recovery build has duplicate or undeclared retained platforms",
|
|
99
|
+
);
|
|
100
|
+
selected.add(platform);
|
|
101
|
+
}
|
|
102
|
+
segments.push({ ...segment, platforms: reusable });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const body = {
|
|
106
|
+
schema: "buildchain.pipeline-recovery-build/v1",
|
|
107
|
+
predecessor: session.observed.attempt,
|
|
108
|
+
predecessorHead: session.observed.head,
|
|
109
|
+
source,
|
|
110
|
+
platforms,
|
|
111
|
+
scheduled: platforms.filter((item) => !selected.has(item.platform)),
|
|
112
|
+
segments,
|
|
113
|
+
runtimes,
|
|
114
|
+
reason: !prior.length
|
|
115
|
+
? "No retained product result; execute declared platforms"
|
|
116
|
+
: runtimes.some((item) => !item.compatible)
|
|
117
|
+
? "Changed stage implementation requires affected platform execution"
|
|
118
|
+
: "Reuse independently verified successful platforms; execute only missing results",
|
|
119
|
+
};
|
|
120
|
+
return { ...body, root: recordDigest(body) };
|
|
121
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recoveryExecution,
|
|
3
|
+
admitRecoverySource,
|
|
4
|
+
recoveryPredecessorRuns,
|
|
5
|
+
} from "./recovery-admission.js";
|
|
6
|
+
import {
|
|
7
|
+
selectRecoveryAttempt,
|
|
8
|
+
openRecoveryAttempt,
|
|
9
|
+
retainedRecoveryPlan,
|
|
10
|
+
} from "./recovery-session.js";
|
|
11
|
+
import { qualifyRecoveryBuild } from "./recovery-build.js";
|
|
12
|
+
import { observeRecoveryOwnership } from "./recovery-ownership.js";
|
|
13
|
+
import {
|
|
14
|
+
terminateRecoveryPredecessor,
|
|
15
|
+
recoveryNodePlan,
|
|
16
|
+
replayRecoveryAdmission,
|
|
17
|
+
} from "./recovery-transition.js";
|
|
18
|
+
import { qualifyRecoveryIntegration } from "./recovery-integration.js";
|
|
19
|
+
import { beginRecoveryBuild } from "./recovery-build-control.js";
|
|
20
|
+
|
|
21
|
+
async function duplicateRecovery(session, host) {
|
|
22
|
+
await retainedRecoveryPlan(session, host);
|
|
23
|
+
// Root CAS can commit before the immutable lookup index acknowledgement.
|
|
24
|
+
await host.index.retain(session.intent, session.observed.attempt);
|
|
25
|
+
await host.project(session);
|
|
26
|
+
return {
|
|
27
|
+
operation: "wait",
|
|
28
|
+
attempt: session.observed.attempt,
|
|
29
|
+
reason: `Recovery already selected this successor (${session.observed.status}); any further recovery must select its exact attempt`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function controlPipelineRecovery(attempt, definitionSha, host) {
|
|
34
|
+
const execution = await recoveryExecution(host, definitionSha);
|
|
35
|
+
let session = await selectRecoveryAttempt(attempt, host, execution.entry);
|
|
36
|
+
if (session.duplicate) return duplicateRecovery(session, host);
|
|
37
|
+
const admitted = await admitRecoverySource(session, execution, host);
|
|
38
|
+
const executions = await recoveryPredecessorRuns(session, host);
|
|
39
|
+
if (!executions.terminal) {
|
|
40
|
+
await host.project(session);
|
|
41
|
+
return {
|
|
42
|
+
operation: "wait",
|
|
43
|
+
attempt,
|
|
44
|
+
reason:
|
|
45
|
+
"Predecessor provider executions are still active; wait for completion or cancel their actual runs before recovery",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const build = await qualifyRecoveryBuild(session, admitted.source, host);
|
|
49
|
+
// Post-merge recovery must qualify the retained publication transaction before
|
|
50
|
+
// opening a successor. Its adapter preserves signed bytes and completed effects.
|
|
51
|
+
const integration = admitted.admission.live.merged
|
|
52
|
+
? await qualifyRecoveryIntegration(session, admitted.admission, host)
|
|
53
|
+
: null;
|
|
54
|
+
const publication =
|
|
55
|
+
admitted.admission.live.merged &&
|
|
56
|
+
session.intent.expectedNodes.includes("publish")
|
|
57
|
+
? await host.recoverPublication(session, admitted, execution)
|
|
58
|
+
: null;
|
|
59
|
+
const ownership = await observeRecoveryOwnership(session, executions, host);
|
|
60
|
+
session = await terminateRecoveryPredecessor(
|
|
61
|
+
session,
|
|
62
|
+
executions,
|
|
63
|
+
ownership,
|
|
64
|
+
host,
|
|
65
|
+
);
|
|
66
|
+
const evidence = {
|
|
67
|
+
admitted,
|
|
68
|
+
executions,
|
|
69
|
+
ownership,
|
|
70
|
+
build,
|
|
71
|
+
publication,
|
|
72
|
+
integration,
|
|
73
|
+
};
|
|
74
|
+
const plan = recoveryNodePlan(session, evidence, execution.entry, host);
|
|
75
|
+
session = await openRecoveryAttempt(session, plan, host, evidence);
|
|
76
|
+
await replayRecoveryAdmission(session, host);
|
|
77
|
+
const context = await beginRecoveryBuild(session, host);
|
|
78
|
+
await host.project(session);
|
|
79
|
+
if (!context)
|
|
80
|
+
return {
|
|
81
|
+
operation: "wait",
|
|
82
|
+
attempt: session.observed.attempt,
|
|
83
|
+
reason: "Recovery build already has an execution owner",
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
operation: context.platforms.length ? "build" : "qualify-build",
|
|
87
|
+
context,
|
|
88
|
+
attempt: context.attempt,
|
|
89
|
+
reason: build.reason,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { settleRecoveryOwnership } from "./recovery-ownership-settlement.js";
|
|
3
|
+
import { retainedRecoveryEvidence } from "./recovery-session.js";
|
|
4
|
+
|
|
5
|
+
export async function qualifyRecoveryIntegration(session, admission, host) {
|
|
6
|
+
const current = {
|
|
7
|
+
...session.observed.history.at(-1),
|
|
8
|
+
intent: session.intent,
|
|
9
|
+
};
|
|
10
|
+
const policyAdmission = await host.source.observe(
|
|
11
|
+
session.intent.source.pullRequest,
|
|
12
|
+
{ "config-path": current.generation.source.configPath },
|
|
13
|
+
);
|
|
14
|
+
if (
|
|
15
|
+
!policyAdmission.eligible ||
|
|
16
|
+
!policyAdmission.live.merged ||
|
|
17
|
+
policyAdmission.live.targetBranch !== session.intent.source.targetBranch ||
|
|
18
|
+
policyAdmission.live.mergeCommit !== admission.live.mergeCommit ||
|
|
19
|
+
recordDigest(policyAdmission.live.source) !==
|
|
20
|
+
recordDigest(current.generation.source)
|
|
21
|
+
)
|
|
22
|
+
throw new Error("Recovery observed changed protected integration");
|
|
23
|
+
const integration = await host.integration.observe(current);
|
|
24
|
+
const review = await host.policy.observeMerged(
|
|
25
|
+
current,
|
|
26
|
+
policyAdmission.protectedPlan.review,
|
|
27
|
+
integration,
|
|
28
|
+
);
|
|
29
|
+
if (!review.review || !review.checksPassing)
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Recovery of protected integration requires current exact review and required checks",
|
|
32
|
+
);
|
|
33
|
+
return { integration, review };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function replayPhase(session, phase, value, host) {
|
|
37
|
+
const observed = await session.journal.read();
|
|
38
|
+
const old = observed.phases[phase];
|
|
39
|
+
if (old?.payload.state === "success") return;
|
|
40
|
+
const root = recordDigest(value);
|
|
41
|
+
const reference = await host
|
|
42
|
+
.materialStore(session)
|
|
43
|
+
.retain(`recovery/${phase}-${root.slice(7)}`, value, "provider-readback");
|
|
44
|
+
await session.progress.progress({
|
|
45
|
+
attempt: observed.attempt,
|
|
46
|
+
phase,
|
|
47
|
+
state: "success",
|
|
48
|
+
eventKey: `recovery-${phase}:${root}`,
|
|
49
|
+
materials: [reference],
|
|
50
|
+
expectedHead: observed.head,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function replayRecoveryIntegration(session, host) {
|
|
55
|
+
session.observed = await session.journal.read();
|
|
56
|
+
const { evidence } = await retainedRecoveryEvidence(session, host);
|
|
57
|
+
if (!evidence.admitted.admission.live.merged) return;
|
|
58
|
+
const qualified = await qualifyRecoveryIntegration(
|
|
59
|
+
session,
|
|
60
|
+
evidence.admitted.admission,
|
|
61
|
+
host,
|
|
62
|
+
);
|
|
63
|
+
await replayPhase(session, "review", qualified.review, host);
|
|
64
|
+
if (session.intent.expectedNodes.includes("warrant")) {
|
|
65
|
+
const ownership = await settleRecoveryOwnership(session, qualified, host);
|
|
66
|
+
await replayPhase(
|
|
67
|
+
session,
|
|
68
|
+
"warrant",
|
|
69
|
+
{
|
|
70
|
+
schema: "buildchain.pipeline-recovery-merged-warrant/v1",
|
|
71
|
+
candidate: ownership.candidate,
|
|
72
|
+
proof: ownership.proof,
|
|
73
|
+
predecessor: evidence.ownership.ownerAttempt,
|
|
74
|
+
},
|
|
75
|
+
host,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
await replayPhase(session, "merge", qualified.integration, host);
|
|
79
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createIntegrationDeliveryProof,
|
|
3
|
+
verifyIntegrationDeliveryProof,
|
|
4
|
+
} from "../../dev-delivery/dev-delivery-warrant.js";
|
|
5
|
+
import { recoveryOwnerSession } from "./recovery-ownership.js";
|
|
6
|
+
|
|
7
|
+
export function verifyRecoveryMergeProof(
|
|
8
|
+
proof,
|
|
9
|
+
candidate,
|
|
10
|
+
integration,
|
|
11
|
+
session,
|
|
12
|
+
) {
|
|
13
|
+
if (
|
|
14
|
+
!verifyIntegrationDeliveryProof(proof, {
|
|
15
|
+
repository: session.intent.repository,
|
|
16
|
+
protectedBase: session.intent.source.targetBranch,
|
|
17
|
+
sourceProofRoot: candidate.sourceProofRoot,
|
|
18
|
+
warrantCandidateId: candidate.candidateId,
|
|
19
|
+
currentBase: integration.mergeCommit,
|
|
20
|
+
replayTree: integration.mergeTree,
|
|
21
|
+
mergeGroupHead: integration.mergeCommit,
|
|
22
|
+
mergeGroupTree: integration.mergeTree,
|
|
23
|
+
}).ok
|
|
24
|
+
)
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Recovery merged Warrant has no exact original integration proof",
|
|
27
|
+
);
|
|
28
|
+
return proof;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function retainedRecoveryMergeProof(
|
|
32
|
+
session,
|
|
33
|
+
candidate,
|
|
34
|
+
integration,
|
|
35
|
+
host,
|
|
36
|
+
) {
|
|
37
|
+
let count = 0;
|
|
38
|
+
for (const owner of [...session.observed.history].reverse()) {
|
|
39
|
+
const store = host.materialStore(recoveryOwnerSession(session, owner));
|
|
40
|
+
for (const reference of owner.events
|
|
41
|
+
.flatMap((event) => event.payload.materials)
|
|
42
|
+
.filter(
|
|
43
|
+
(item) =>
|
|
44
|
+
item.id ===
|
|
45
|
+
`merge/proof-${candidate.terminal.evidenceRoot.slice(7)}` ||
|
|
46
|
+
item.id.startsWith("recovery/ownership-request/"),
|
|
47
|
+
)) {
|
|
48
|
+
if (++count > 100)
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Recovery integration proof inventory exceeds its bound",
|
|
51
|
+
);
|
|
52
|
+
const value = await store.read(reference);
|
|
53
|
+
const proof = reference.id.startsWith("merge/proof-")
|
|
54
|
+
? value
|
|
55
|
+
: value.proof;
|
|
56
|
+
if (proof?.proofRoot === candidate.terminal.evidenceRoot)
|
|
57
|
+
return verifyRecoveryMergeProof(proof, candidate, integration, session);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error(
|
|
61
|
+
"Merged predecessor lost its retained pre-effect integration proof",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createRecoveryMergeProof(
|
|
66
|
+
session,
|
|
67
|
+
queue,
|
|
68
|
+
candidate,
|
|
69
|
+
integration,
|
|
70
|
+
) {
|
|
71
|
+
if (
|
|
72
|
+
queue.activeWarrant?.candidateId !== candidate.candidateId ||
|
|
73
|
+
queue.activeWarrant.phase !== "qualified"
|
|
74
|
+
)
|
|
75
|
+
throw new Error(
|
|
76
|
+
"Merged recovery requires the original qualified active Warrant",
|
|
77
|
+
);
|
|
78
|
+
return createIntegrationDeliveryProof({
|
|
79
|
+
repository: session.intent.repository,
|
|
80
|
+
protectedBase: session.intent.source.targetBranch,
|
|
81
|
+
sourceProofRoot: candidate.sourceProofRoot,
|
|
82
|
+
warrant: queue.activeWarrant,
|
|
83
|
+
currentBase: integration.mergeCommit,
|
|
84
|
+
replayTree: integration.mergeTree,
|
|
85
|
+
mergeGroupHead: integration.mergeCommit,
|
|
86
|
+
mergeGroupTree: integration.mergeTree,
|
|
87
|
+
requiredContextRoots: [integration.build.root, integration.root],
|
|
88
|
+
verifiedAt: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import {
|
|
3
|
+
readRecoveryOwnership,
|
|
4
|
+
recoveryDelivery,
|
|
5
|
+
} from "./recovery-ownership.js";
|
|
6
|
+
import { terminalPipelineCandidate } from "./reconcile.js";
|
|
7
|
+
import {
|
|
8
|
+
createRecoveryMergeProof,
|
|
9
|
+
retainedRecoveryMergeProof,
|
|
10
|
+
} from "./recovery-merge-proof.js";
|
|
11
|
+
|
|
12
|
+
function cleanupRequest(session, ownership, qualified) {
|
|
13
|
+
const { queue, candidate, worker } = ownership;
|
|
14
|
+
const active = queue.activeWarrant?.candidateId === candidate.candidateId;
|
|
15
|
+
if (!active && candidate.status !== "queued")
|
|
16
|
+
throw new Error("Recovery cannot clean up an unfenced candidate");
|
|
17
|
+
if (active && worker?.status !== "completed")
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Recovery cleanup requires exact terminal native execution",
|
|
20
|
+
);
|
|
21
|
+
const proof = qualified
|
|
22
|
+
? createRecoveryMergeProof(session, queue, candidate, qualified.integration)
|
|
23
|
+
: null;
|
|
24
|
+
const evidence = {
|
|
25
|
+
schema: "buildchain.pipeline-recovery-ownership/v1",
|
|
26
|
+
attempt: session.observed.attempt,
|
|
27
|
+
ownership,
|
|
28
|
+
proof,
|
|
29
|
+
};
|
|
30
|
+
const request = {
|
|
31
|
+
expectedOldStateRoot: queue.stateRoot,
|
|
32
|
+
pullRequestNumber: candidate.pullRequestNumber,
|
|
33
|
+
expectedSourceHead: candidate.sourceHead,
|
|
34
|
+
eventAction: "cancelled",
|
|
35
|
+
outcome: proof ? "merged" : "cancelled",
|
|
36
|
+
evidenceRoot: proof?.proofRoot || recordDigest(evidence),
|
|
37
|
+
reason: proof
|
|
38
|
+
? "exact-protected-recovery-integration"
|
|
39
|
+
: "explicit-pipeline-recovery",
|
|
40
|
+
execute: true,
|
|
41
|
+
...(active
|
|
42
|
+
? {
|
|
43
|
+
fencingToken: queue.activeWarrant.fencingToken,
|
|
44
|
+
leaseGeneration: queue.activeWarrant.generation,
|
|
45
|
+
}
|
|
46
|
+
: {
|
|
47
|
+
candidateId: candidate.candidateId,
|
|
48
|
+
observedSourceHead: candidate.sourceHead,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
return { ...evidence, method: active ? "settle" : "cancelQueued", request };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function retainRequest(session, pending, host) {
|
|
55
|
+
const root = recordDigest(pending);
|
|
56
|
+
const material = await host
|
|
57
|
+
.materialStore(session)
|
|
58
|
+
.retain(
|
|
59
|
+
`recovery/ownership-request/${root.slice(7)}`,
|
|
60
|
+
pending,
|
|
61
|
+
"provider-readback",
|
|
62
|
+
);
|
|
63
|
+
session.observed = await session.journal.read();
|
|
64
|
+
await session.progress.progress({
|
|
65
|
+
attempt: session.observed.attempt,
|
|
66
|
+
phase: "admission",
|
|
67
|
+
state: "waiting",
|
|
68
|
+
eventKey: `recovery-ownership:${root}`,
|
|
69
|
+
materials: [material],
|
|
70
|
+
expectedHead: session.observed.head,
|
|
71
|
+
});
|
|
72
|
+
session.observed = await session.journal.read();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function verifyTerminal(before, after, request) {
|
|
76
|
+
if (
|
|
77
|
+
!after ||
|
|
78
|
+
after.candidateId !== before.candidateId ||
|
|
79
|
+
after.sourceRoot !== before.sourceRoot ||
|
|
80
|
+
after.status !== request.outcome ||
|
|
81
|
+
after.terminal?.evidenceRoot !== request.evidenceRoot
|
|
82
|
+
)
|
|
83
|
+
throw new Error("Recovery cleanup lacks exact terminal native readback");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function applyRequest(session, pending, host) {
|
|
87
|
+
const current = await session.journal.read();
|
|
88
|
+
if (
|
|
89
|
+
current.attempt !== pending.attempt ||
|
|
90
|
+
current.head !== session.observed.head
|
|
91
|
+
)
|
|
92
|
+
throw new Error(
|
|
93
|
+
"Recovery ownership admission advanced before native cleanup",
|
|
94
|
+
);
|
|
95
|
+
const delivery = recoveryDelivery(session, host);
|
|
96
|
+
let response = null,
|
|
97
|
+
error = null;
|
|
98
|
+
try {
|
|
99
|
+
response = await delivery.service[pending.method](pending.request);
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
error = cause;
|
|
102
|
+
}
|
|
103
|
+
const queue = await delivery.read();
|
|
104
|
+
const candidate = queue.candidates.find(
|
|
105
|
+
(item) => item.candidateId === pending.ownership.candidate.candidateId,
|
|
106
|
+
);
|
|
107
|
+
try {
|
|
108
|
+
verifyTerminal(pending.ownership.candidate, candidate, pending.request);
|
|
109
|
+
if (queue.activeWarrant?.candidateId === candidate.candidateId)
|
|
110
|
+
throw new Error("Recovery cleanup retained its active Warrant");
|
|
111
|
+
} catch (readbackError) {
|
|
112
|
+
throw error || readbackError;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
queue,
|
|
116
|
+
candidate,
|
|
117
|
+
proof: pending.proof,
|
|
118
|
+
response,
|
|
119
|
+
lostResponse: Boolean(error),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function settleRecoveryOwnership(session, qualified, host) {
|
|
124
|
+
const ownership = await readRecoveryOwnership(session, host);
|
|
125
|
+
if (!ownership?.candidate) {
|
|
126
|
+
if (qualified && session.intent.expectedNodes.includes("warrant"))
|
|
127
|
+
throw new Error(
|
|
128
|
+
"Merged recovery lost its original native candidate authority",
|
|
129
|
+
);
|
|
130
|
+
return ownership;
|
|
131
|
+
}
|
|
132
|
+
if (terminalPipelineCandidate(ownership.candidate)) {
|
|
133
|
+
if (Boolean(qualified) !== (ownership.candidate.status === "merged"))
|
|
134
|
+
throw new Error(
|
|
135
|
+
"Protected integration conflicts with terminal native ownership",
|
|
136
|
+
);
|
|
137
|
+
const proof = qualified
|
|
138
|
+
? await retainedRecoveryMergeProof(
|
|
139
|
+
session,
|
|
140
|
+
ownership.candidate,
|
|
141
|
+
qualified.integration,
|
|
142
|
+
host,
|
|
143
|
+
)
|
|
144
|
+
: null;
|
|
145
|
+
return { ...ownership, proof, response: null, lostResponse: false };
|
|
146
|
+
}
|
|
147
|
+
const pending = cleanupRequest(session, ownership, qualified);
|
|
148
|
+
await retainRequest(session, pending, host);
|
|
149
|
+
return applyRequest(session, pending, host);
|
|
150
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { pipelineCandidate, terminalPipelineCandidate } from "./reconcile.js";
|
|
2
|
+
import {
|
|
3
|
+
pipelineDeliveryStore,
|
|
4
|
+
observePipelineWorker,
|
|
5
|
+
} from "./delivery-observation.js";
|
|
6
|
+
|
|
7
|
+
export function recoveryDelivery(session, host) {
|
|
8
|
+
return host.delivery
|
|
9
|
+
? host.delivery(session)
|
|
10
|
+
: pipelineDeliveryStore(session, host);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function recoveryOwnerSession(session, owner) {
|
|
14
|
+
return {
|
|
15
|
+
...session,
|
|
16
|
+
observed: {
|
|
17
|
+
...session.observed,
|
|
18
|
+
attempt: owner.identity.id,
|
|
19
|
+
generation: owner.generation.id,
|
|
20
|
+
history: [owner],
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function recoveryOwnedCandidate(session, queue) {
|
|
26
|
+
if (
|
|
27
|
+
queue.repository !== session.intent.repository ||
|
|
28
|
+
queue.protectedBase !== session.intent.source.targetBranch
|
|
29
|
+
)
|
|
30
|
+
throw new Error(
|
|
31
|
+
"Recovery ownership crossed its protected repository or branch",
|
|
32
|
+
);
|
|
33
|
+
const matches = [...session.observed.history]
|
|
34
|
+
.reverse()
|
|
35
|
+
.filter((owner) => owner.generation.id === session.observed.generation)
|
|
36
|
+
.map((owner) => ({
|
|
37
|
+
owner,
|
|
38
|
+
candidate: pipelineCandidate(queue, { ...owner, intent: session.intent }),
|
|
39
|
+
}))
|
|
40
|
+
.filter((item) => item.candidate);
|
|
41
|
+
if (
|
|
42
|
+
matches.filter((item) => !terminalPipelineCandidate(item.candidate))
|
|
43
|
+
.length > 1
|
|
44
|
+
)
|
|
45
|
+
throw new Error(
|
|
46
|
+
"Recovery found conflicting live native owners in predecessor history",
|
|
47
|
+
);
|
|
48
|
+
return (
|
|
49
|
+
matches.find(
|
|
50
|
+
(item) => item.candidate.candidateId === queue.activeWarrant?.candidateId,
|
|
51
|
+
) ||
|
|
52
|
+
matches[0] ||
|
|
53
|
+
null
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function observeRecoveryOwnership(session, executions, host) {
|
|
58
|
+
if (
|
|
59
|
+
!executions.terminal ||
|
|
60
|
+
executions.predecessor !== session.observed.attempt
|
|
61
|
+
)
|
|
62
|
+
throw new Error(
|
|
63
|
+
"Recovery ownership requires exact terminal predecessor runs",
|
|
64
|
+
);
|
|
65
|
+
return readRecoveryOwnership(session, host);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function readRecoveryOwnership(session, host) {
|
|
69
|
+
if (!session.intent.expectedNodes.includes("warrant")) return null;
|
|
70
|
+
const queue = await recoveryDelivery(session, host).read();
|
|
71
|
+
const selected = recoveryOwnedCandidate(session, queue);
|
|
72
|
+
if (!selected)
|
|
73
|
+
return { queue, ownerAttempt: null, candidate: null, worker: null };
|
|
74
|
+
const { owner, candidate } = selected;
|
|
75
|
+
const active = queue.activeWarrant?.candidateId === candidate.candidateId;
|
|
76
|
+
const worker = active
|
|
77
|
+
? await observePipelineWorker(
|
|
78
|
+
recoveryOwnerSession(session, owner),
|
|
79
|
+
{ ...owner, intent: session.intent },
|
|
80
|
+
queue,
|
|
81
|
+
host,
|
|
82
|
+
)
|
|
83
|
+
: null;
|
|
84
|
+
if (active && worker?.status !== "completed")
|
|
85
|
+
throw new Error(
|
|
86
|
+
"Recovery cannot transfer an active Warrant without exact terminal native worker evidence",
|
|
87
|
+
);
|
|
88
|
+
// Read-only admission: the successor retains cleanup intent before settling
|
|
89
|
+
// original ownership and before starting any new execution.
|
|
90
|
+
return { queue, ownerAttempt: owner.identity.id, candidate, worker };
|
|
91
|
+
}
|