@kungfu-tech/buildchain 3.0.8 → 3.0.9-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/actions/promote-buildchain-ref/README.md +8 -7
  2. package/bin/buildchain.mjs +34 -33
  3. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  4. package/dist/site/agent-index.json +0 -1
  5. package/dist/site/artifact-schemas.json +0 -2
  6. package/dist/site/badge-endpoint-registry.json +9 -9
  7. package/dist/site/badges/v1/kfd-8/aligned.json +1 -1
  8. package/dist/site/badges/v1/kfd-8/declared.json +1 -1
  9. package/dist/site/badges/v1/kfd-8/downgraded.json +1 -1
  10. package/dist/site/badges/v1/kfd-8/draft.json +1 -1
  11. package/dist/site/badges/v1/kfd-8/failed.json +1 -1
  12. package/dist/site/badges/v1/kfd-8/missing.json +1 -1
  13. package/dist/site/badges/v1/kfd-8/passed.json +1 -1
  14. package/dist/site/badges/v1/kfd-8/planned.json +1 -1
  15. package/dist/site/buildchain-contract.json +27 -22
  16. package/dist/site/buildchain-site.json +38 -28
  17. package/dist/site/capability-registry.json +1 -1
  18. package/dist/site/cli-registry.json +8 -7
  19. package/dist/site/controller-registry.json +6 -2
  20. package/dist/site/kfd-claims.json +34 -7
  21. package/dist/site/kfd-upstream-aggregate.json +10 -10
  22. package/dist/site/manual-registry.json +6 -6
  23. package/dist/site/node-api-registry.json +754 -228
  24. package/dist/site/page-registry.json +28 -18
  25. package/dist/site/public-surface-audit.json +19 -9
  26. package/dist/site/publication-registry.json +4 -4
  27. package/dist/site/release-passport-check-manifest.json +33 -5
  28. package/dist/site/release-provenance.json +2 -1
  29. package/dist/site/schemas/release-passport-v1.schema.json +3 -0
  30. package/dist/site/site-manifest.json +10 -10
  31. package/dist/site/workflow-registry.json +19 -9
  32. package/docs/aws-us-elastic-runner-burst-plane.md +15 -3
  33. package/docs/cli-reference.md +6 -6
  34. package/docs/cli.md +2 -3
  35. package/docs/dev-alpha-candidate-patrol.md +45 -20
  36. package/docs/kfd-support.md +7 -7
  37. package/docs/node-api-reference.md +179 -140
  38. package/docs/release-passport.md +18 -12
  39. package/docs/release-train.md +8 -0
  40. package/package.json +4 -3
  41. package/packages/core/artifact-verification-envelope.js +114 -0
  42. package/packages/core/dev-alpha-active-release-train.js +460 -0
  43. package/packages/core/dev-alpha-candidate-selection.js +181 -0
  44. package/packages/core/index.js +4 -5
  45. package/packages/core/kfd-adopter-manifest.js +390 -0
  46. package/packages/core/kfd-product-gates.js +0 -300
  47. package/packages/core/release-passport-contract.js +219 -6
  48. package/packages/core/release-passport.js +139 -242
  49. package/scripts/aws-macos-jit-controller-core.mjs +76 -0
  50. package/scripts/aws-macos-jit-controller.mjs +29 -0
  51. package/scripts/aws-macos-jit-source-rebind.mjs +503 -0
  52. package/scripts/buildchain-cli-help.mjs +4 -2
  53. package/scripts/check-inventory.mjs +2 -2
  54. package/scripts/dev-alpha-candidate-patrol.mjs +196 -201
  55. package/scripts/generate-site-bundle.mjs +1 -8
  56. package/scripts/site-capability-metadata.mjs +3 -1
  57. package/dist/site/schemas/kfd-support-projection-v1.schema.json +0 -106
@@ -399,18 +399,24 @@ instead of trusting docs or release notes.
399
399
 
400
400
  ### KFD support projection
401
401
 
