@kungfu-tech/buildchain 3.0.2-alpha.2 → 3.0.2-alpha.4

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.
@@ -0,0 +1,64 @@
1
+ ---
2
+ status: preview
3
+ period: ongoing
4
+ theme: dev-alpha-candidate-patrol
5
+ doc_type: architecture-and-usage
6
+ source_level: local-files
7
+ confidence: high
8
+ sensitivity: public
9
+ evidence_grade: A
10
+ review_state: self-reviewed
11
+ last_reviewed: 2026-07-26
12
+ ai_provenance:
13
+ model_family: GPT-5
14
+ product: Codex
15
+ generated_at: 2026-07-26
16
+ visible_context: Existing Buildchain stable-candidate source locks, Kungfu exact-source Alpha preflight, Dev Patrol, and repository release governance.
17
+ invisible_context_boundary: No credentials, private logs, or private configuration were used.
18
+ ---
19
+
20
+ # Dev to Alpha Candidate Patrol
21
+
22
+ Buildchain provides a reusable observation and PR controller for repositories
23
+ that promote a development branch into a protected Alpha branch. It does not
24
+ publish Alpha. The controller reads the exact heads of both branches, walks the
25
+ bounded development history from newest to oldest (stopping early at the Alpha
26
+ head), and selects the newest commit that satisfies all of these conditions:
27
+
28
+ - the source is strictly ahead of the recorded target head;
29
+ - the latest completed Dev Patrol for that exact commit SHA succeeded;
30
+ - the latest completed Alpha preflight for the same commit SHA succeeded; and
31
+ - both runs are within the caller's evidence age limit.
32
+
33
+ The selected commit can be behind the observed development head when newer
34
+ commits have not completed both workflows yet. The decision binds the observed
35
+ head, selected SHA, and count of skipped newer commits. This makes a slow native
36
+ verification lane live under continuous development without silently treating
37
+ an unqualified head as releasable.
38
+
39
+ History discovery is bounded to the newest 1000 development commits. The
40
+ controller then compares the selected SHA to the exact Alpha head before it can
41
+ be eligible, so a bounded scan cannot turn a commit outside the promotion
42
+ ancestry into a candidate.
43
+
44
+ The decision is `kungfu-buildchain-channel-candidate-decision/v1`. It records the
45
+ source and target branches and SHAs, comparison distance, workflow paths, run
46
+ identities and attempts, completion times, URLs, policy, and a canonical decision
47
+ root. Missing, stale, failed, duplicate, or source-mismatched evidence fails
48
+ closed.
49
+
50
+ ## Reusable workflow
51
+
52
+ Call `.github/workflows/dev-alpha-candidate-patrol.yml` from a thin repository
53
+ workflow. Start with `dry-run: true`. Once the repository has proven that its
54
+ two workflow names and branch topology produce exact same-SHA evidence, it may
55
+ set `create-pull-request: true` and `dry-run: false`.
56
+
57
+ Candidate mode creates one branch named from the target branch and the first 12
58
+ characters of the full source SHA. An existing branch must point to the same
59
+ full SHA or the run fails. The controller then creates or reuses one open pull
60
+ request from that immutable branch to the protected Alpha branch.
61
+
62
+ The workflow never moves the Alpha ref directly, merges or auto-merges the pull
63
+ request, publishes npm, creates a Git tag or GitHub Release, or changes branch
64
+ protection. Those remain repository-owned protected settlement actions.
@@ -94,9 +94,10 @@ jobs:
94
94
 
95
95
  | Preset | Platforms |
96
96
  | ----------------------- | ------------------------------------------------------------------------ |
97
- | `github-hosted` | `ubuntu-24.04`, `macos-latest`, `windows-2022` |
98
- | `kungfu-v4-self-hosted` | Kungfu Linux x64, macOS ARM64, and Windows x64 self-hosted runner labels |
99
- | `custom` | Requires `platforms-json` |
97
+ | `github-hosted` | `ubuntu-24.04`, `macos-latest`, `windows-2022` |
98
+ | `kungfu-v4-self-hosted` | Kungfu Linux x64, macOS ARM64, and Windows x64 self-hosted runner labels |
99
+ | `kungfu-v4-native` | Kungfu Linux x64, Linux ARM64, macOS ARM64, and Windows x64; Linux ARM64 uses GitHub-hosted `ubuntu-24.04-arm` |
100
+ | `custom` | Requires `platforms-json` |
100
101
 
