@kungfu-tech/buildchain 2.7.1-alpha.0 → 2.8.0-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.
@@ -209,6 +209,11 @@ After a publish transaction reaches `complete`, the action generates the unified
209
209
  persists those files under `release-passport/` in the durable release-state ref.
210
210
  Set `release-passport-product-name` to record the consumer product name, for
211
211
  example `Libnode`, instead of the default `Buildchain`.
212
+ Set `release-passport-kfd-1-witness-jsons` to newline-separated KFD-1
213
+ contract-world witness JSON paths when released artifacts must prove
214
+ byte-for-byte KFD contract surfaces. Buildchain imports the KFD metadata from
215
+ `@kungfu-tech/kfd`, freezes the witness, verifies artifact bytes, and writes the
216
+ result under the KFD-provided `kfd-1` passport section.
212
217
  When present, the passport includes the aggregate build summary, platform
213
218
  artifact manifests, npm publish evidence, dist-tag promotion evidence, the
214
219
  release-state ref, trusted publishing metadata, and the Buildchain transaction
@@ -76,6 +76,7 @@ function usage() {
76
76
  [--build-summary-json <json-or-path>]
77
77
  [--platform-manifest-json <json-or-path>]...
78
78
  [--dist-tag-evidence-json <json-or-path>]
79
+ [--kfd-1-witness-json <json-or-path>]...
79
80
  [--release-extra-json <json-or-path>]
80
81
  [--publish-json <json-or-path>] [--output-dir <dir>] [--json]
81
82
  buildchain verify release-passport <file-or-url> [--json]
@@ -734,6 +735,7 @@ async function main(argv = process.argv.slice(2)) {
734
735
  buildSummaryJson: readFlag(collectArgs, "build-summary-json", ""),
735
736
  platformManifestJsons: readRepeatedFlag(collectArgs, "platform-manifest-json"),
736
737
  distTagEvidenceJson: readFlag(collectArgs, "dist-tag-evidence-json", ""),
738
+ kfd1WitnessJsons: readRepeatedFlag(collectArgs, "kfd-1-witness-json"),
737
739
  releaseJsonExtra: readFlag(collectArgs, "release-extra-json", ""),
738
740
  publishJson: readFlag(collectArgs, "publish-json", ""),
739
741
  workflow,
@@ -14,6 +14,7 @@
14
14
  "./diagnostics": "./packages/core/diagnostics.js",
15
15
  "./issue-reporting": "./packages/core/issue-reporting.js",
16
16
  "./logging": "./packages/core/logging.js",
17
+ "./kfd-gate": "./packages/core/kfd-gate.js",
17
18
  "./release-candidate": "./packages/core/release-candidate.js",
18
19
  "./release-passport": "./packages/core/release-passport.js",
19
20
  "./release-propagation": "./packages/core/release-propagation.js",
package/docs/MAP.md CHANGED
@@ -33,6 +33,7 @@ running artifact), *use* (consume / extend) - and a **status**:
33
33
  | How do I declare version files and custom lifecycle commands? | [`lifecycle-protocol.md`](lifecycle-protocol.md) | use | stable |
34
34
  | How does publish evidence, recovery, and finalization work? | [`publish-transaction.md`](publish-transaction.md) | verify | stable |
35
35
  | How do I publish or verify release passport artifacts? | [`release-passport.md`](release-passport.md) | use | stable |
36
+ | How do I gate release artifacts with KFD-1 contract-world witnesses? | [`release-passport.md`](release-passport.md#kfd-1-contract-world-release-gate) | verify/use | stable |
36
37
  | How do I propagate finalized upstream releases to downstream package/site PRs? | [`release-propagation.md`](release-propagation.md) | use | preview |
37
38
  | How do I prove a PR-stage reusable build is the artifact source promoted later? | [`release-candidate.md`](release-candidate.md) + [`reusable-build-surface.md`](reusable-build-surface.md) | verify | stable |
38
39
  | Why are binary release assets archived by platform, and where is the single bundle? | [`binary-distribution.md`](binary-distribution.md) | verify | stable |
@@ -90,6 +91,8 @@ running artifact), *use* (consume / extend) - and a **status**:
90
91
  [`cli.md`](cli.md) and [`publish-transaction.md`](publish-transaction.md).
91
92
  - **release chains / upstream package as source of truth / site synchronization** ->
92
93
  [`release-propagation.md`](release-propagation.md).
94
+ - **KFD-1 contract worlds / byte-for-byte release gates** ->
95
+ [`release-passport.md`](release-passport.md#kfd-1-contract-world-release-gate).
93
96
  - **GitHub Release passport / binary assets / artifact evidence / agent release checks** ->
94
97
  [`release-passport.md`](release-passport.md),
95
98
  [`binary-distribution.md`](binary-distribution.md), and [`cli.md`](cli.md).
package/docs/cli.md CHANGED
@@ -260,6 +260,7 @@ buildchain collect github-release \
260
260
  --platform-manifest-json .buildchain/artifacts/darwin-arm64/manifest.json \
261
261
  --platform-manifest-json .buildchain/artifacts/win32-x64/manifest.json \
262
262
  --dist-tag-evidence-json .buildchain/release-evidence/v2.3.2/dist-tag-evidence.json \
263
+ --kfd-1-witness-json .buildchain/kfd-1/contract-world.witness.json \
263
264
  --release-extra-json '{"channel":"release","targetRef":"release/v2/v2.3"}' \
264
265
  --output-dir .buildchain/release-passport
265
266
  ```
@@ -276,6 +277,15 @@ platform packages with version, dist-tag, and digest evidence. Verification
276
277
  fails closed if supplied sections are internally incomplete or point to
277
278
  artifacts without matching evidence.
278
279
 
280
+ `--kfd-1-witness-json` attaches a KFD-1 contract-world release gate. The witness
281
+ is structured JSON: consumers declare the contract world, canonical JSON policy,
282
+ artifact paths, and expected SHA-256 digests. Buildchain imports KFD-owned
283
+ metadata from `@kungfu-tech/kfd`, freezes the witness before build publication,
284
+ then verifies the resulting artifact bytes itself and writes the evidence under
285
+ the KFD-provided top-level key currently named `kfd-1`. Consumers should not
286
+ duplicate this by running repository-specific scripts or invoking the Kungfu
287
+ SDK from their release workflow.
288
+
279
289
  `--impact-json` supplies the surface-aware impact ledger. Production release
280
290
  passports (`release/*`) and major publish-gate passports require
281
291
  `surfaceImpacts[]`; alpha, local, and legacy passport contexts keep it
@@ -97,6 +97,7 @@ buildchain collect github-release \
97
97
  --platform-manifest-json .buildchain/artifacts/darwin-arm64/manifest.json \
98
98
  --platform-manifest-json .buildchain/artifacts/win32-x64/manifest.json \
99
99
  --dist-tag-evidence-json .buildchain/release-evidence/v2.2.0/dist-tag-evidence.json \
100
+ --kfd-1-witness-json .buildchain/kfd-1/contract-world.witness.json \
100
101
  --output-dir .buildchain/release-passport
101
102
  ```
102
103
 
@@ -106,6 +107,55 @@ dist-tag, registry, role, platform, and digest, so agents do not need to stitch
106
107
  npm facts back together from the lower-level evidence files.
107
108
  `buildSummary`, `platformArtifactManifests`, and `distTagPromotion` preserve the
108
109
  build and npm dist-tag evidence chain in the same passport.
110
+
111
+ ### KFD-1 contract-world release gate
112
+
113
+ Buildchain can gate release artifacts with KFD-1 contract-world witnesses. This
114
+ is a structured evidence protocol, not a request for consumers to shell out to
115
+ the Kungfu SDK. The authority chain is:
116
+
117
+ 1. KFD owns the standard metadata and schema ids in `@kungfu-tech/kfd`.
118
+ 2. Buildchain imports that metadata, owns the JSON formatting policy, freezes
119
+ the pre-build witness, and independently verifies post-build artifact bytes.
120
+ 3. Consumers only pass declarative witness JSON plus the artifact payloads their
121
+ build already produced.
122
+
123
+ The witness JSON names the contract world, the canonical serialization policy,
124
+ and the release surfaces that must be byte-for-byte verified:
125
+
126
+ ```json
127
+ {
128
+ "id": "kungfu-config",
129
+ "standard": "kfd-1",
130
+ "source": "kfd",
131
+ "contractWorld": {
132
+ "id": "kungfu-config",
133
+ "kind": "schema",
134
+ "name": "Kungfu config schema"
135
+ },
136
+ "canonicalPolicy": {
137
+ "format": "json",
138
+ "encoding": "utf-8",
139
+ "indent": 2,
140
+ "trailingNewline": true
141
+ },
142
+ "surfaces": [
143
+ {
144
+ "id": "kungfu-config-schema",
145
+ "artifactPath": "Contents/Resources/core/config.schema.json",
146
+ "expectedSha256": "..."
147
+ }
148
+ ]
149
+ }
150
+ ```
151
+
152
+ `collect github-release` writes the result under the KFD-provided top-level key
153
+ currently named `kfd-1`. Each contract world records the frozen witness digest,
154
+ the KFD package version, KFD schema ids, the Buildchain formatting policy, and
155
+ the actual artifact digest observed after the build. Verification fails closed
156
+ when the witness is missing required facts, an artifact cannot be found, or a
157
+ post-build digest does not match the frozen witness.
158
+
109
159
  `impact.json` can be supplied with `--impact-json`. Production release
110
160
  passports (`release/*`) and major publish-gate passports require
111
161
  `surfaceImpacts[]`; alpha, local, and legacy passport contexts keep the field
@@ -61,6 +61,7 @@ repository changes are patch".
61
61
 
62
62
  | Date | Action | Line | Faces | Class | Rationale | PR |
63
63
  | --- | --- | --- | --- | --- | --- | --- |
64
+ | 2026-07-06 | open-minor | `v2.8` | kfd-1-contract-world-release-gate, required-check-protection | additive | KFD-1 contract-world release gates add a public witness API, Buildchain-owned passport evidence, and fail-closed byte verification, while protected channel required checks are repaired to bind GitHub Actions check runs instead of legacy commit status contexts. | |
64
65
  | 2026-07-04 | open-minor | `v2.5` | scheduled-integration-governance | additive | Scheduled integration governance adds scheduled feature-branch discovery, conflict-free integration, reporting, and agent-visible governance automation for dev-line maintenance. | |
65
66
  | 2026-07-03 | open-minor | `v2.4` | infra-contract-lifecycle | additive | Infra contract lifecycle adds the provider-neutral `infra-contract` CLI command family, project type, adapter capability contract, lifecycle evidence bundle, propagation evidence, CI evidence mode, and consumer-facing contract artifacts. | |
66
67
  | 2026-07-02 | open-minor | `v2.3` | web-surface-host-mapping | additive | Web surface host mapping adds first-class multi-host surface bindings, reusable workflow URL outputs, per-surface deployment overrides, and an agent-readable fixture contract. | |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.7.1-alpha.0",
3
+ "version": "2.8.0-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",
@@ -17,6 +17,7 @@
17
17
  "./diagnostics": "./packages/core/diagnostics.js",
18
18
  "./issue-reporting": "./packages/core/issue-reporting.js",
19
19
  "./logging": "./packages/core/logging.js",
20
+ "./kfd-gate": "./packages/core/kfd-gate.js",
20
21
  "./release-candidate": "./packages/core/release-candidate.js",
21
22
  "./release-passport": "./packages/core/release-passport.js",
22
23
  "./release-propagation": "./packages/core/release-propagation.js",
@@ -91,6 +92,7 @@
91
92
  "vitest": "^4.1.9"
92
93
  },
93
94
  "dependencies": {
95
+ "@kungfu-tech/kfd": "1.0.0-alpha.2",
94
96
  "smol-toml": "1.7.0"
95
97
  }
96
98
  }
@@ -104,6 +104,17 @@ export {
104
104
  verifyArtifactPassport,
105
105
  } from "./artifact-passport.js";
106
106
 
107
+ export {
108
+ BUILDCHAIN_JSON_FORMATTING_POLICY,
109
+ KFD1_RELEASE_GATE_CONTRACT,
110
+ KFD1_WITNESS_SET_CONTRACT,
111
+ createKfd1ReleaseGateEvidence,
112
+ normalizeKfd1ContractWorldWitness,
113
+ resolveKfd1Metadata,
114
+ sha256Json as sha256KfdJson,
115
+ validateKfd1ReleaseGateEvidence,
116
+ } from "./kfd-gate.js";
117
+
107
118
  export {
108
119
  AGENT_INDEX_CONTRACT,
109
120
  ARTIFACT_EVIDENCE_CONTRACT,
@@ -0,0 +1,413 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+
6
+ const require = createRequire(import.meta.url);
7
+
8
+ export const KFD1_RELEASE_GATE_CONTRACT = "kungfu-buildchain-kfd-1-release-gate";
9
+ export const KFD1_WITNESS_SET_CONTRACT = "kungfu-buildchain-kfd-1-witness-set";
10
+ export const BUILDCHAIN_JSON_FORMATTING_POLICY = Object.freeze({
11
+ name: "buildchain-release-evidence-json-v1",
12
+ indentation: 2,
13
+ trailingNewline: true,
14
+ canonicalDigest: "stable-json-sha256-v1",
15
+ });
16
+
17
+ function optionalString(value) {
18
+ return value === undefined || value === null ? "" : String(value);
19
+ }
20
+
21
+ function nonEmptyString(value, label) {
22
+ const normalized = optionalString(value).trim();
23
+ if (!normalized) {
24
+ throw new Error(`${label} must be a non-empty string`);
25
+ }
26
+ return normalized;
27
+ }
28
+
29
+ function stableJson(value) {
30
+ if (Array.isArray(value)) {
31
+ return `[${value.map(stableJson).join(",")}]`;
32
+ }
33
+ if (value && typeof value === "object") {
34
+ return `{${Object.keys(value)
35
+ .sort()
36
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
37
+ .join(",")}}`;
38
+ }
39
+ return JSON.stringify(value);
40
+ }
41
+
42
+ function sha256Bytes(value) {
43
+ return crypto.createHash("sha256").update(value).digest("hex");
44
+ }
45
+
46
+ export function sha256Json(value) {
47
+ return sha256Bytes(stableJson(value));
48
+ }
49
+
50
+ export function sha256File(filePath) {
51
+ return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
52
+ }
53
+
54
+ function readJsonPackageExport(exportPath) {
55
+ return JSON.parse(fs.readFileSync(require.resolve(exportPath), "utf8"));
56
+ }
57
+
58
+ function loadKfdPackageMetadata() {
59
+ const packageJson = readJsonPackageExport("@kungfu-tech/kfd/package.json");
60
+ const standards = readJsonPackageExport("@kungfu-tech/kfd/standards.json");
61
+ return { packageJson, standards };
62
+ }
63
+
64
+ export function resolveKfd1Metadata() {
65
+ const { packageJson, standards } = loadKfdPackageMetadata();
66
+ const entries = Object.entries(standards.standards || {});
67
+ const entry = entries.find(([key, value]) => {
68
+ const names = [key, value?.key, value?.id, value?.label]
69
+ .filter(Boolean)
70
+ .map((name) => String(name).toLowerCase());
71
+ return (
72
+ names.includes("kfd-1") &&
73
+ value?.schemaIds?.contractWorld &&
74
+ value?.schemaIds?.witness &&
75
+ value?.schemaPaths?.contractWorld &&
76
+ value?.schemaPaths?.witness
77
+ );
78
+ });
79
+ if (!entry) {
80
+ throw new Error("KFD metadata package does not expose the KFD-1 contract-world witness standard");
81
+ }
82
+ const [key, standard] = entry;
83
+ return {
84
+ key: standard.key || key,
85
+ id: standard.id || standard.label || key,
86
+ label: standard.label || standard.id || key,
87
+ title: standard.title || "",
88
+ status: standard.status || "",
89
+ revision: standard.revision || 0,
90
+ package: {
91
+ name: packageJson.name,
92
+ version: packageJson.version,
93
+ repository: packageJson.repository?.url || "",
94
+ },
95
+ schemaIds: { ...standard.schemaIds },
96
+ schemaPaths: { ...standard.schemaPaths },
97
+ concepts: { ...(standard.concepts || {}) },
98
+ metadataSchema: standards.metadataSchema || {},
99
+ source: standards.source || {},
100
+ };
101
+ }
102
+
103
+ function normalizeHash(value, label, { required = false } = {}) {
104
+ const text = optionalString(value).replace(/^sha256:/, "").trim();
105
+ if (!text && !required) {
106
+ return "";
107
+ }
108
+ if (!/^[0-9a-f]{64}$/i.test(text)) {
109
+ throw new Error(`${label} must be a sha256 hex digest`);
110
+ }
111
+ return text.toLowerCase();
112
+ }
113
+
114
+ function normalizePath(value, label) {
115
+ const text = nonEmptyString(value, label).replace(/\\/g, "/");
116
+ if (path.isAbsolute(text)) {
117
+ throw new Error(`${label} must be repository-relative`);
118
+ }
119
+ if (text.split("/").some((part) => part === "..")) {
120
+ throw new Error(`${label} must not contain ..`);
121
+ }
122
+ return text;
123
+ }
124
+
125
+ function normalizeEvidenceFile(value, label) {
126
+ if (!value) {
127
+ return undefined;
128
+ }
129
+ if (typeof value === "string") {
130
+ return { path: normalizePath(value, `${label}.path`) };
131
+ }
132
+ if (typeof value !== "object" || Array.isArray(value)) {
133
+ throw new Error(`${label} must be an object`);
134
+ }
135
+ return {
136
+ ...value,
137
+ path: value.path ? normalizePath(value.path, `${label}.path`) : "",
138
+ sha256: value.sha256 ? normalizeHash(value.sha256, `${label}.sha256`) : "",
139
+ };
140
+ }
141
+
142
+ function normalizeSurface(surface, index) {
143
+ if (!surface || typeof surface !== "object" || Array.isArray(surface)) {
144
+ throw new Error(`surfaces[${index}] must be an object`);
145
+ }
146
+ const artifactPath = normalizePath(surface.artifactPath || surface.artifact_path, `surfaces[${index}].artifactPath`);
147
+ return {
148
+ name: nonEmptyString(surface.name || surface.id, `surfaces[${index}].name`),
149
+ sourcePath: surface.sourcePath || surface.source_path
150
+ ? normalizePath(surface.sourcePath || surface.source_path, `surfaces[${index}].sourcePath`)
151
+ : "",
152
+ sourceSha256: normalizeHash(surface.sourceSha256 || surface.source_sha256, `surfaces[${index}].sourceSha256`),
153
+ artifactPath,
154
+ expectedSha256: normalizeHash(surface.expectedSha256 || surface.expected_sha256, `surfaces[${index}].expectedSha256`, { required: true }),
155
+ byteForByte: surface.byteForByte === undefined && surface.byte_for_byte === undefined
156
+ ? true
157
+ : Boolean(surface.byteForByte ?? surface.byte_for_byte),
158
+ };
159
+ }
160
+
161
+ export function normalizeKfd1ContractWorldWitness(witness, { metadata = resolveKfd1Metadata() } = {}) {
162
+ if (!witness || typeof witness !== "object" || Array.isArray(witness)) {
163
+ throw new Error("KFD-1 witness must be a JSON object");
164
+ }
165
+ const surfaces = Array.isArray(witness.surfaces)
166
+ ? witness.surfaces.map((surface, index) => normalizeSurface(surface, index))
167
+ : [];
168
+ if (surfaces.length === 0) {
169
+ throw new Error("KFD-1 witness surfaces[] must include at least one artifact surface");
170
+ }
171
+ const standard = optionalString(witness.standard || metadata.key);
172
+ if (standard && standard !== metadata.key && standard !== metadata.id && standard !== metadata.label) {
173
+ throw new Error(`KFD-1 witness standard must match ${metadata.key}`);
174
+ }
175
+ return {
176
+ schemaVersion: 1,
177
+ contract: KFD1_WITNESS_SET_CONTRACT,
178
+ id: nonEmptyString(witness.id, "KFD-1 witness id"),
179
+ standard: metadata.key,
180
+ standardLabel: metadata.label,
181
+ source: witness.source && typeof witness.source === "object" && !Array.isArray(witness.source)
182
+ ? { ...witness.source }
183
+ : {},
184
+ contractWorld: witness.contractWorld && typeof witness.contractWorld === "object" && !Array.isArray(witness.contractWorld)
185
+ ? {
186
+ ...witness.contractWorld,
187
+ schemaId: witness.contractWorld.schemaId || metadata.schemaIds.contractWorld,
188
+ digest: optionalString(witness.contractWorld.digest || witness.contractWorld.sha256),
189
+ }
190
+ : {
191
+ schemaId: metadata.schemaIds.contractWorld,
192
+ digest: "",
193
+ },
194
+ canonicalPolicy: normalizeEvidenceFile(witness.canonicalPolicy || witness.canonical_policy, "canonicalPolicy"),
195
+ registry: normalizeEvidenceFile(witness.registry, "registry"),
196
+ surfaces,
197
+ metadata: {
198
+ kfdPackage: metadata.package,
199
+ schemaIds: metadata.schemaIds,
200
+ schemaPaths: metadata.schemaPaths,
201
+ },
202
+ };
203
+ }
204
+
205
+ function resolveArtifactFile({ cwd, artifactRoot, artifacts = [], artifactPath }) {
206
+ const candidates = [
207
+ path.resolve(cwd, artifactPath),
208
+ artifactRoot ? path.resolve(artifactRoot, artifactPath) : "",
209
+ ].filter(Boolean);
210
+ for (const artifact of artifacts) {
211
+ const sourcePath = artifact.sourcePath || artifact.path || "";
212
+ if (!sourcePath) {
213
+ continue;
214
+ }
215
+ const normalizedSource = sourcePath.replace(/\\/g, "/");
216
+ if (normalizedSource === artifactPath || normalizedSource.endsWith(`/${artifactPath}`)) {
217
+ candidates.push(path.resolve(sourcePath));
218
+ }
219
+ const name = artifact.name || artifact.filename || "";
220
+ if (name && name === artifactPath) {
221
+ candidates.push(path.resolve(sourcePath));
222
+ }
223
+ }
224
+ return candidates.find((candidate) => candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) || "";
225
+ }
226
+
227
+ export function createKfd1ReleaseGateEvidence({
228
+ cwd = process.cwd(),
229
+ artifactRoot = "",
230
+ artifacts = [],
231
+ witnesses = [],
232
+ verifiedAt = new Date().toISOString(),
233
+ metadata = resolveKfd1Metadata(),
234
+ } = {}) {
235
+ const normalizedWitnesses = witnesses.map((witness) => normalizeKfd1ContractWorldWitness(witness, { metadata }));
236
+ if (normalizedWitnesses.length === 0) {
237
+ return undefined;
238
+ }
239
+ const worlds = normalizedWitnesses.map((witness) => {
240
+ const preBuildWitnessSha256 = sha256Json(witness);
241
+ const surfaceResults = witness.surfaces.map((surface) => {
242
+ const filePath = resolveArtifactFile({
243
+ cwd,
244
+ artifactRoot,
245
+ artifacts,
246
+ artifactPath: surface.artifactPath,
247
+ });
248
+ if (!filePath) {
249
+ return {
250
+ name: surface.name,
251
+ artifactPath: surface.artifactPath,
252
+ expectedSha256: surface.expectedSha256,
253
+ actualSha256: "",
254
+ byteForByte: surface.byteForByte,
255
+ status: "failed",
256
+ reason: "artifact-missing",
257
+ };
258
+ }
259
+ const actualSha256 = sha256File(filePath);
260
+ const passed = actualSha256 === surface.expectedSha256;
261
+ return {
262
+ name: surface.name,
263
+ artifactPath: surface.artifactPath,
264
+ expectedSha256: surface.expectedSha256,
265
+ actualSha256,
266
+ byteForByte: surface.byteForByte,
267
+ status: passed ? "passed" : "failed",
268
+ reason: passed ? "" : "digest-mismatch",
269
+ };
270
+ });
271
+ const status = surfaceResults.every((surface) => surface.status === "passed") ? "passed" : "failed";
272
+ return {
273
+ id: witness.id,
274
+ standard: metadata.label,
275
+ standardKey: metadata.key,
276
+ preBuildWitnessSha256,
277
+ witness,
278
+ artifactVerification: {
279
+ status,
280
+ verifiedAt,
281
+ surfaces: surfaceResults,
282
+ },
283
+ };
284
+ });
285
+ const status = worlds.every((world) => world.artifactVerification.status === "passed") ? "passed" : "failed";
286
+ return {
287
+ key: metadata.key,
288
+ passportSection: {
289
+ schemaVersion: 1,
290
+ contract: KFD1_RELEASE_GATE_CONTRACT,
291
+ status,
292
+ metadata: {
293
+ standard: {
294
+ key: metadata.key,
295
+ id: metadata.id,
296
+ label: metadata.label,
297
+ title: metadata.title,
298
+ revision: metadata.revision,
299
+ status: metadata.status,
300
+ },
301
+ schemas: {
302
+ ids: metadata.schemaIds,
303
+ paths: metadata.schemaPaths,
304
+ metadata: metadata.metadataSchema,
305
+ },
306
+ package: metadata.package,
307
+ },
308
+ formatting: BUILDCHAIN_JSON_FORMATTING_POLICY,
309
+ contractWorlds: worlds,
310
+ },
311
+ };
312
+ }
313
+
314
+ export function validateKfd1ReleaseGateEvidence(section, { metadata = resolveKfd1Metadata() } = {}) {
315
+ const issues = [];
316
+ if (!section) {
317
+ return issues;
318
+ }
319
+ if (typeof section !== "object" || Array.isArray(section)) {
320
+ issues.push({
321
+ level: "error",
322
+ code: `${metadata.key}.object`,
323
+ message: `${metadata.key} release gate evidence must be an object`,
324
+ details: {},
325
+ });
326
+ return issues;
327
+ }
328
+ if (section.contract !== KFD1_RELEASE_GATE_CONTRACT) {
329
+ issues.push({
330
+ level: "error",
331
+ code: `${metadata.key}.contract`,
332
+ message: `${metadata.key}.contract must be ${KFD1_RELEASE_GATE_CONTRACT}`,
333
+ details: {},
334
+ });
335
+ }
336
+ if (section.metadata?.standard?.key !== metadata.key) {
337
+ issues.push({
338
+ level: "error",
339
+ code: `${metadata.key}.metadata.standard.key`,
340
+ message: `${metadata.key} metadata key must come from the KFD metadata package`,
341
+ details: { expected: metadata.key, actual: section.metadata?.standard?.key || "" },
342
+ });
343
+ }
344
+ const worlds = Array.isArray(section.contractWorlds) ? section.contractWorlds : [];
345
+ if (worlds.length === 0) {
346
+ issues.push({
347
+ level: "error",
348
+ code: `${metadata.key}.contractWorlds.empty`,
349
+ message: `${metadata.key}.contractWorlds must include at least one witness`,
350
+ details: {},
351
+ });
352
+ }
353
+ for (const [worldIndex, world] of worlds.entries()) {
354
+ if (!world.preBuildWitnessSha256) {
355
+ issues.push({
356
+ level: "error",
357
+ code: `${metadata.key}.contractWorlds[${worldIndex}].preBuildWitnessSha256`,
358
+ message: "KFD-1 contract world must record the frozen pre-build witness digest",
359
+ details: {},
360
+ });
361
+ }
362
+ if (world.standardKey !== metadata.key) {
363
+ issues.push({
364
+ level: "error",
365
+ code: `${metadata.key}.contractWorlds[${worldIndex}].standardKey`,
366
+ message: "KFD-1 contract world standardKey must match KFD metadata",
367
+ details: { expected: metadata.key, actual: world.standardKey || "" },
368
+ });
369
+ }
370
+ if (world.artifactVerification?.status !== "passed") {
371
+ issues.push({
372
+ level: "error",
373
+ code: `${metadata.key}.contractWorlds[${worldIndex}].artifactVerification`,
374
+ message: "KFD-1 artifact verification must pass before release passport finalization",
375
+ details: { id: world.id || "", status: world.artifactVerification?.status || "" },
376
+ });
377
+ }
378
+ const surfaces = Array.isArray(world.artifactVerification?.surfaces) ? world.artifactVerification.surfaces : [];
379
+ if (surfaces.length === 0) {
380
+ issues.push({
381
+ level: "error",
382
+ code: `${metadata.key}.contractWorlds[${worldIndex}].surfaces.empty`,
383
+ message: "KFD-1 artifact verification must list checked surfaces",
384
+ details: { id: world.id || "" },
385
+ });
386
+ }
387
+ for (const [surfaceIndex, surface] of surfaces.entries()) {
388
+ if (surface.status !== "passed" || !surface.expectedSha256 || !surface.actualSha256 || surface.expectedSha256 !== surface.actualSha256) {
389
+ issues.push({
390
+ level: "error",
391
+ code: `${metadata.key}.contractWorlds[${worldIndex}].surfaces[${surfaceIndex}]`,
392
+ message: "KFD-1 verified surface digest mismatch",
393
+ details: {
394
+ id: world.id || "",
395
+ name: surface.name || "",
396
+ artifactPath: surface.artifactPath || "",
397
+ status: surface.status || "",
398
+ reason: surface.reason || "",
399
+ },
400
+ });
401
+ }
402
+ }
403
+ }
404
+ if (section.status !== "passed") {
405
+ issues.push({
406
+ level: "error",
407
+ code: `${metadata.key}.status`,
408
+ message: "KFD-1 release gate status must be passed",
409
+ details: { status: section.status || "" },
410
+ });
411
+ }
412
+ return issues;
413
+ }
@@ -4,6 +4,11 @@ import http from "node:http";
4
4
  import https from "node:https";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
+ import {
8
+ createKfd1ReleaseGateEvidence,
9
+ resolveKfd1Metadata,
10
+ validateKfd1ReleaseGateEvidence,
11
+ } from "./kfd-gate.js";
7
12
 
8
13
  export const RELEASE_PASSPORT_CONTRACT = "kungfu-buildchain-release-passport";
9
14
  export const ARTIFACT_EVIDENCE_CONTRACT = "kungfu-buildchain-artifact-evidence";
@@ -621,6 +626,7 @@ export function createReleasePassport({
621
626
  publish = {},
622
627
  impact = undefined,
623
628
  workflow = {},
629
+ kfd1 = undefined,
624
630
  } = {}) {
625
631
  const normalizedTag = nonEmptyString(tag, "tag");
626
632
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: normalizedTag, sourceSha, workflow });
@@ -636,6 +642,8 @@ export function createReleasePassport({
636
642
  ? normalizeEvidenceDocument(distTagPromotionEvidence, "distTagPromotionEvidence")
637
643
  : undefined;
638
644
  const normalizedImpact = normalizeImpactLedger(impact, { tag: normalizedTag, line });
645
+ const kfd1Metadata = resolveKfd1Metadata();
646
+ const normalizedKfd1 = kfd1?.passportSection ? kfd1 : undefined;
639
647
  const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
640
648
  const normalizedPublishSummary = normalizePublishSummary({
641
649
  packageSet: normalizedPackageSet,
@@ -736,6 +744,7 @@ export function createReleasePassport({
736
744
  ...(normalizedBuildSummary ? { buildSummary: normalizedBuildSummary } : {}),
737
745
  ...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
738
746
  ...(normalizedDistTagPromotionEvidence ? { distTagPromotion: normalizedDistTagPromotionEvidence } : {}),
747
+ ...(normalizedKfd1 ? { [normalizedKfd1.key || kfd1Metadata.key]: normalizedKfd1.passportSection } : {}),
739
748
  versionImpact: normalizedImpact.versionImpact,
740
749
  surfaceImpacts: normalizedImpact.surfaceImpacts,
741
750
  artifacts: [
@@ -771,6 +780,7 @@ export function createReleasePassport({
771
780
  artifactName: manifest.artifactName,
772
781
  })),
773
782
  distTagPromotionEvidence: normalizedDistTagPromotionEvidence?.path || "",
783
+ kfd1: normalizedKfd1 ? `${normalizedKfd1.key || kfd1Metadata.key}` : "",
774
784
  impact: impactPath,
775
785
  checkReport: checkReportPath,
776
786
  agentIndex: agentIndexPath,
@@ -804,6 +814,7 @@ export function collectGitHubReleasePassport({
804
814
  buildSummaryJson = "",
805
815
  platformManifestJsons = [],
806
816
  distTagEvidenceJson = "",
817
+ kfd1WitnessJsons = [],
807
818
  releaseJsonExtra = "",
808
819
  publishJson = "",
809
820
  workflow = {},
@@ -822,11 +833,15 @@ export function collectGitHubReleasePassport({
822
833
  .filter(Boolean)
823
834
  .map((manifestJson) => parseJsonInputWithMeta(manifestJson, undefined));
824
835
  const distTagEvidenceMeta = parseJsonInputWithMeta(distTagEvidenceJson, undefined);
836
+ const kfd1WitnessMetas = (kfd1WitnessJsons || [])
837
+ .filter(Boolean)
838
+ .map((witnessJson) => parseJsonInputWithMeta(witnessJson, undefined))
839
+ .filter((meta) => meta.value);
825
840
  const publish = parseJsonInput(publishJson, {});
826
841
  const assets = [
827
842
  ...(Array.isArray(release.assets) ? release.assets : []),
828
843
  ...(Array.isArray(assetsFromJson) ? assetsFromJson : []),
829
- ...discoverAssetsFromDir(assetsDir),
844
+ ...discoverAssetsFromDir(assetsDir ? path.resolve(cwd, assetsDir) : ""),
830
845
  ];
831
846
  const resolvedTag = tag || release.tag_name || release.name || "";
832
847
  const resolvedOutputDir = path.resolve(cwd, outputDir);
@@ -834,6 +849,12 @@ export function collectGitHubReleasePassport({
834
849
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: resolvedTag, sourceSha, workflow });
835
850
  const impact = normalizeImpactLedger(impactMeta.value, { tag: resolvedTag, line, decision: "unknown" });
836
851
  const agentIndex = defaultAgentIndex({ tag: resolvedTag });
852
+ const kfd1 = createKfd1ReleaseGateEvidence({
853
+ cwd,
854
+ artifactRoot: assetsDir ? path.resolve(cwd, assetsDir) : "",
855
+ artifacts: assets,
856
+ witnesses: kfd1WitnessMetas.map((meta) => meta.value),
857
+ });
837
858
  const passport = createReleasePassport({
838
859
  cwd,
839
860
  repository,
@@ -870,6 +891,7 @@ export function collectGitHubReleasePassport({
870
891
  release: releaseExtra,
871
892
  publish,
872
893
  impact,
894
+ kfd1,
873
895
  publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
874
896
  transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
875
897
  workflow,
@@ -1170,6 +1192,8 @@ export function createReleaseCheckReport({
1170
1192
  const declaredFinalImpact = normalizeImpactLevel(impact?.versionImpact?.final || impact?.classification);
1171
1193
  const computedFinalImpact = highestImpactLevel(surfaceImpacts.map((entry) => entry.impact));
1172
1194
  const requiredSurfaceImpacts = surfaceImpactRequirement({ passport, impact });
1195
+ const kfd1Metadata = resolveKfd1Metadata();
1196
+ issues.push(...validateKfd1ReleaseGateEvidence(passport?.[kfd1Metadata.key], { metadata: kfd1Metadata }));
1173
1197
  if (requiredSurfaceImpacts.required && surfaceImpacts.length === 0) {
1174
1198
  issues.push(issue("error", "impact.surfaceImpacts.required", "surfaceImpacts[] is required for this release passport type", requiredSurfaceImpacts));
1175
1199
  }
@@ -93,6 +93,9 @@ if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-report
93
93
  if (rootPackage.exports?.["./logging"] !== "./packages/core/logging.js") {
94
94
  throw new Error("root package must export @kungfu-tech/buildchain/logging");
95
95
  }
96
+ if (rootPackage.exports?.["./kfd-gate"] !== "./packages/core/kfd-gate.js") {
97
+ throw new Error("root package must export @kungfu-tech/buildchain/kfd-gate");
98
+ }
96
99
  if (rootPackage.exports?.["./release-passport"] !== "./packages/core/release-passport.js") {
97
100
  throw new Error("root package must export @kungfu-tech/buildchain/release-passport");
98
101
  }
@@ -138,10 +141,22 @@ if (!coreIndexSource.includes("collectBuildchainDiagnostics")) {
138
141
  if (!coreIndexSource.includes("reportBuildchainIssue")) {
139
142
  throw new Error("packages/core/index.js must export reportBuildchainIssue");
140
143
  }
144
+ for (const requiredSnippet of [
145
+ "createKfd1ReleaseGateEvidence",
146
+ "resolveKfd1Metadata",
147
+ "validateKfd1ReleaseGateEvidence",
148
+ ]) {
149
+ if (!coreIndexSource.includes(requiredSnippet)) {
150
+ throw new Error(`packages/core/index.js must export KFD-1 gate API: ${requiredSnippet}`);
151
+ }
152
+ }
141
153
  for (const requiredSnippet of [
142
154
  "surfaceImpacts[]",
143
155
  "versionImpact.final",
144
156
  "kfd-registry-schema",
157
+ "`v2.8`",
158
+ "kfd-1-contract-world-release-gate",
159
+ "required-check-protection",
145
160
  "`v2.2`",
146
161
  "GitHub-hosted runners for production",
147
162
  "Self-hosted runners remain compatibility fixtures",
@@ -150,6 +165,18 @@ for (const requiredSnippet of [
150
165
  throw new Error(`versioning doc missing required snippet: ${requiredSnippet}`);
151
166
  }
152
167
  }
168
+ const releasePassportDoc = fs.readFileSync(path.join(root, "docs/release-passport.md"), "utf8");
169
+ for (const requiredSnippet of [
170
+ "--kfd-1-witness-json",
171
+ "@kungfu-tech/kfd",
172
+ "currently named `kfd-1`",
173
+ "Buildchain formatting policy",
174
+ "Verification fails closed",
175
+ ]) {
176
+ if (!releasePassportDoc.includes(requiredSnippet)) {
177
+ throw new Error(`release passport doc missing KFD-1 gate snippet: ${requiredSnippet}`);
178
+ }
179
+ }
153
180
  for (const [docName, docSource] of Object.entries({ "docs/cli.md": cliDoc, "docs/install.md": installDoc })) {
154
181
  for (const requiredSnippet of [
155
182
  "minimumReleaseAgeExclude",
@@ -227,12 +254,24 @@ for (const requiredSnippet of [
227
254
  "target-sha: ${{ github.event.workflow_run.head_sha || inputs.sha || github.sha }}",
228
255
  "publish-required-artifacts-json: \"[]\"",
229
256
  "release-passport-impact-json: >-",
257
+ "Buildchain v2.8 promotes KFD-1 contract-world release gates",
258
+ "kfd-1-contract-world-release-gate",
259
+ "required-check-protection",
230
260
  "\"surfaceImpacts\":[",
231
261
  ]) {
232
262
  if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
233
263
  throw new Error(`buildchain ref promotion workflow missing npm transaction snippet: ${requiredSnippet}`);
234
264
  }
235
265
  }
266
+ const releaseCandidatePromoteWorkflow = fs.readFileSync(path.join(root, ".github/workflows/release-candidate-promote.yml"), "utf8");
267
+ for (const requiredSnippet of [
268
+ "release-passport-kfd-1-witness-jsons:",
269
+ "release-passport-kfd-1-witness-jsons: ${{ inputs.release-passport-kfd-1-witness-jsons }}",
270
+ ]) {
271
+ if (!releaseCandidatePromoteWorkflow.includes(requiredSnippet)) {
272
+ throw new Error(`release candidate promote workflow missing KFD-1 gate pass-through: ${requiredSnippet}`);
273
+ }
274
+ }
236
275
  for (const forbiddenSnippet of [
237
276
  "run: node scripts/release-candidate-resolver.mjs",
238
277
  "uses: ./actions/promote-buildchain-ref",