402
- Pass `--kfd-support-matrix-json` together with three
403
- `--kfd-product-gate-json` arguments for KFD-4, KFD-5, and KFD-7. The collector
404
- verifies the product-owned KFD-1..13 matrix against the installed
405
- `@kungfu-tech/kfd` standards, binds each gate to the exact release source and
406
- fresh evidence cut, and emits `kfd-support.json`. The identical projection is
407
- embedded as `kfdSupport` in `buildchain.release.json`.
408
-
409
- Release verification fails closed when the sibling is missing or differs,
410
- when a gate expires, when matrix and gate status disagree, or when a matrix
411
- widens KFD-4/5 candidate, KFD-6 unsupported, KFD-8..13 draft, or any non-shipped
412
- state. A passed projection is release evidence only; product qualification,
413
- activation, certification, and support ownership remain outside Buildchain.
402
+ Pass the standard full-cut declaration with `--kfd-adopter-manifest-json`
403
+ together with three `--kfd-product-gate-json` arguments for KFD-4, KFD-5, and
404
+ KFD-7. The collector invokes the verifier from the exact installed
405
+ `@kungfu-tech/kfd` package, binds the package artifact, registry and verifier
406
+ roots, the exact Buildchain source, the manifest/report/bundle witness roots,
407
+ and the existing product-gate roots. It emits `kfd-adopter-manifest.json`,
408
+ `kfd-adopter-manifest-gate.json`, and a legacy `kfd-support.json` projection.
409
+ The release passport and `artifact-evidence.json` carry the same rooted
410
+ `kfdAdopter` binding.
411
+
412
+ The adopter manifest is the sole declaration authority. The old
413
+ `--kfd-support-matrix-json` input is retained only as a compatibility check: if
414
+ supplied with the manifest, it must exactly equal the generated legacy
415
+ projection. Release verification fails closed when any sibling, package root,
416
+ source root, witness root, gate root, passport binding, or artifact-evidence
417
+ binding differs. A passed binding is release evidence only; product
418
+ qualification, activation, certification, and support ownership remain outside
419
+ Buildchain.
414
420
 
415
421
  ### Floating Buildchain contract lock
416
422
 
@@ -74,6 +74,14 @@ an invalid transition fails closed.
74
74
  changing `trainRoot`, `cutRoot`, candidate identity, generation, or state. The
75
75
  same exact observation is idempotent.
76
76
 
77
+ Buildchain v3 Candidate Patrol resolves the open managed candidate's persisted
78
+ train before it considers a new qualified development head. It reads back the
79
+ candidate ref and tree, Alpha base, and exact Buildchain runtime. Matching
80
+ coordinates resume the frozen candidate; a newer development head becomes a
81
+ single rooted observation. Candidate, tree, base, runtime, or route drift emits
82
+ a rooted hold and stops settlement. Only a validated train already carrying an
83
+ enumerated `superseded` transition is reported as superseded.
84
+
77
85
  ## Readback and legacy state
78
86
 
79
87
  `validateReleaseCut()` checks the standalone immutable cut and its canonical
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "3.0.8",
3
+ "version": "3.0.9-alpha.0",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -33,6 +33,8 @@
33
33
  "./issue-reporting": "./packages/core/issue-reporting.js",
34
34
  "./kfd": "./packages/core/kfd.js",
35
35
  "./kfd-product-gates": "./packages/core/kfd-product-gates.js",
36
+ "./kfd-adopter-manifest": "./packages/core/kfd-adopter-manifest.js",
37
+ "./kfd-adopter-release-binding": "./packages/core/artifact-verification-envelope.js",
36
38
  "./kfd-agent-hub": "./packages/core/kfd-agent-hub.js",
37
39
  "./buildchain-layout": "./packages/core/buildchain-layout.js",
38
40
  "./release-line-bootstrap": "./packages/core/release-line-bootstrap.js",
@@ -87,7 +89,6 @@
87
89
  "./site/schemas/release-passport-v1.schema.json": "./dist/site/schemas/release-passport-v1.schema.json",
88
90
  "./site/schemas/kfd-agent-hub-adoption.schema.json": "./dist/site/schemas/kfd-agent-hub-adoption.schema.json",
89
91
  "./site/schemas/kfd-product-gate-input-v1.schema.json": "./dist/site/schemas/kfd-product-gate-input-v1.schema.json",
90
- "./site/schemas/kfd-support-projection-v1.schema.json": "./dist/site/schemas/kfd-support-projection-v1.schema.json",
91
92
  "./site/buildchain-contract.json": "./dist/site/buildchain-contract.json",