101
102
  Callers can still provide a custom matrix with `platforms-json`. Each platform
102
103
  object has:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "3.0.2-alpha.2",
3
+ "version": "3.0.2-alpha.4",
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",
@@ -20,6 +20,7 @@
20
20
  "./anchored-version-material": "./packages/core/anchored-version-material.js",
21
21
  "./buildchain-contract": "./packages/core/buildchain-contract.js",
22
22
  "./candidate-timeline": "./packages/core/candidate-timeline.js",
23
+ "./channel-candidate": "./packages/core/channel-candidate.js",
23
24
  "./cache-evidence": "./packages/core/cache-evidence.js",
24
25
  "./controller-evidence": "./packages/core/controller-evidence.js",
25
26
  "./diagnostics": "./packages/core/diagnostics.js",
@@ -27,6 +27,7 @@ const DESCRIPTORS = Object.freeze([
27
27
  [".github/workflows/buildchain-patrol.yml", "governance-write"],
28
28
  [".github/workflows/buildchain-ref-promotion.yml", "governance-write"],
29
29
  [".github/workflows/buildchain-stable-candidate-patrol.yml", "governance-write"],
30
+ [".github/workflows/dev-alpha-candidate-patrol.yml", "governance-write"],
30
31
  [".github/workflows/dev-merge-queue-governance.yml", "governance-write"],
31
32
  [".github/workflows/dev-pr-auto-merge.yml", "governance-write"],
32
33
  [".github/workflows/github-governance-audit.yml", "governance-write"],
@@ -0,0 +1,186 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+
3
+ import crypto from "node:crypto";
4
+
5
+ export const CHANNEL_CANDIDATE_DECISION_SCHEMA =
6
+ "kungfu-buildchain-channel-candidate-decision/v1";
7
+
8
+ const SHA = /^[0-9a-f]{40}$/;
9
+
10
+ function canonical(value) {
11
+ if (Array.isArray(value)) return value.map(canonical);
12
+ if (value && typeof value === "object") {
13
+ return Object.fromEntries(
14
+ Object.entries(value)
15
+ .sort(([left], [right]) => left.localeCompare(right))
16
+ .map(([key, item]) => [key, canonical(item)]),
17
+ );
18
+ }
19
+ return value;
20
+ }
21
+
22
+ function root(value) {
23
+ return `sha256:${crypto
24
+ .createHash("sha256")
25
+ .update(JSON.stringify(canonical(value)))
26
+ .digest("hex")}`;
27
+ }
28
+
29
+ function required(value, name) {
30
+ const normalized = String(value || "").trim();
31
+ if (!normalized) throw new Error(`${name} is required`);
32
+ return normalized;
33
+ }
34
+
35
+ function sha(value, name) {
36
+ const normalized = required(value, name).toLowerCase();
37
+ if (!SHA.test(normalized))
38
+ throw new Error(`${name} must be an exact 40-character SHA`);
39
+ return normalized;
40
+ }
41
+
42
+ function qualifyWorkflow(row, { sourceSha, now, maxAgeSeconds }) {
43
+ if (!row || typeof row !== "object")
44
+ throw new Error("workflow evidence is required");
45
+ const workflowPath = required(row.workflowPath, "workflowPath");
46
+ if (sha(row.headSha, `${workflowPath} headSha`) !== sourceSha) {
47
+ throw new Error(
48
+ `${workflowPath} evidence does not bind source SHA ${sourceSha}`,
49
+ );
50
+ }
51
+ if (row.status !== "completed" || row.conclusion !== "success") {
52
+ throw new Error(`${workflowPath} is not a completed successful run`);
53
+ }
54
+ const completedAt = required(row.completedAt, `${workflowPath} completedAt`);
55
+ const ageSeconds = (Date.parse(now) - Date.parse(completedAt)) / 1000;
56
+ if (
57
+ !Number.isFinite(ageSeconds) ||
58
+ ageSeconds < 0 ||
59
+ ageSeconds > maxAgeSeconds
60
+ ) {
61
+ throw new Error(
62
+ `${workflowPath} evidence is stale or has an invalid completion time`,
63
+ );
64
+ }
65
+ const runId = Number(row.runId);
66
+ const runAttempt = Number(row.runAttempt || 1);
67
+ if (!Number.isSafeInteger(runId) || runId <= 0)
68
+ throw new Error(`${workflowPath} runId is invalid`);
69
+ if (!Number.isSafeInteger(runAttempt) || runAttempt <= 0)
70
+ throw new Error(`${workflowPath} runAttempt is invalid`);
71
+ return {
72
+ workflowPath,
73
+ workflowName: required(row.workflowName, `${workflowPath} workflowName`),
74
+ runId,
75
+ runAttempt,
76
+ headSha: sourceSha,
77
+ status: "completed",
78
+ conclusion: "success",
79
+ completedAt,
80
+ url: required(row.url, `${workflowPath} url`),
81
+ };
82
+ }
83
+
84
+ export function channelCandidateSourceLockRef(targetBranch, sourceSha) {
85
+ const target = required(targetBranch, "targetBranch");
86
+ const exactSha = sha(sourceSha, "sourceSha");
87
+ return `buildchain/candidate/${target.replace(/[^A-Za-z0-9._-]+/g, "-")}/${exactSha.slice(0, 12)}`;
88
+ }
89
+
90
+ export function decideChannelCandidate(input) {
91
+ const repository = required(input.repository, "repository");
92
+ const sourceBranch = required(input.sourceBranch, "sourceBranch");
93
+ const targetBranch = required(input.targetBranch, "targetBranch");
94
+ if (sourceBranch === targetBranch)
95
+ throw new Error("sourceBranch and targetBranch must differ");
96
+ const sourceSha = sha(input.sourceSha, "sourceSha");
97
+ const targetSha = sha(input.targetSha, "targetSha");
98
+ const selection = input.selection
99
+ ? {
100
+ mode: required(input.selection.mode, "selection.mode"),
101
+ observedSourceHeadSha: sha(
102
+ input.selection.observedSourceHeadSha,
103
+ "selection.observedSourceHeadSha",
104
+ ),
105
+ skippedNewerCommitCount: Number(
106
+ input.selection.skippedNewerCommitCount || 0,
107
+ ),
108
+ }
109
+ : undefined;
110
+ if (
111
+ selection &&
112
+ (!Number.isSafeInteger(selection.skippedNewerCommitCount) ||
113
+ selection.skippedNewerCommitCount < 0)
114
+ ) {
115
+ throw new Error(
116
+ "selection.skippedNewerCommitCount must be a non-negative integer",
117
+ );
118
+ }
119
+ const now = required(input.now || new Date().toISOString(), "now");
120
+ const maxAgeSeconds = Number(input.maxAgeSeconds ?? 7 * 24 * 60 * 60);
121
+ if (!Number.isSafeInteger(maxAgeSeconds) || maxAgeSeconds <= 0) {
122
+ throw new Error("maxAgeSeconds must be a positive integer");
123
+ }
124
+ const comparison = input.comparison || {};
125
+ if (comparison.status !== "ahead" || Number(comparison.aheadBy) <= 0) {
126
+ const reason =
127
+ comparison.status === "identical"
128
+ ? "target-already-current"
129
+ : "source-does-not-lead-target";
130
+ return {
131
+ schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
132
+ eligible: false,
133
+ reason,
134
+ repository,
135
+ source: { branch: sourceBranch, sha: sourceSha },
136
+ target: { branch: targetBranch, sha: targetSha },
137
+ comparison: {
138
+ status: String(comparison.status || "unknown"),
139
+ aheadBy: Number(comparison.aheadBy || 0),
140
+ },
141
+ decidedAt: now,
142
+ };
143
+ }
144
+ const rows = Array.isArray(input.workflowEvidence)
145
+ ? input.workflowEvidence
146
+ : [];
147
+ const expectedPaths = [
148
+ ...new Set((input.requiredWorkflowPaths || []).map(String)),
149
+ ];
150
+ if (expectedPaths.length === 0)
151
+ throw new Error("requiredWorkflowPaths must not be empty");
152
+ if (rows.length !== expectedPaths.length) {
153
+ throw new Error(
154
+ `expected exactly ${expectedPaths.length} workflow evidence rows, got ${rows.length}`,
155
+ );
156
+ }
157
+ const byPath = new Map();
158
+ for (const row of rows) {
159
+ const qualified = qualifyWorkflow(row, { sourceSha, now, maxAgeSeconds });
160
+ if (byPath.has(qualified.workflowPath)) {
161
+ throw new Error(`duplicate workflow evidence: ${qualified.workflowPath}`);
162
+ }
163
+ byPath.set(qualified.workflowPath, qualified);
164
+ }
165
+ for (const workflowPath of expectedPaths) {
166
+ if (!byPath.has(workflowPath))
167
+ throw new Error(`missing workflow evidence: ${workflowPath}`);
168
+ }
169
+ const body = {
170
+ schema: CHANNEL_CANDIDATE_DECISION_SCHEMA,
171
+ eligible: true,
172
+ reason: "same-source-qualified",
173
+ repository,
174
+ source: { branch: sourceBranch, sha: sourceSha },
175
+ target: { branch: targetBranch, sha: targetSha },
176
+ comparison: { status: "ahead", aheadBy: Number(comparison.aheadBy) },
177
+ ...(selection ? { selection } : {}),
178
+ sourceLockRef: channelCandidateSourceLockRef(targetBranch, sourceSha),
179
+ workflowEvidence: expectedPaths.map((workflowPath) =>
180
+ byPath.get(workflowPath),
181
+ ),
182
+ policy: { maxAgeSeconds, requiredWorkflowPaths: expectedPaths },
183
+ decidedAt: now,
184
+ };
185
+ return { ...body, decisionRoot: root(body) };
186
+ }
@@ -100,6 +100,12 @@ export {
100
100
  normalizeCandidateTimelineEvent,
101
101
  } from "./candidate-timeline.js";
102
102
 
103
+ export {
104
+ CHANNEL_CANDIDATE_DECISION_SCHEMA,
105
+ channelCandidateSourceLockRef,
106
+ decideChannelCandidate,
107
+ } from "./channel-candidate.js";
108
+
103
109
  export {
104
110
  BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT,
105
111
  BUILDCHAIN_DIAGNOSTICS_CONTRACT,
@@ -31,6 +31,36 @@ export const RUNNER_PRESETS = Object.freeze({
31
31
  capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
32
32
  },
33
33
  ],
34
+ "kungfu-v4-native": [
35
+ {
36
+ id: "linux-x64",
37
+ name: "Linux x64",
38
+ platform: "linux",
39
+ runner: '["self-hosted","Linux","X64","kungfu-build-v4-linux-x64"]',
40
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
41
+ },
42
+ {
43
+ id: "linux-arm64",
44
+ name: "Linux ARM64",
45
+ platform: "linux",
46
+ runner: '["ubuntu-24.04-arm"]',
47
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
48
+ },
49
+ {
50
+ id: "macos-arm64",
51
+ name: "macOS ARM64",
52
+ platform: "macos",
53
+ runner: '["self-hosted","macOS","ARM64","kungfu-build-v4-macos-arm64"]',
54
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
55
+ },
56
+ {
57
+ id: "windows-x64",
58
+ name: "Windows x64",
59
+ platform: "windows",
60
+ runner: '["self-hosted","Windows","X64","kungfu-build-v4-windows-x64"]',
61
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
62
+ },
63
+ ],
34
64
  });
35
65
 
36
66
  export const LINUX_CONTAINER_PRESETS = Object.freeze({
@@ -46,6 +76,7 @@ const RUNNER_PRESET_ALIASES = Object.freeze({
46
76
  kungfu: "kungfu-v4-self-hosted",
47
77
  "kungfu-self-hosted": "kungfu-v4-self-hosted",
48
78
  "kungfu-v4": "kungfu-v4-self-hosted",
79
+ "kungfu-v4-four-platform": "kungfu-v4-native",
49
80
  });
50
81
 
51
82
  const LINUX_CONTAINER_PRESET_ALIASES = Object.freeze({