@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.3
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/architecture/action-taxonomy.json +24 -0
- package/architecture/agent-change-map.md +256 -2
- package/architecture/ci-lane-change-budget.json +374 -0
- 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 +99 -0
- package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
- package/architecture/internal-capabilities.json +361 -2
- package/architecture/maintainability-debt.json +6 -4
- package/architecture/maintainability-policy.json +8 -8
- package/architecture/minimal-consumer-migration.json +7567 -0
- package/architecture/product-upstreams.json +20 -0
- package/architecture/release-topology.json +282 -11
- package/architecture/universal-workflow-bootstrap.json +12 -0
- package/architecture/universal-workflow-capability-policy.json +1 -1
- package/architecture/workflow-taxonomy.json +66 -0
- package/dist/readers/business-attempt.cjs +809 -0
- package/dist/site/buildchain-contract.json +7 -5
- package/dist/site/buildchain-site.json +18 -13
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/kfd-claims.json +385 -10
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +21 -21
- package/dist/site/page-registry.json +13 -8
- package/dist/site/public-surface-audit.json +258 -7
- package/dist/site/publication-authority-registry.json +141 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +522 -7
- package/docs/node-api-reference.md +39 -39
- package/docs/runtime-entry.md +45 -5
- package/docs/workflow-catalog.md +6 -0
- package/package.json +4 -3
- 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 +146 -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/native/actions.js +7 -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/queue/landing-action.js +5 -0
- package/packages/core/governance/buildchain-publication-authority.js +6 -0
- 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 +7 -2
- package/packages/core/providers/github/pipeline-checkout.js +49 -0
- package/packages/core/providers/github/pipeline-entry-runs.js +203 -0
- package/packages/core/providers/github/pipeline-events.js +52 -0
- package/packages/core/providers/github/pipeline-integration.js +95 -0
- package/packages/core/providers/github/pipeline-policy.js +213 -0
- package/packages/core/providers/github/pipeline-product-payload.js +50 -0
- package/packages/core/providers/github/pipeline-product-release.js +191 -0
- package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
- package/packages/core/providers/github/pipeline-release-evidence.js +111 -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 +178 -0
- package/packages/core/providers/github/pipeline-stable-source.js +354 -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/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 +87 -0
- package/packages/core/publication/npm/pipeline-provider.js +109 -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 +188 -0
- package/packages/core/publication/pipeline/build-segments.js +107 -0
- package/packages/core/publication/pipeline/capsules.js +89 -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 +124 -0
- package/packages/core/publication/pipeline/documents.js +192 -0
- package/packages/core/publication/pipeline/effects.js +162 -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 +110 -0
- package/packages/core/publication/pipeline/next-development.js +117 -0
- package/packages/core/publication/pipeline/pack.js +157 -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 +43 -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 +215 -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/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 +87 -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 +75 -0
- package/packages/core/workflow/pipeline/build-control.js +140 -0
- package/packages/core/workflow/pipeline/build-evidence.js +92 -0
- package/packages/core/workflow/pipeline/build-qualification.js +145 -0
- package/packages/core/workflow/pipeline/build-result.js +69 -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 +127 -0
- package/packages/core/workflow/pipeline/controller.js +133 -0
- package/packages/core/workflow/pipeline/delivery-control.js +204 -0
- package/packages/core/workflow/pipeline/delivery-observation.js +118 -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 +56 -0
- package/packages/core/workflow/pipeline/guard-build.js +37 -0
- package/packages/core/workflow/pipeline/guard.js +156 -0
- package/packages/core/workflow/pipeline/host.js +114 -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/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 +66 -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 +106 -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/scripts/build-release-discussion-reader.mjs +11 -7
- package/scripts/check-pipeline-publication-topology.mjs +48 -0
- package/scripts/check-release-topology.mjs +7 -1
- package/scripts/generate-minimal-consumer-contract.mjs +155 -0
- package/scripts/generate-site-bundle.mjs +1 -1
- package/scripts/verify-golden-path.mjs +5 -1
- 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,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
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordDigest,
|
|
3
|
+
validateRuntime,
|
|
4
|
+
} from "../../release/discussion/envelope.js";
|
|
5
|
+
import { object, text, choice } from "../../consumer/contract/shape.js";
|
|
6
|
+
import { RECOVERY_ENTRY } from "../../consumer/contract/entries.js";
|
|
7
|
+
|
|
8
|
+
export const PIPELINE_RECOVERY_PLAN = "buildchain.pipeline-recovery-plan/v1";
|
|
9
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
10
|
+
|
|
11
|
+
export function recoveryRequestKey(predecessor, runtime, entry) {
|
|
12
|
+
text(predecessor, "predecessor", /^attempt-[0-9a-f]{64}$/u);
|
|
13
|
+
object(runtime, ["repository", "sha", "readerDigest"]);
|
|
14
|
+
validateRuntime(runtime);
|
|
15
|
+
object(entry, ["repository", "workflow", "sha"]);
|
|
16
|
+
if (
|
|
17
|
+
runtime.repository !== "kungfu-systems/buildchain" ||
|
|
18
|
+
entry.repository !== "kungfu-systems/buildchain" ||
|
|
19
|
+
entry.workflow !== RECOVERY_ENTRY ||
|
|
20
|
+
!/^[0-9a-f]{40}$/u.test(entry.sha)
|
|
21
|
+
)
|
|
22
|
+
throw new Error(
|
|
23
|
+
"Recovery requires an exact canonical runtime and recovery entry",
|
|
24
|
+
);
|
|
25
|
+
return `recover:${recordDigest({ predecessor, runtime, entry })}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Provider adapters must qualify each node before this pure plan is admitted.
|
|
29
|
+
// A reused node names its actual evidence; omission never means rebuild-all.
|
|
30
|
+
export function planPipelineRecovery({
|
|
31
|
+
observed,
|
|
32
|
+
runtime,
|
|
33
|
+
entry,
|
|
34
|
+
nodes,
|
|
35
|
+
evidenceRoot,
|
|
36
|
+
}) {
|
|
37
|
+
const current = observed.history.at(-1);
|
|
38
|
+
if (!current || observed.status === "complete" || !observed.missing.length)
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Completed or absent attempts have no remaining recovery work",
|
|
41
|
+
);
|
|
42
|
+
if (!["failure", "cancelled", "superseded"].includes(observed.status))
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Recovery requires terminal predecessor execution and ownership",
|
|
45
|
+
);
|
|
46
|
+
text(evidenceRoot, "recovery.evidenceRoot", ROOT);
|
|
47
|
+
if (
|
|
48
|
+
!Array.isArray(nodes) ||
|
|
49
|
+
nodes.length !== observed.intent.expectedNodes.length
|
|
50
|
+
)
|
|
51
|
+
throw new Error("Recovery must explicitly classify every required node");
|
|
52
|
+
let pending = false;
|
|
53
|
+
for (const [index, node] of nodes.entries()) {
|
|
54
|
+
object(node, ["phase", "operation", "reason", "evidenceRoots"]);
|
|
55
|
+
if (node.phase !== observed.intent.expectedNodes[index])
|
|
56
|
+
throw new Error("Recovery node order differs from the original intent");
|
|
57
|
+
choice(
|
|
58
|
+
node.operation,
|
|
59
|
+
["reuse", "reconcile", "execute"],
|
|
60
|
+
"recovery.operation",
|
|
61
|
+
);
|
|
62
|
+
text(node.reason, "recovery.reason");
|
|
63
|
+
if (
|
|
64
|
+
node.reason.length > 1000 ||
|
|
65
|
+
!Array.isArray(node.evidenceRoots) ||
|
|
66
|
+
node.evidenceRoots.length > 100 ||
|
|
67
|
+
new Set(node.evidenceRoots).size !== node.evidenceRoots.length
|
|
68
|
+
)
|
|
69
|
+
throw new Error("Recovery node evidence exceeds its exact bound");
|
|
70
|
+
node.evidenceRoots.forEach((root) =>
|
|
71
|
+
text(root, "recovery.nodeEvidence", ROOT),
|
|
72
|
+
);
|
|
73
|
+
if (node.operation === "reuse" && (!node.evidenceRoots.length || pending))
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Only an independently qualified contiguous prefix can be replayed",
|
|
76
|
+
);
|
|
77
|
+
if (node.operation !== "reuse") pending = true;
|
|
78
|
+
}
|
|
79
|
+
const body = {
|
|
80
|
+
schema: PIPELINE_RECOVERY_PLAN,
|
|
81
|
+
intent: observed.intent.id,
|
|
82
|
+
predecessor: observed.attempt,
|
|
83
|
+
predecessorHead: observed.head,
|
|
84
|
+
generation: current.generation.id,
|
|
85
|
+
sourceRoot: recordDigest(current.generation.source),
|
|
86
|
+
runtime,
|
|
87
|
+
entry,
|
|
88
|
+
requestKey: recoveryRequestKey(observed.attempt, runtime, entry),
|
|
89
|
+
nodes: structuredClone(nodes),
|
|
90
|
+
evidenceRoot,
|
|
91
|
+
};
|
|
92
|
+
return { ...body, root: recordDigest(body) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function verifyRecoveryPlan(plan, observed) {
|
|
96
|
+
object(plan, [
|
|
97
|
+
"schema",
|
|
98
|
+
"intent",
|
|
99
|
+
"predecessor",
|
|
100
|
+
"predecessorHead",
|
|
101
|
+
"generation",
|
|
102
|
+
"sourceRoot",
|
|
103
|
+
"runtime",
|
|
104
|
+
"entry",
|
|
105
|
+
"requestKey",
|
|
106
|
+
"nodes",
|
|
107
|
+
"evidenceRoot",
|
|
108
|
+
"root",
|
|
109
|
+
]);
|
|
110
|
+
if (
|
|
111
|
+
recordDigest(plan) !==
|
|
112
|
+
recordDigest(
|
|
113
|
+
planPipelineRecovery({
|
|
114
|
+
observed,
|
|
115
|
+
runtime: plan.runtime,
|
|
116
|
+
entry: plan.entry,
|
|
117
|
+
nodes: plan.nodes,
|
|
118
|
+
evidenceRoot: plan.evidenceRoot,
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
throw new Error(
|
|
123
|
+
"Recovery plan changed its exact predecessor or qualified evidence",
|
|
124
|
+
);
|
|
125
|
+
return plan;
|
|
126
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
recordDigest,
|
|
3
|
+
validateRuntime,
|
|
4
|
+
} from "../../release/discussion/envelope.js";
|
|
5
|
+
|
|
6
|
+
const stages = {
|
|
7
|
+
build: "actions/workflow/pipeline/build/dist",
|
|
8
|
+
"publication-build": "actions/publication/pipeline/build/dist",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function qualifyRecoveryRuntime(
|
|
12
|
+
previous,
|
|
13
|
+
current,
|
|
14
|
+
stage,
|
|
15
|
+
request,
|
|
16
|
+
) {
|
|
17
|
+
validateRuntime(previous);
|
|
18
|
+
validateRuntime(current);
|
|
19
|
+
if (
|
|
20
|
+
![previous.repository, current.repository].every(
|
|
21
|
+
(repo) => repo === "kungfu-systems/buildchain",
|
|
22
|
+
) ||
|
|
23
|
+
!stages[stage]
|
|
24
|
+
)
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Recovery runtime comparison requires a declared canonical execution stage",
|
|
27
|
+
);
|
|
28
|
+
if (
|
|
29
|
+
previous.sha === current.sha &&
|
|
30
|
+
previous.readerDigest !== current.readerDigest
|
|
31
|
+
)
|
|
32
|
+
throw new Error("An exact runtime changed its immutable reader bytes");
|
|
33
|
+
async function closure(runtime) {
|
|
34
|
+
const files = [];
|
|
35
|
+
for (const name of ["index.js", "buildchain-domain.wasm"]) {
|
|
36
|
+
const file = `${stages[stage]}/${name}`;
|
|
37
|
+
const metadata = await request(
|
|
38
|
+
`/repos/${runtime.repository}/contents/${file}?ref=${runtime.sha}`,
|
|
39
|
+
{ allow404: name.endsWith(".wasm") },
|
|
40
|
+
);
|
|
41
|
+
if (!metadata && name.endsWith(".wasm")) {
|
|
42
|
+
files.push({ path: file, blob: null });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (
|
|
46
|
+
metadata?.type !== "file" ||
|
|
47
|
+
!/^[0-9a-f]{40}$/u.test(metadata.sha || "")
|
|
48
|
+
)
|
|
49
|
+
throw new Error(
|
|
50
|
+
"Recovery runtime has no exact distributed stage implementation",
|
|
51
|
+
);
|
|
52
|
+
files.push({ path: file, blob: metadata.sha });
|
|
53
|
+
}
|
|
54
|
+
return files;
|
|
55
|
+
}
|
|
56
|
+
const before = await closure(previous);
|
|
57
|
+
const after = previous.sha === current.sha ? before : await closure(current);
|
|
58
|
+
const body = {
|
|
59
|
+
schema: "buildchain.pipeline-recovery-runtime/v1",
|
|
60
|
+
stage,
|
|
61
|
+
previous,
|
|
62
|
+
current,
|
|
63
|
+
before,
|
|
64
|
+
after,
|
|
65
|
+
compatible: recordDigest(before) === recordDigest(after),
|
|
66
|
+
};
|
|
67
|
+
return { ...body, root: recordDigest(body) };
|
|
68
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { readBusinessAttempt } from "../attempt/reader.js";
|
|
2
|
+
import { atomicAttemptJournal } from "../attempt/journal.js";
|
|
3
|
+
import { businessAttempt } from "../attempt/identity.js";
|
|
4
|
+
import { attemptRecord } from "../attempt/records.js";
|
|
5
|
+
import { pipelineProgress } from "./progress.js";
|
|
6
|
+
import { recoveryRequestKey, verifyRecoveryPlan } from "./recovery-plan.js";
|
|
7
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
8
|
+
|
|
9
|
+
export async function selectRecoveryAttempt(attempt, host, entry) {
|
|
10
|
+
const loaded = await host.index.resolve(attempt);
|
|
11
|
+
const journal = atomicAttemptJournal(host.provider, loaded.snapshot.intent);
|
|
12
|
+
const observed = await journal.read();
|
|
13
|
+
if (observed.intent.repository !== host.repository)
|
|
14
|
+
throw new Error("Recovery cannot cross consumer repositories");
|
|
15
|
+
const selected = observed.history.find(
|
|
16
|
+
(item) => item.identity.id === attempt,
|
|
17
|
+
);
|
|
18
|
+
if (!selected)
|
|
19
|
+
throw new Error("Recovery attempt is absent from canonical history");
|
|
20
|
+
const key = recoveryRequestKey(attempt, host.runtime, entry);
|
|
21
|
+
const current = observed.history.at(-1);
|
|
22
|
+
const duplicate = current.identity.id !== attempt;
|
|
23
|
+
if (
|
|
24
|
+
duplicate &&
|
|
25
|
+
(current.identity.predecessor !== attempt ||
|
|
26
|
+
current.identity.requestKey !== key)
|
|
27
|
+
)
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Historical recovery cannot fork current ownership; select ${current.identity.id}`,
|
|
30
|
+
);
|
|
31
|
+
return {
|
|
32
|
+
intent: observed.intent,
|
|
33
|
+
journal,
|
|
34
|
+
observed,
|
|
35
|
+
selected,
|
|
36
|
+
duplicate,
|
|
37
|
+
progress: pipelineProgress(journal, { intent: observed.intent, ...host }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function openRecoveryAttempt(session, plan, host, evidence) {
|
|
42
|
+
const observed = await session.journal.read();
|
|
43
|
+
verifyRecoveryPlan(plan, observed);
|
|
44
|
+
if (recordDigest(plan.runtime) !== recordDigest(host.runtime))
|
|
45
|
+
throw new Error("Recovery executor differs from the admitted runtime");
|
|
46
|
+
if (evidence === undefined || recordDigest(evidence) !== plan.evidenceRoot)
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Recovery requires the actual qualified evidence bound by its plan",
|
|
49
|
+
);
|
|
50
|
+
const generation = observed.history.at(-1).generation;
|
|
51
|
+
const identity = businessAttempt({
|
|
52
|
+
intent: session.intent,
|
|
53
|
+
generation,
|
|
54
|
+
predecessor: observed.attempt,
|
|
55
|
+
requestKey: plan.requestKey,
|
|
56
|
+
});
|
|
57
|
+
const pending = {
|
|
58
|
+
...session,
|
|
59
|
+
observed: { ...observed, history: [{ identity, generation }] },
|
|
60
|
+
};
|
|
61
|
+
const reference = await host
|
|
62
|
+
.materialStore(pending)
|
|
63
|
+
.retain("recovery/admission", plan);
|
|
64
|
+
const evidenceReference = await host
|
|
65
|
+
.materialStore(pending)
|
|
66
|
+
.retain("recovery/evidence", evidence);
|
|
67
|
+
const root = attemptRecord({
|
|
68
|
+
intent: session.intent,
|
|
69
|
+
attempt: identity,
|
|
70
|
+
generation,
|
|
71
|
+
writer: host.writer,
|
|
72
|
+
runtime: host.runtime,
|
|
73
|
+
eventKey: `open:${identity.id}`,
|
|
74
|
+
phase: "attempt",
|
|
75
|
+
state: "running",
|
|
76
|
+
reason:
|
|
77
|
+
"Explicit recovery; completed effects remain immutable in predecessor history",
|
|
78
|
+
materials: [reference, evidenceReference],
|
|
79
|
+
});
|
|
80
|
+
const opened = await session.journal.append(root, observed.head);
|
|
81
|
+
await host.index.retain(session.intent, identity.id);
|
|
82
|
+
return {
|
|
83
|
+
...session,
|
|
84
|
+
observed: opened,
|
|
85
|
+
duplicate: false,
|
|
86
|
+
progress: pipelineProgress(session.journal, {
|
|
87
|
+
intent: session.intent,
|
|
88
|
+
...host,
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function retainedRecoveryPlan(session, host) {
|
|
94
|
+
const current = session.observed.history.at(-1);
|
|
95
|
+
const references = current.events[0].payload.materials.filter(
|
|
96
|
+
(item) => item.id === "recovery/admission",
|
|
97
|
+
);
|
|
98
|
+
if (references.length !== 1)
|
|
99
|
+
throw new Error("Recovery successor has no unique retained admission plan");
|
|
100
|
+
const plan = await host.materialStore(session).read(references[0]);
|
|
101
|
+
const records = session.observed.snapshot.records.filter((record) =>
|
|
102
|
+
session.observed.history
|
|
103
|
+
.slice(0, -1)
|
|
104
|
+
.some((item) => item.identity.id === record.attempt),
|
|
105
|
+
);
|
|
106
|
+
const predecessor = readBusinessAttempt({ intent: session.intent, records });
|
|
107
|
+
verifyRecoveryPlan(plan, predecessor);
|
|
108
|
+
if (
|
|
109
|
+
current.identity.predecessor !== plan.predecessor ||
|
|
110
|
+
current.identity.requestKey !== plan.requestKey
|
|
111
|
+
)
|
|
112
|
+
throw new Error("Recovery successor does not bind its immutable admission");
|
|
113
|
+
return plan;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function retainedRecoveryEvidence(session, host) {
|
|
117
|
+
const plan = await retainedRecoveryPlan(session, host);
|
|
118
|
+
const references = session.observed.history
|
|
119
|
+
.at(-1)
|
|
120
|
+
.events[0].payload.materials.filter(
|
|
121
|
+
(item) => item.id === "recovery/evidence",
|
|
122
|
+
);
|
|
123
|
+
if (references.length !== 1)
|
|
124
|
+
throw new Error("Recovery successor lacks its unique qualified evidence");
|
|
125
|
+
const evidence = await host.materialStore(session).read(references[0]);
|
|
126
|
+
if (recordDigest(evidence) !== plan.evidenceRoot)
|
|
127
|
+
throw new Error("Recovery evidence differs from its immutable admission");
|
|
128
|
+
return { plan, evidence };
|
|
129
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { recordDigest } from "../../release/discussion/envelope.js";
|
|
2
|
+
import { planPipelineRecovery } from "./recovery-plan.js";
|
|
3
|
+
import { settleRecoveryOwnership } from "./recovery-ownership-settlement.js";
|
|
4
|
+
import { qualifyRecoveryIntegration } from "./recovery-integration.js";
|
|
5
|
+
import { retainedRecoveryEvidence } from "./recovery-session.js";
|
|
6
|
+
|
|
7
|
+
export async function terminateRecoveryPredecessor(
|
|
8
|
+
session,
|
|
9
|
+
executions,
|
|
10
|
+
ownership,
|
|
11
|
+
host,
|
|
12
|
+
) {
|
|
13
|
+
const observed = await session.journal.read();
|
|
14
|
+
if (observed.head !== session.observed.head || !executions.terminal)
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Recovery predecessor advanced after terminal execution readback",
|
|
17
|
+
);
|
|
18
|
+
if (["failure", "cancelled", "superseded"].includes(observed.status))
|
|
19
|
+
return session;
|
|
20
|
+
if (observed.status === "complete")
|
|
21
|
+
throw new Error("Completed attempt has no recovery work");
|
|
22
|
+
const evidence = { executions, ownership };
|
|
23
|
+
const material = await host
|
|
24
|
+
.materialStore(session)
|
|
25
|
+
.retain("recovery/predecessor-terminal", evidence, "provider-readback");
|
|
26
|
+
const after = await session.progress.progress({
|
|
27
|
+
attempt: observed.attempt,
|
|
28
|
+
phase: observed.missing[0],
|
|
29
|
+
state: "cancelled",
|
|
30
|
+
eventKey: `recovery-terminal:${recordDigest(evidence)}`,
|
|
31
|
+
reason:
|
|
32
|
+
"Explicit recovery after provider executions became terminal; native ownership remains fenced until successor admission",
|
|
33
|
+
materials: [material],
|
|
34
|
+
expectedHead: observed.head,
|
|
35
|
+
});
|
|
36
|
+
return { ...session, observed: after };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function recoveryNodePlan(session, evidence, entry, host) {
|
|
40
|
+
const cleanup = Boolean(evidence.ownership?.candidate);
|
|
41
|
+
const nodes = session.intent.expectedNodes.map((phase) => {
|
|
42
|
+
if (phase === "admission")
|
|
43
|
+
return {
|
|
44
|
+
phase,
|
|
45
|
+
operation: cleanup ? "reconcile" : "reuse",
|
|
46
|
+
reason:
|
|
47
|
+
"Exact original source and protected route independently reobserved",
|
|
48
|
+
evidenceRoots: [recordDigest(evidence.admitted)],
|
|
49
|
+
};
|
|
50
|
+
if (phase === "build")
|
|
51
|
+
return {
|
|
52
|
+
phase,
|
|
53
|
+
operation: evidence.build.scheduled.length
|
|
54
|
+
? "execute"
|
|
55
|
+
: cleanup
|
|
56
|
+
? "reconcile"
|
|
57
|
+
: "reuse",
|
|
58
|
+
reason: evidence.build.reason,
|
|
59
|
+
evidenceRoots: [evidence.build.root],
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
phase,
|
|
63
|
+
operation: "reconcile",
|
|
64
|
+
reason:
|
|
65
|
+
"Reobserve protected provider authority and execute remaining work",
|
|
66
|
+
evidenceRoots: [],
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
return planPipelineRecovery({
|
|
70
|
+
observed: session.observed,
|
|
71
|
+
runtime: host.runtime,
|
|
72
|
+
entry,
|
|
73
|
+
nodes,
|
|
74
|
+
evidenceRoot: recordDigest(evidence),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function replayRecoveryAdmission(session, host) {
|
|
79
|
+
session.observed = await session.journal.read();
|
|
80
|
+
const { plan, evidence } = await retainedRecoveryEvidence(session, host);
|
|
81
|
+
const value = {
|
|
82
|
+
schema: "buildchain.pipeline-recovery-source/v1",
|
|
83
|
+
recoveryPlanRoot: plan.root,
|
|
84
|
+
predecessor: plan.predecessor,
|
|
85
|
+
source: evidence.admitted.source.identity,
|
|
86
|
+
admission: evidence.admitted.admission,
|
|
87
|
+
caller: evidence.admitted.caller,
|
|
88
|
+
};
|
|
89
|
+
const prior = session.observed.phases.admission;
|
|
90
|
+
if (prior?.payload.state === "success") {
|
|
91
|
+
if (
|
|
92
|
+
prior.payload.state !== "success" ||
|
|
93
|
+
prior.payload.materials.length !== 1 ||
|
|
94
|
+
recordDigest(
|
|
95
|
+
sourceFields(
|
|
96
|
+
await host.materialStore(session).read(prior.payload.materials[0]),
|
|
97
|
+
),
|
|
98
|
+
) !== recordDigest(value)
|
|
99
|
+
)
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Recovery admission replay differs from its immutable source qualification",
|
|
102
|
+
);
|
|
103
|
+
return session;
|
|
104
|
+
}
|
|
105
|
+
const qualified = evidence.admitted.admission.live.merged
|
|
106
|
+
? await qualifyRecoveryIntegration(
|
|
107
|
+
session,
|
|
108
|
+
evidence.admitted.admission,
|
|
109
|
+
host,
|
|
110
|
+
)
|
|
111
|
+
: null;
|
|
112
|
+
const ownership = await settleRecoveryOwnership(session, qualified, host);
|
|
113
|
+
const reference = await host
|
|
114
|
+
.materialStore(session)
|
|
115
|
+
.retain(
|
|
116
|
+
"source/recovery-admission",
|
|
117
|
+
{ ...value, ownership },
|
|
118
|
+
"provider-readback",
|
|
119
|
+
);
|
|
120
|
+
await session.progress.progress({
|
|
121
|
+
attempt: session.observed.attempt,
|
|
122
|
+
phase: "admission",
|
|
123
|
+
state: "success",
|
|
124
|
+
eventKey: `recovery-source:${plan.root}`,
|
|
125
|
+
materials: [reference],
|
|
126
|
+
expectedHead: session.observed.head,
|
|
127
|
+
});
|
|
128
|
+
session.observed = await session.journal.read();
|
|
129
|
+
return session;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function sourceFields(value) {
|
|
133
|
+
const { ownership, ...source } = value;
|
|
134
|
+
return source;
|
|
135
|
+
}
|