92
93
  "./site/product-mechanism.json": "./dist/site/product-mechanism.json",
93
94
  "./site/release-provenance.json": "./dist/site/release-provenance.json",
@@ -139,7 +140,7 @@
139
140
  "typescript": "^5.9.0"
140
141
  },
141
142
  "dependencies": {
142
- "@kungfu-tech/kfd": "1.0.0-alpha.53",
143
+ "@kungfu-tech/kfd": "1.0.0-alpha.60",
143
144
  "ajv": "8.20.0",
144
145
  "smol-toml": "1.7.0"
145
146
  }
@@ -1,4 +1,11 @@
1
1
  import crypto from "node:crypto";
2
+ import kfdPackageJson from "@kungfu-tech/kfd/package.json" with { type: "json" };
3
+ import kfdSelfAdopterManifest from "@kungfu-tech/kfd/adopter-conformance/adopters/kfd/manifest.json" with { type: "json" };
4
+ import {
5
+ validateKfdAdopterManifestGate,
6
+ validateKfdLegacySupportMatrixProjection,
7
+ } from "./kfd-adopter-manifest.js";
8
+ import { kfdProductGateDigest } from "./kfd-product-gates.js";
2
9
 
3
10
  export const ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT =
4
11
  "kungfu-buildchain-artifact-verification-envelope";
@@ -6,6 +13,8 @@ export const ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT =
6
13
  "kungfu-buildchain-artifact-verification-envelope-check";
7
14
  export const KFX_ADMISSION_INPUTS_CONTRACT =
8
15
  "kungfu-buildchain-kfx-admission-inputs";
16
+ export const KFD_ADOPTER_RELEASE_BINDING_CONTRACT =
17
+ "kungfu-buildchain-kfd-adopter-release-binding";
9
18
 
10
19
  const ARTIFACT_VERIFICATION_CONTRACT =
11
20
  "kungfu-buildchain-artifact-verification";
@@ -384,3 +393,108 @@ export function projectArtifactVerificationEnvelopeToKfx({
384
393
  kfdAssessment: jsonClone(envelope.kfdAssessment),
385
394
  };
386
395
  }
396
+
397
+ export function installedKfdPackageArtifactRoot() {
398
+ const cut = kfdSelfAdopterManifest?.kfdCut?.package;
399
+ if (cut?.name !== kfdPackageJson.name || cut?.version !== kfdPackageJson.version
400
+ || !SHA256_PATTERN.test(String(cut?.artifactRoot || ""))) {
401
+ throw new Error("installed KFD package does not expose a matching rooted self-adopter package cut");
402
+ }
403
+ return cut.artifactRoot;
404
+ }
405
+
406
+ function kfdBindingBody({ manifest, manifestGate, legacyProjection, paths }) {
407
+ return {
408
+ schemaVersion: 1,
409
+ contract: KFD_ADOPTER_RELEASE_BINDING_CONTRACT,
410
+ status: "passed",
411
+ qualifying: false,
412
+ selfCertified: false,
413
+ authority: {
414
+ manifestPath: paths.manifest,
415
+ manifestContract: manifest.contract,
416
+ manifestRoot: manifestGate.authority.manifestRoot,
417
+ gatePath: paths.gate,
418
+ gateRoot: manifestGate.gateRoot,
419
+ },
420
+ source: jsonClone(manifestGate.source),
421
+ standardPackage: jsonClone(manifestGate.standardPackage),
422
+ witness: {
423
+ decisionRoot: manifestGate.decisionWitness.root,
424
+ verificationReportRoot: manifestGate.manifestVerificationReportRoot,
425
+ bundleRoot: manifestGate.manifestBundleRoot,
426
+ },
427
+ legacyProjection: {
428
+ path: paths.legacy,
429
+ contract: legacyProjection.contract,
430
+ root: kfdProductGateDigest(legacyProjection),
431
+ authorityRoot: legacyProjection.authority.root,
432
+ gateRoot: legacyProjection.authority.gateRoot,
433
+ },
434
+ nonClaims: [
435
+ "This binding proves exact release-envelope consumption, not KFD certification or release authorization.",
436
+ "The legacy support matrix remains a projection of the standard adopter manifest authority.",
437
+ ],
438
+ };
439
+ }
440
+
441
+ export function createKfdAdopterReleaseBinding({
442
+ manifest,
443
+ manifestGate,
444
+ legacyProjection,
445
+ manifestPath = "kfd-adopter-manifest.json",
446
+ gatePath = "kfd-adopter-manifest-gate.json",
447
+ legacyProjectionPath = "kfd-support.json",
448
+ expectedSourceSha = "",
449
+ } = {}) {
450
+ const gate = validateKfdAdopterManifestGate(manifestGate, {
451
+ expectedSourceSha,
452
+ checkedAt: manifestGate?.checkedAt,
453
+ });
454
+ const legacy = validateKfdLegacySupportMatrixProjection(legacyProjection, { manifest, manifestGate });
455
+ if (!gate.valid || !legacy.valid) {
456
+ throw new Error(`KFD adopter release binding inputs are invalid: ${JSON.stringify([...gate.issues, ...legacy.issues])}`);
457
+ }
458
+ const body = kfdBindingBody({
459
+ manifest,
460
+ manifestGate,
461
+ legacyProjection,
462
+ paths: { manifest: manifestPath, gate: gatePath, legacy: legacyProjectionPath },
463
+ });
464
+ return { ...body, bindingRoot: kfdProductGateDigest(body) };
465
+ }
466
+
467
+ export function validateKfdAdopterReleaseBinding(binding, {
468
+ manifest,
469
+ manifestGate,
470
+ legacyProjection,
471
+ expectedSourceSha = "",
472
+ } = {}) {
473
+ const issues = [];
474
+ if (!binding || binding.schemaVersion !== 1 || binding.contract !== KFD_ADOPTER_RELEASE_BINDING_CONTRACT) {
475
+ return { valid: false, issues: [{ code: "adopter-release-binding-contract", path: "", message: `binding must use ${KFD_ADOPTER_RELEASE_BINDING_CONTRACT} v1` }] };
476
+ }
477
+ const copy = jsonClone(binding);
478
+ const bindingRoot = copy.bindingRoot;
479
+ delete copy.bindingRoot;
480
+ if (bindingRoot !== kfdProductGateDigest(copy)) {
481
+ issues.push({ code: "adopter-release-binding-root", path: "bindingRoot", message: "release binding root does not match its content" });
482
+ }
483
+ try {
484
+ const expected = createKfdAdopterReleaseBinding({
485
+ manifest,
486
+ manifestGate,
487
+ legacyProjection,
488
+ manifestPath: binding.authority?.manifestPath,
489
+ gatePath: binding.authority?.gatePath,
490
+ legacyProjectionPath: binding.legacyProjection?.path,
491
+ expectedSourceSha,
492
+ });
493
+ if (stableJson(binding) !== stableJson(expected)) {
494
+ issues.push({ code: "adopter-release-binding-drift", path: "", message: "release binding differs from the exact manifest, gate, or legacy projection closure" });
495
+ }
496
+ } catch (error) {
497
+ issues.push({ code: "adopter-release-binding-authority", path: "", message: error.message });
498
+ }
499
+ return { valid: issues.length === 0, issues };
500
+ }
@@ -0,0 +1,460 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import crypto from "node:crypto";
4
+ import {
5
+ createReleaseTrain,
6
+ observeReleaseTrain,
7
+ validateReleaseTrain,
8
+ } from "./release-train.js";
9
+
10
+ export const RELEASE_TRAIN_HOLD_SCHEMA =
11
+ "kungfu-buildchain-release-train-hold/v1";
12
+
13
+ function canonical(value) {
14
+ if (Array.isArray(value)) return value.map(canonical);
15
+ if (value && typeof value === "object") {
16
+ return Object.fromEntries(
17
+ Object.entries(value)
18
+ .sort(([left], [right]) => left.localeCompare(right))
19
+ .map(([key, item]) => [key, canonical(item)]),
20
+ );
21
+ }
22
+ return value;
23
+ }
24
+
25
+ function evidenceRoot(value) {
26
+ return `sha256:${crypto
27
+ .createHash("sha256")
28
+ .update(JSON.stringify(canonical(value)))
29
+ .digest("hex")}`;
30
+ }
31
+
32
+ function sameHold(left, right) {
33
+ return (
34
+ left?.schema === RELEASE_TRAIN_HOLD_SCHEMA &&
35
+ right?.schema === RELEASE_TRAIN_HOLD_SCHEMA &&
36
+ left.trainRoot === right.trainRoot &&
37
+ left.code === right.code &&
38
+ left.observedDevSha === right.observedDevSha &&
39
+ left.observedCandidateSha === right.observedCandidateSha &&
40
+ left.observedCandidateTreeSha === right.observedCandidateTreeSha &&
41
+ left.observedAlphaBaseSha === right.observedAlphaBaseSha &&
42
+ left.observedRuntimeSha === right.observedRuntimeSha
43
+ );
44
+ }
45
+
46
+ function releaseTrainHold({
47
+ train,
48
+ code,
49
+ reason,
50
+ observedDevSha,
51
+ observedCandidateSha,
52
+ observedCandidateTreeSha,
53
+ observedAlphaBaseSha,
54
+ observedRuntimeSha,
55
+ recordedAt,
56
+ priorHold,
57
+ }) {
58
+ const body = {
59
+ schema: RELEASE_TRAIN_HOLD_SCHEMA,
60
+ trainRoot: train.trainRoot,
61
+ cutRoot: train.releaseCut.cutRoot,
62
+ generation: train.releaseCut.generation,
63
+ candidateSha: train.releaseCut.candidateSha,
64
+ candidateTreeSha: train.releaseCut.candidateTreeSha,
65
+ code,
66
+ reason,
67
+ observedDevSha,
68
+ observedCandidateSha,
69
+ observedCandidateTreeSha,
70
+ observedAlphaBaseSha,
71
+ observedRuntimeSha,
72
+ recordedAt,
73
+ };
74
+ if (sameHold(priorHold, body)) return priorHold;
75
+ return { ...body, holdRoot: evidenceRoot(body) };
76
+ }
77
+
78
+ export function observeDevOnce(train, observedDevSha, observedAt) {
79
+ if (
80
+ train.observations.some(
81
+ (observation) => observation.observedDevSha === observedDevSha,
82
+ )
83
+ ) {
84
+ return train;
85
+ }
86
+ return observeReleaseTrain(train, { observedDevSha, observedAt });
87
+ }
88
+
89
+ export async function resolveManagedCandidate({
90
+ client,
91
+ targetBranch,
92
+ repository,
93
+ sourceBranch,
94
+ parseCandidate,
95
+ }) {
96
+ const openPullRequests = await client.listOpenPullRequests(targetBranch);
97
+ const managed = openPullRequests
98
+ .map((pullRequest) => parseCandidate(pullRequest, targetBranch))
99
+ .filter(Boolean);
100
+ for (const candidate of managed) {
101
+ if (
102
+ candidate.state &&
103
+ (candidate.state.repository !== repository ||
104
+ candidate.state.sourceBranch !== sourceBranch)
105
+ ) {
106
+ throw new Error(
107
+ `candidate PR #${candidate.number} state does not bind ${repository} ${sourceBranch}`,
108
+ );
109
+ }
110
+ }
111
+ if (managed.length > 1) {
112
+ throw new Error(
113
+ `multiple open Buildchain candidate PRs target ${targetBranch}: ${managed
114
+ .map((candidate) => `#${candidate.number}`)
115
+ .join(", ")}`,
116
+ );
117
+ }
118
+ return managed[0] || null;
119
+ }
120
+
121
+ export async function cutInitialReleaseTrain({
122
+ options,
123
+ client,
124
+ decision,
125
+ candidate,
126
+ sourceSha,
127
+ targetSha,
128
+ observedSourceHeadSha,
129
+ }) {
130
+ if (!candidate) return null;
131
+ if (!options.buildchainRuntimeSha) {
132
+ throw new Error(
133
+ "buildchainRuntimeSha is required to cut an active Release Train",
134
+ );
135
+ }
136
+ if (!client.resolveCommitTreeSha) {
137
+ throw new Error(
138
+ "active Release Train creation requires candidate tree readback",
139
+ );
140
+ }
141
+ const candidateTreeSha = await client.resolveCommitTreeSha(sourceSha);
142
+ let train = createReleaseTrain({
143
+ repository: options.repository,
144
+ sourceBranch: options.sourceBranch,
145
+ targetBranch: options.targetBranch,
146
+ originDevSha: sourceSha,
147
+ candidateSha: sourceSha,
148
+ candidateTreeSha,
149
+ alphaBaseSha: targetSha,
150
+ buildchainRuntimeSha: options.buildchainRuntimeSha,
151
+ generation: 1,
152
+ authorityRoots: [decision.decisionRoot, candidate.qualificationRoot].sort(),
153
+ createdAt: options.cutCreatedAt || options.now,
154
+ });
155
+ train = observeDevOnce(train, observedSourceHeadSha, options.now);
156
+ if (
157
+ options.expectedCutRoot &&
158
+ train.releaseCut.cutRoot !== options.expectedCutRoot
159
+ ) {
160
+ throw new Error(
161
+ `Release Cut changed between observation and settlement: expected ${options.expectedCutRoot}, observed ${train.releaseCut.cutRoot}`,
162
+ );
163
+ }
164
+ return train;
165
+ }
166
+
167
+ export function reconcileActiveReleaseTrain({
168
+ releaseTrain,
169
+ repository,
170
+ sourceBranch,
171
+ targetBranch,
172
+ activeCandidateSha,
173
+ sourceLockSha,
174
+ candidateTreeSha,
175
+ alphaBaseSha,
176
+ buildchainRuntimeSha,
177
+ observedDevSha,
178
+ observedAt,
179
+ priorHold = null,
180
+ }) {
181
+ const train = observeDevOnce(
182
+ validateReleaseTrain(releaseTrain),
183
+ observedDevSha,
184
+ observedAt,
185
+ );
186
+ if (train.state.status === "superseded") {
187
+ return { status: "superseded", train, hold: null };
188
+ }
189
+ const cut = train.releaseCut;
190
+ const checks = [
191
+ [
192
+ cut.repository !== repository ||
193
+ cut.sourceBranch !== sourceBranch ||
194
+ cut.targetBranch !== targetBranch,
195
+ "invalid-authority",
196
+ "the active Release Cut route no longer matches the controller route",
197
+ ],
198
+ [
199
+ activeCandidateSha !== cut.candidateSha ||
200
+ sourceLockSha !== cut.candidateSha,
201
+ "candidate-ref-drift",
202
+ "the managed candidate PR or source-lock ref drifted from the frozen candidate",
203
+ ],
204
+ [
205
+ candidateTreeSha !== cut.candidateTreeSha,
206
+ "candidate-tree-drift",
207
+ "the frozen candidate tree readback does not match the Release Cut",
208
+ ],
209
+ [
210
+ alphaBaseSha !== cut.alphaBaseSha,
211
+ "alpha-base-drift",
212
+ "the Alpha base moved after the Release Cut and requires explicit supersession or repair",
213
+ ],
214
+ [
215
+ buildchainRuntimeSha !== cut.buildchainRuntimeSha,
216
+ "runtime-drift",
217
+ "the Buildchain runtime moved after the Release Cut and requires explicit supersession or repair",
218
+ ],
219
+ ];
220
+ const failure = checks.find(([failed]) => failed);
221
+ if (failure) {
222
+ const [, code, reason] = failure;
223
+ return {
224
+ status: "held",
225
+ train,
226
+ hold: releaseTrainHold({
227
+ train,
228
+ code,
229
+ reason,
230
+ observedDevSha,
231
+ observedCandidateSha: sourceLockSha,
232
+ observedCandidateTreeSha: candidateTreeSha,
233
+ observedAlphaBaseSha: alphaBaseSha,
234
+ observedRuntimeSha: buildchainRuntimeSha,
235
+ recordedAt: observedAt,
236
+ priorHold,
237
+ }),
238
+ };
239
+ }
240
+ return {
241
+ status: "active",
242
+ train,
243
+ hold: null,
244
+ drift: {
245
+ observedDevSha,
246
+ originDevSha: cut.originDevSha,
247
+ moved: observedDevSha !== cut.originDevSha,
248
+ observationRoot:
249
+ train.observations.find(
250
+ (observation) => observation.observedDevSha === observedDevSha,
251
+ )?.observationRoot || null,
252
+ },
253
+ };
254
+ }
255
+
256
+ function activeTrainDecision({
257
+ options,
258
+ candidate,
259
+ reconciliation,
260
+ observedSourceHeadSha,
261
+ targetSha,
262
+ }) {
263
+ const cut = reconciliation.train.releaseCut;
264
+ const eligible = reconciliation.status === "active";
265
+ const body = {
266
+ schema: "kungfu-buildchain-channel-candidate-decision/v1",
267
+ eligible,
268
+ reason: eligible
269
+ ? "active-release-train"
270
+ : reconciliation.status === "superseded"
271
+ ? "active-release-train-superseded"
272
+ : "active-release-train-held",
273
+ repository: options.repository,
274
+ source: { branch: options.sourceBranch, sha: cut.candidateSha },
275
+ target: { branch: options.targetBranch, sha: targetSha },
276
+ comparison: { status: "frozen-release-cut", aheadBy: 0 },
277
+ selection: {
278
+ mode: "active-release-train",
279
+ observedSourceHeadSha,
280
+ skippedNewerCommitCount: 0,
281
+ trainRoot: reconciliation.train.trainRoot,
282
+ cutRoot: cut.cutRoot,
283
+ generation: cut.generation,
284
+ candidateTreeSha: cut.candidateTreeSha,
285
+ },
286
+ workflowEvidence: [],
287
+ requiredWorkflowPaths: [],
288
+ sourceLockRef: candidate.sourceLockRef,
289
+ ...(reconciliation.hold
290
+ ? {
291
+ blockReason: reconciliation.hold.reason,
292
+ holdRoot: reconciliation.hold.holdRoot,
293
+ }
294
+ : {}),
295
+ decidedAt: options.now,
296
+ };
297
+ return { ...body, decisionRoot: evidenceRoot(body) };
298
+ }
299
+
300
+ function candidateIdentity(candidate) {
301
+ return {
302
+ sourceSha: candidate.sourceSha,
303
+ sourceLockRef: candidate.sourceLockRef,
304
+ decisionRoot: candidate.decisionRoot || null,
305
+ pullRequestNumber: candidate.number,
306
+ pullRequestUrl: candidate.url,
307
+ };
308
+ }
309
+
310
+ function semanticState(value) {
311
+ if (!value) return null;
312
+ const normalized = { ...value };
313
+ delete normalized.observedAt;
314
+ delete normalized.observationDecisionRoot;
315
+ delete normalized.stateRoot;
316
+ delete normalized.priorStateRoot;
317
+ return normalized;
318
+ }
319
+
320
+ export async function runActiveReleaseTrain({
321
+ options,
322
+ client,
323
+ activeCandidate,
324
+ adapters,
325
+ }) {
326
+ const [observedSourceHeadSha, targetSha, sourceLockSha] = await Promise.all([
327
+ client.resolveBranch(options.sourceBranch),
328
+ client.resolveBranch(options.targetBranch),
329
+ client.resolveBranch(activeCandidate.sourceLockRef),
330
+ ]);
331
+ if (!client.resolveCommitTreeSha) {
332
+ throw new Error(
333
+ "active Release Train validation requires candidate tree readback",
334
+ );
335
+ }
336
+ const candidateTreeSha = await client.resolveCommitTreeSha(
337
+ activeCandidate.sourceSha,
338
+ );
339
+ const reconciliation = reconcileActiveReleaseTrain({
340
+ releaseTrain: activeCandidate.releaseTrain,
341
+ repository: options.repository,
342
+ sourceBranch: options.sourceBranch,
343
+ targetBranch: options.targetBranch,
344
+ activeCandidateSha: activeCandidate.sourceSha,
345
+ sourceLockSha,
346
+ candidateTreeSha,
347
+ alphaBaseSha: targetSha,
348
+ buildchainRuntimeSha: options.buildchainRuntimeSha,
349
+ observedDevSha: observedSourceHeadSha,
350
+ observedAt: options.now,
351
+ priorHold: activeCandidate.hold,
352
+ });
353
+ if (
354
+ options.expectedCutRoot &&
355
+ reconciliation.train.releaseCut.cutRoot !== options.expectedCutRoot
356
+ ) {
357
+ throw new Error(
358
+ `active Release Cut changed between observation and settlement: expected ${options.expectedCutRoot}, observed ${reconciliation.train.releaseCut.cutRoot}`,
359
+ );
360
+ }
361
+ const decision = activeTrainDecision({
362
+ options,
363
+ candidate: activeCandidate,
364
+ reconciliation,
365
+ observedSourceHeadSha,
366
+ targetSha,
367
+ });
368
+ if (
369
+ options.expectedSelectedSha &&
370
+ decision.source.sha !== options.expectedSelectedSha
371
+ ) {
372
+ throw new Error(
373
+ `selected source changed between observation and settlement: expected ${options.expectedSelectedSha}, observed ${decision.source.sha}`,
374
+ );
375
+ }
376
+ const priorStateRoot = adapters.persistedStateRoot(activeCandidate);
377
+ if (
378
+ options.expectedPriorStateRoot &&
379
+ options.expectedPriorStateRoot !== priorStateRoot
380
+ ) {
381
+ throw new Error(
382
+ `candidate controller compare-and-swap failed: expected prior state root ${options.expectedPriorStateRoot}, observed ${priorStateRoot}`,
383
+ );
384
+ }
385
+ const desiredState = adapters.candidateStateBody({
386
+ options,
387
+ targetSha,
388
+ decision,
389
+ activeCandidate: candidateIdentity(activeCandidate),
390
+ nextCandidate: null,
391
+ supersededCandidate: null,
392
+ priorStateRoot,
393
+ generation: reconciliation.train.releaseCut.generation,
394
+ tombstones: activeCandidate.state?.tombstones || [],
395
+ releaseTrain: reconciliation.train,
396
+ hold: reconciliation.hold,
397
+ });
398
+ const transitionNeeded =
399
+ !activeCandidate.state ||
400
+ JSON.stringify(canonical(semanticState(activeCandidate.state))) !==
401
+ JSON.stringify(canonical(semanticState(desiredState)));
402
+ const state = transitionNeeded ? desiredState : activeCandidate.state;
403
+ let settlementAction = "none";
404
+ if (transitionNeeded && options.settlementAuthorized && !options.dryRun) {
405
+ const nextBody = adapters.replaceCandidateStateMarker(
406
+ activeCandidate.body,
407
+ state,
408
+ );
409
+ await client.updatePullRequestBody(
410
+ activeCandidate.number,
411
+ nextBody,
412
+ priorStateRoot,
413
+ );
414
+ settlementAction =
415
+ reconciliation.status === "held"
416
+ ? "hold-active-release-train"
417
+ : reconciliation.status === "superseded"
418
+ ? "record-superseded-release-train"
419
+ : "observe-active-release-train";
420
+ }
421
+ const pullRequest = adapters.activePullRequest(activeCandidate);
422
+ const autoMerge = await adapters.armCandidateAutoMerge(
423
+ options,
424
+ reconciliation.status,
425
+ pullRequest,
426
+ client,
427
+ );
428
+ return {
429
+ schema: "kungfu-buildchain-dev-alpha-candidate-patrol/v1",
430
+ dryRun: options.dryRun,
431
+ createPullRequest: options.createPullRequest,
432
+ settlementAuthorized: options.settlementAuthorized,
433
+ autoMerge,
434
+ decision,
435
+ releaseTrain: reconciliation.train,
436
+ drift: reconciliation.drift || null,
437
+ hold: reconciliation.hold,
438
+ controller: {
439
+ schema: adapters.stateSchema,
440
+ state: reconciliation.status,
441
+ activeCandidate,
442
+ nextCandidate: null,
443
+ supersededCandidate: null,
444
+ settlementAction,
445
+ stateRoot: state.stateRoot,
446
+ priorStateRoot,
447
+ generation: reconciliation.train.releaseCut.generation,
448
+ trainRoot: reconciliation.train.trainRoot,
449
+ cutRoot: reconciliation.train.releaseCut.cutRoot,
450
+ candidateTreeSha: reconciliation.train.releaseCut.candidateTreeSha,
451
+ buildchainRuntimeSha:
452
+ reconciliation.train.releaseCut.buildchainRuntimeSha,
453
+ cutCreatedAt: reconciliation.train.releaseCut.createdAt,
454
+ observedAt: options.now,
455
+ holdRoot: reconciliation.hold?.holdRoot || null,
456
+ tombstones: state.tombstones || [],
457
+ },
458
+ pullRequest,
459
+ };
460
+ }