@kungfu-tech/buildchain 3.0.7-alpha.0 → 3.0.7

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 (67) hide show
  1. package/actions/promote-buildchain-ref/README.md +10 -0
  2. package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
  3. package/contracts/engineering-housekeeper-v1.schema.json +143 -0
  4. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
  5. package/dist/site/buildchain-contract.json +24 -24
  6. package/dist/site/buildchain-site.json +91 -30
  7. package/dist/site/capability-registry.json +3 -3
  8. package/dist/site/controller-registry.json +6 -2
  9. package/dist/site/kfd-claims.json +122 -11
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +8 -7
  12. package/dist/site/node-api-registry.json +683 -105
  13. package/dist/site/page-registry.json +80 -19
  14. package/dist/site/public-surface-audit.json +98 -7
  15. package/dist/site/publication-authority-registry.json +81 -1
  16. package/dist/site/publication-registry.json +4 -4
  17. package/dist/site/release-provenance.json +2 -0
  18. package/dist/site/site-manifest.json +11 -11
  19. package/dist/site/workflow-registry.json +119 -2
  20. package/docs/MAP.md +1 -0
  21. package/docs/auditable-demo.md +2 -2
  22. package/docs/dev-delivery-warrant.md +49 -4
  23. package/docs/engineering-housekeeper.md +138 -0
  24. package/docs/lifecycle-protocol.md +4 -2
  25. package/docs/node-api-reference.md +277 -212
  26. package/docs/release-governance.md +17 -2
  27. package/docs/release-tail-provider-plane.md +1 -1
  28. package/docs/reusable-build-surface.md +11 -0
  29. package/package.json +4 -1
  30. package/packages/core/artifact-signing.js +61 -0
  31. package/packages/core/buildchain-config.js +66 -6
  32. package/packages/core/buildchain-publication-authority.js +4 -0
  33. package/packages/core/controller-evidence.js +2 -1
  34. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  35. package/packages/core/dev-delivery-warrant-shadow.js +502 -0
  36. package/packages/core/dev-delivery-warrant.js +15 -6
  37. package/packages/core/diagnostics.js +8 -3
  38. package/packages/core/engineering-housekeeper-github-client.js +222 -0
  39. package/packages/core/engineering-housekeeper-github.js +501 -0
  40. package/packages/core/engineering-housekeeper.js +259 -0
  41. package/packages/core/index.js +3 -0
  42. package/packages/core/kfd-gate.js +45 -15
  43. package/packages/core/publication-rehearsal-runtime.js +13 -1
  44. package/packages/core/release-passport.js +130 -20
  45. package/scripts/assemble-self-publication-admission.mjs +1 -1
  46. package/scripts/audit-publication-control-plane.mjs +1 -1
  47. package/scripts/auditable-demo-bundle-verification.mjs +2 -3
  48. package/scripts/auditable-demo-platform.mjs +2 -2
  49. package/scripts/auditable-demo-renditions.mjs +1 -1
  50. package/scripts/auditable-demo.mjs +2 -2
  51. package/scripts/build-contract-core.mjs +8 -3
  52. package/scripts/build-standalone-binary.mjs +14 -3
  53. package/scripts/check-inventory.mjs +3 -1
  54. package/scripts/dev-delivery-warrant.mjs +31 -4
  55. package/scripts/dev-pr-auto-merge.mjs +30 -4
  56. package/scripts/dev-pr-delivery-warrant.mjs +50 -0
  57. package/scripts/engineering-housekeeper-workflow.mjs +394 -0
  58. package/scripts/generate-site-bundle.mjs +23 -4
  59. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  60. package/scripts/materialize-self-release-candidate-version.mjs +6 -0
  61. package/scripts/publication-commit-evidence.mjs +69 -23
  62. package/scripts/release-candidate-resolver.mjs +16 -10
  63. package/scripts/resume-from-candidate-run.mjs +123 -9
  64. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  65. package/scripts/site-capability-metadata.mjs +2 -0
  66. package/scripts/web-surface-core.mjs +8 -2
  67. package/scripts/workflow-call-contract.mjs +1 -1
@@ -0,0 +1,259 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export const ENGINEERING_HOUSEKEEPER_CONTRACT =
4
+ "kungfu-buildchain-engineering-housekeeper/v1";
5
+ export const ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION = 1;
6
+
7
+ export const HOUSEKEEPER_REASON_CODES = Object.freeze({
8
+ ELIGIBLE_MERGED_BRANCH: "eligible.merged-branch",
9
+ PROTECTED_BRANCH: "branch.protected",
10
+ RETAINED_BRANCH: "branch.retained",
11
+ DEFAULT_BRANCH: "branch.default",
12
+ TARGET_BRANCH: "branch.target",
13
+ OPEN_PR_HEAD: "branch.open-pr-head",
14
+ CROSS_REPOSITORY: "branch.cross-repository",
15
+ NOT_MERGED: "branch.not-merged",
16
+ HEAD_ADVANCED: "branch.head-advanced",
17
+ RENAMED: "branch.renamed",
18
+ TARGET_ADVANCED: "branch.target-advanced",
19
+ PERMISSION_DENIED: "branch.permission-denied",
20
+ PR_ACTIVE: "pull-request.active",
21
+ PR_STALE_REPORT: "pull-request.stale-report",
22
+ PR_LABEL_ELIGIBLE: "pull-request.label-eligible",
23
+ PR_AUTO_CLOSE_DISABLED: "pull-request.auto-close-disabled",
24
+ PR_FORKED: "pull-request.forked",
25
+ REPEATED_NO_OP: "replay.already-applied",
26
+ });
27
+
28
+ export const DEFAULT_HOUSEKEEPER_POLICY = Object.freeze({
29
+ protectedPatterns: ["dev/**", "alpha/**", "release/**", "publish-gate/**"],
30
+ retainedPatterns: ["train/**", "authority/**"],
31
+ pullRequests: Object.freeze({
32
+ reportStale: true,
33
+ label: "",
34
+ autoClose: false,
35
+ }),
36
+ });
37
+
38
+ function stableJson(value) {
39
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
40
+ if (value && typeof value === "object") {
41
+ return `{${Object.keys(value)
42
+ .sort()
43
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
44
+ .join(",")}}`;
45
+ }
46
+ return JSON.stringify(value);
47
+ }
48
+
49
+ export function engineeringHousekeeperRoot(value) {
50
+ return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
51
+ }
52
+
53
+ function globMatches(pattern, value) {
54
+ const expression = String(pattern)
55
+ .split("**")
56
+ .map((chunk) =>
57
+ chunk
58
+ .split("*")
59
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
60
+ .join("[^/]*"),
61
+ )
62
+ .join(".*");
63
+ return new RegExp(`^${expression}$`).test(value);
64
+ }
65
+
66
+ function matchesAny(patterns, value) {
67
+ return [...(patterns || [])].some((pattern) => globMatches(pattern, value));
68
+ }
69
+
70
+ function normalizePolicy(policy = {}) {
71
+ return {
72
+ protectedPatterns: [
73
+ ...(policy.protectedPatterns ||
74
+ DEFAULT_HOUSEKEEPER_POLICY.protectedPatterns),
75
+ ].sort(),
76
+ retainedPatterns: [
77
+ ...(policy.retainedPatterns ||
78
+ DEFAULT_HOUSEKEEPER_POLICY.retainedPatterns),
79
+ ].sort(),
80
+ pullRequests: {
81
+ reportStale: policy.pullRequests?.reportStale !== false,
82
+ label: String(policy.pullRequests?.label || ""),
83
+ autoClose: false,
84
+ },
85
+ };
86
+ }
87
+
88
+ function sortedUnique(values) {
89
+ return [...new Set(values)].sort();
90
+ }
91
+
92
+ export function classifyHousekeeperBranch(branch, policyInput = {}) {
93
+ const policy = normalizePolicy(policyInput);
94
+ const reasons = [];
95
+ if (branch.isDefault) reasons.push(HOUSEKEEPER_REASON_CODES.DEFAULT_BRANCH);
96
+ if (branch.name === branch.target?.name)
97
+ reasons.push(HOUSEKEEPER_REASON_CODES.TARGET_BRANCH);
98
+ if (
99
+ branch.isProtected === true ||
100
+ matchesAny(policy.protectedPatterns, branch.name)
101
+ )
102
+ reasons.push(HOUSEKEEPER_REASON_CODES.PROTECTED_BRANCH);
103
+ if (matchesAny(policy.retainedPatterns, branch.name))
104
+ reasons.push(HOUSEKEEPER_REASON_CODES.RETAINED_BRANCH);
105
+ if (branch.sourceRepository && branch.sourceRepository !== branch.repository)
106
+ reasons.push(HOUSEKEEPER_REASON_CODES.CROSS_REPOSITORY);
107
+ if ((branch.openPullRequestNumbers || []).length > 0)
108
+ reasons.push(HOUSEKEEPER_REASON_CODES.OPEN_PR_HEAD);
109
+ if (branch.ancestry !== "ancestor")
110
+ reasons.push(HOUSEKEEPER_REASON_CODES.NOT_MERGED);
111
+ const eligible =
112
+ reasons.length === 0 &&
113
+ Boolean(branch.headOid) &&
114
+ Boolean(branch.target?.headOid);
115
+ return {
116
+ kind: "branch",
117
+ repository: branch.repository,
118
+ name: branch.name,
119
+ headOid: branch.headOid,
120
+ target: { name: branch.target?.name, headOid: branch.target?.headOid },
121
+ eligible,
122
+ decision: eligible ? "delete" : "retain",
123
+ reasonCodes: eligible
124
+ ? [HOUSEKEEPER_REASON_CODES.ELIGIBLE_MERGED_BRANCH]
125
+ : sortedUnique(reasons),
126
+ };
127
+ }
128
+
129
+ export function classifyHousekeeperPullRequest(pullRequest, policyInput = {}) {
130
+ const policy = normalizePolicy(policyInput);
131
+ const active = ["open", "draft"].includes(pullRequest.state);
132
+ const forked = pullRequest.headRepository !== pullRequest.repository;
133
+ const stale = active && pullRequest.stale === true;
134
+ const actions = stale && policy.pullRequests.reportStale ? ["report"] : [];
135
+ if (stale && policy.pullRequests.label) actions.push("label");
136
+ const reasons = [HOUSEKEEPER_REASON_CODES.PR_AUTO_CLOSE_DISABLED];
137
+ if (active) reasons.push(HOUSEKEEPER_REASON_CODES.PR_ACTIVE);
138
+ if (stale) reasons.push(HOUSEKEEPER_REASON_CODES.PR_STALE_REPORT);
139
+ if (stale && policy.pullRequests.label)
140
+ reasons.push(HOUSEKEEPER_REASON_CODES.PR_LABEL_ELIGIBLE);
141
+ if (forked) reasons.push(HOUSEKEEPER_REASON_CODES.PR_FORKED);
142
+ return {
143
+ kind: "pull-request",
144
+ repository: pullRequest.repository,
145
+ number: pullRequest.number,
146
+ headRepository: pullRequest.headRepository,
147
+ headRef: pullRequest.headRef,
148
+ headOid: pullRequest.headOid,
149
+ state: pullRequest.state,
150
+ actions: actions.sort(),
151
+ reasonCodes: sortedUnique(reasons),
152
+ };
153
+ }
154
+
155
+ export function createEngineeringHousekeeperPlan({
156
+ repository,
157
+ target,
158
+ branches = [],
159
+ pullRequests = [],
160
+ policy = {},
161
+ observedAt,
162
+ }) {
163
+ const normalizedPolicy = normalizePolicy(policy);
164
+ const inventory = [
165
+ ...branches.map((branch) =>
166
+ classifyHousekeeperBranch(branch, normalizedPolicy),
167
+ ),
168
+ ...pullRequests.map((pullRequest) =>
169
+ classifyHousekeeperPullRequest(pullRequest, normalizedPolicy),
170
+ ),
171
+ ].sort((left, right) =>
172
+ `${left.kind}:${left.name || String(left.number).padStart(12, "0")}`.localeCompare(
173
+ `${right.kind}:${right.name || String(right.number).padStart(12, "0")}`,
174
+ ),
175
+ );
176
+ const body = {
177
+ contract: ENGINEERING_HOUSEKEEPER_CONTRACT,
178
+ schemaVersion: ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION,
179
+ mode: "plan",
180
+ repository,
181
+ target,
182
+ observedAt,
183
+ policy: normalizedPolicy,
184
+ inventory,
185
+ actions: inventory.flatMap((entry) =>
186
+ entry.kind === "branch" && entry.eligible
187
+ ? [
188
+ {
189
+ kind: "delete-branch",
190
+ name: entry.name,
191
+ expectedHeadOid: entry.headOid,
192
+ targetName: entry.target.name,
193
+ expectedTargetHeadOid: entry.target.headOid,
194
+ },
195
+ ]
196
+ : entry.kind === "pull-request"
197
+ ? entry.actions.map((action) => ({
198
+ kind: `${action}-pull-request`,
199
+ number: entry.number,
200
+ expectedHeadOid: entry.headOid,
201
+ }))
202
+ : [],
203
+ ),
204
+ };
205
+ return { ...body, planRoot: engineeringHousekeeperRoot(body) };
206
+ }
207
+
208
+ export function revalidateHousekeeperBranchAction(
209
+ action,
210
+ current,
211
+ policy = {},
212
+ ) {
213
+ const reasons = [];
214
+ if (current.name !== action.name)
215
+ reasons.push(HOUSEKEEPER_REASON_CODES.RENAMED);
216
+ if (current.headOid !== action.expectedHeadOid)
217
+ reasons.push(HOUSEKEEPER_REASON_CODES.HEAD_ADVANCED);
218
+ if (current.target?.headOid !== action.expectedTargetHeadOid)
219
+ reasons.push(HOUSEKEEPER_REASON_CODES.TARGET_ADVANCED);
220
+ const classification = classifyHousekeeperBranch(current, policy);
221
+ if (!classification.eligible) reasons.push(...classification.reasonCodes);
222
+ return {
223
+ ok: reasons.length === 0,
224
+ action,
225
+ currentHeadOid: current.headOid,
226
+ currentTargetHeadOid: current.target?.headOid,
227
+ reasonCodes: sortedUnique(reasons),
228
+ };
229
+ }
230
+
231
+ export function createEngineeringHousekeeperReceipt({
232
+ plan,
233
+ outcomes,
234
+ appliedAt,
235
+ }) {
236
+ const orderedOutcomes = [...outcomes].sort((a, b) =>
237
+ stableJson(a).localeCompare(stableJson(b)),
238
+ );
239
+ const body = {
240
+ contract: ENGINEERING_HOUSEKEEPER_CONTRACT,
241
+ schemaVersion: ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION,
242
+ mode: "receipt",
243
+ planRoot: plan.planRoot,
244
+ appliedAt,
245
+ outcomes: orderedOutcomes,
246
+ };
247
+ return { ...body, receiptRoot: engineeringHousekeeperRoot(body) };
248
+ }
249
+
250
+ export function classifyHousekeeperReplay(plan, priorReceipt) {
251
+ const alreadyApplied = priorReceipt?.planRoot === plan.planRoot;
252
+ return {
253
+ alreadyApplied,
254
+ action: alreadyApplied ? "no-op" : "apply",
255
+ reasonCodes: alreadyApplied
256
+ ? [HOUSEKEEPER_REASON_CODES.REPEATED_NO_OP]
257
+ : [],
258
+ };
259
+ }
@@ -287,6 +287,9 @@ export {
287
287
  verifyGithubGovernanceReceipt,
288
288
  } from "./github-governance-authority.js";
289
289
 
290
+ export * from "./engineering-housekeeper.js";
291
+ export * from "./engineering-housekeeper-github.js";
292
+
290
293
  export {
291
294
  ARTIFACT_PASSPORT_LOCATOR_CONTRACT,
292
295
  ARTIFACT_PASSPORT_POINTER_CONTRACT,
@@ -944,26 +944,49 @@ export function normalizeKfd1ContractWorldWitness(witness, { metadata = resolveK
944
944
  };
945
945
  }
946
946
 
947
- function resolveArtifactFile({ cwd, artifactRoot, artifacts = [], artifactPath }) {
948
- const candidates = [
949
- path.resolve(cwd, artifactPath),
950
- artifactRoot ? path.resolve(artifactRoot, artifactPath) : "",
951
- ].filter(Boolean);
947
+ function indexArtifactSearchRoots({ roots = [], artifactPaths = [] } = {}) {
948
+ const wanted = [...new Set(artifactPaths.filter(Boolean))];
949
+ const indexed = new Map();
950
+ const stack = roots.filter((root) => root && fs.existsSync(root) && fs.statSync(root).isDirectory());
951
+ while (stack.length > 0) {
952
+ const current = stack.pop();
953
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
954
+ const candidate = path.join(current, entry.name);
955
+ if (entry.isDirectory()) { stack.push(candidate); continue; }
956
+ if (!entry.isFile()) continue;
957
+ const normalized = candidate.replace(/\\/g, "/");
958
+ for (const artifactPath of wanted) {
959
+ if (normalized === artifactPath || normalized.endsWith(`/${artifactPath}`)) {
960
+ const candidates = indexed.get(artifactPath) || [];
961
+ candidates.push(candidate);
962
+ indexed.set(artifactPath, candidates);
963
+ }
964
+ }
965
+ }
966
+ }
967
+ return indexed;
968
+ }
969
+
970
+ function resolveArtifactFile({ cwd, artifactRoot, artifactIndex = new Map(), artifacts = [], artifactPath, expectedSha256 = "" }) {
971
+ const candidates = artifactIndex.size > 0
972
+ ? [...(artifactIndex.get(artifactPath) || []), artifactRoot ? path.resolve(artifactRoot, artifactPath) : "", path.resolve(cwd, artifactPath)]
973
+ : [path.resolve(cwd, artifactPath), artifactRoot ? path.resolve(artifactRoot, artifactPath) : ""];
952
974
  for (const artifact of artifacts) {
953
975
  const sourcePath = artifact.sourcePath || artifact.path || "";
954
- if (!sourcePath) {
955
- continue;
956
- }
976
+ if (!sourcePath) continue;
957
977
  const normalizedSource = sourcePath.replace(/\\/g, "/");
958
- if (normalizedSource === artifactPath || normalizedSource.endsWith(`/${artifactPath}`)) {
959
- candidates.push(path.resolve(sourcePath));
960
- }
978
+ if (normalizedSource === artifactPath || normalizedSource.endsWith(`/${artifactPath}`)) candidates.push(path.resolve(sourcePath));
961
979
  const name = artifact.name || artifact.filename || "";
962
- if (name && name === artifactPath) {
963
- candidates.push(path.resolve(sourcePath));
964
- }
980
+ if (name && name === artifactPath) candidates.push(path.resolve(sourcePath));
965
981
  }
966
- return candidates.find((candidate) => candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) || "";
982
+ const existing = [...new Set(candidates)].filter(
983
+ (candidate) => candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile(),
984
+ );
985
+ if (expectedSha256) {
986
+ const exact = existing.find((candidate) => sha256File(candidate) === expectedSha256);
987
+ if (exact) return exact;
988
+ }
989
+ return existing[0] || "";
967
990
  }
968
991
 
969
992
  function resolveSourceFile({ cwd, sourcePath }) {
@@ -977,6 +1000,7 @@ function resolveSourceFile({ cwd, sourcePath }) {
977
1000
  export function createKfd1ReleaseGateEvidence({
978
1001
  cwd = process.cwd(),
979
1002
  artifactRoot = "",
1003
+ artifactSearchRoots = [],
980
1004
  artifacts = [],
981
1005
  witnesses = [],
982
1006
  verifiedAt = new Date().toISOString(),
@@ -986,6 +1010,10 @@ export function createKfd1ReleaseGateEvidence({
986
1010
  if (normalizedWitnesses.length === 0) {
987
1011
  return undefined;
988
1012
  }
1013
+ const artifactIndex = indexArtifactSearchRoots({
1014
+ roots: artifactSearchRoots,
1015
+ artifactPaths: normalizedWitnesses.flatMap((witness) => witness.surfaces.map((surface) => surface.artifactPath)),
1016
+ });
989
1017
  const worlds = normalizedWitnesses.map((witness) => {
990
1018
  const preBuildWitnessSha256 = sha256Json(witness);
991
1019
  const sourceResults = witness.surfaces.map((surface) => {
@@ -1025,8 +1053,10 @@ export function createKfd1ReleaseGateEvidence({
1025
1053
  const filePath = resolveArtifactFile({
1026
1054
  cwd,
1027
1055
  artifactRoot,
1056
+ artifactIndex,
1028
1057
  artifacts,
1029
1058
  artifactPath: surface.artifactPath,
1059
+ expectedSha256: surface.expectedSha256,
1030
1060
  });
1031
1061
  if (!filePath) {
1032
1062
  return {
@@ -102,7 +102,19 @@ function safeRelativePath(value, label) {
102
102
  }
103
103
 
104
104
  function fileDigest(filePath) {
105
- return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
105
+ const hash = crypto.createHash("sha256");
106
+ const descriptor = fs.openSync(filePath, "r");
107
+ const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
108
+ try {
109
+ for (;;) {
110
+ const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
111
+ if (count === 0) break;
112
+ hash.update(buffer.subarray(0, count));
113
+ }
114
+ } finally {
115
+ fs.closeSync(descriptor);
116
+ }
117
+ return `sha256:${hash.digest("hex")}`;
106
118
  }
107
119
 
108
120
  function rootedPayload(value) {
@@ -146,6 +146,15 @@ function invariantSemanticPreimage(passport) {
146
146
  return value;
147
147
  }
148
148
 
149
+ function hasCompleteInvariantCoverage(value) {
150
+ const coverage = value?.coverage;
151
+ return Number.isInteger(coverage?.required)
152
+ && coverage.required > 0
153
+ && coverage.verified === coverage.required
154
+ && [coverage.missing, coverage.falsified, coverage.unqualified]
155
+ .every((items) => Array.isArray(items) && items.length === 0);
156
+ }
157
+
149
158
  function normalizeInvariantPassport(meta, index = 0) {
150
159
  const value = meta?.value;
151
160
  const label = `invariantPassportJsons[${index}]`;
@@ -165,11 +174,18 @@ function normalizeInvariantPassport(meta, index = 0) {
165
174
  if (value.passportRoot !== expectedRoot) {
166
175
  throw new Error(`${label}.passportRoot mismatch: expected ${expectedRoot}, got ${value.passportRoot}`);
167
176
  }
168
- if (value.verdict !== "verified") {
169
- throw new Error(`${label}.verdict must be verified, got ${value.verdict}`);
170
- }
171
- if (value.coverage?.complete !== true) {
172
- throw new Error(`${label}.coverage.complete must be true`);
177
+ const verifiedPassport = value.verdict === "verified" && value.coverage?.complete === true;
178
+ const scopedUnqualifiedClaim = value.verdict === "unqualified"
179
+ && value.coverage?.complete === false
180
+ && hasCompleteInvariantCoverage(value)
181
+ && value.releaseClaims?.verdict === "unqualified"
182
+ && /^sha256:[0-9a-f]{64}$/.test(optionalString(value.releaseClaims?.claimRoot))
183
+ && Array.isArray(value.releaseClaims?.diagnostics)
184
+ && value.releaseClaims.diagnostics.length > 0
185
+ && Array.isArray(value.diagnostics)
186
+ && value.diagnostics.length === 0;
187
+ if (!verifiedPassport && !scopedUnqualifiedClaim) {
188
+ throw new Error(`${label}.verdict must be verified, or unqualified only for complete invariant coverage with one explicit unqualified releaseClaims section`);
173
189
  }
174
190
  if (value.source?.dirty !== false) {
175
191
  throw new Error(`${label}.source.dirty must be false`);
@@ -182,6 +198,9 @@ function normalizeInvariantPassport(meta, index = 0) {
182
198
  : [];
183
199
  if (platforms.length === 0) throw new Error(`${label}.coverage.platforms must be non-empty`);
184
200
  if (!Array.isArray(value.residualRisk)) throw new Error(`${label}.residualRisk must be an array`);
201
+ const releaseClaimRisks = scopedUnqualifiedClaim
202
+ ? value.releaseClaims.diagnostics.map((entry) => `Exit/provider claim ${optionalString(entry?.code) || "unqualified"}: ${optionalString(entry?.message) || "consumer claim remains unqualified"}`)
203
+ : [];
185
204
  return {
186
205
  path: optionalString(meta.path),
187
206
  sha256: optionalString(meta.sha256),
@@ -194,7 +213,15 @@ function normalizeInvariantPassport(meta, index = 0) {
194
213
  source: structuredClone(value.source),
195
214
  coverage: structuredClone(value.coverage),
196
215
  platforms,
197
- residualRisk: structuredClone(value.residualRisk),
216
+ admission: {
217
+ scope: "consumer-invariant-coverage",
218
+ result: "passed",
219
+ consumerVerdict: value.verdict,
220
+ releaseClaimsVerdict: optionalString(value.releaseClaims?.verdict),
221
+ },
222
+ releaseClaims: value.releaseClaims ? structuredClone(value.releaseClaims) : undefined,
223
+ diagnostics: Array.isArray(value.diagnostics) ? structuredClone(value.diagnostics) : [],
224
+ residualRisk: [...new Set([...value.residualRisk, ...releaseClaimRisks])].sort(),
198
225
  };
199
226
  }
200
227
 
@@ -533,7 +560,12 @@ function mergeAuthoritativeImpactBase(impact, basePassport = undefined) {
533
560
  return merged;
534
561
  }
535
562
 
536
- function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "command" } = {}) {
563
+ function parseJsonCommandOutput({
564
+ command = "",
565
+ cwd = process.cwd(),
566
+ label = "command",
567
+ acceptNonzeroJson = undefined,
568
+ } = {}) {
537
569
  const normalized = String(command || "").trim();
538
570
  if (!normalized) {
539
571
  return { value: undefined, path: "", sha256: "" };
@@ -547,7 +579,7 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
547
579
  if (result.error) {
548
580
  throw new Error(`${label} failed to start: ${result.error.message}`);
549
581
  }
550
- if (result.status !== 0) {
582
+ if (result.status !== 0 && typeof acceptNonzeroJson !== "function") {
551
583
  const stderr = String(result.stderr || "").trim();
552
584
  throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
553
585
  }
@@ -561,10 +593,15 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
561
593
  } catch (error) {
562
594
  throw new Error(`${label} output must be valid JSON: ${error.message}`, { cause: error });
563
595
  }
596
+ if (result.status !== 0 && !acceptNonzeroJson({ status: result.status, value: parsed })) {
597
+ const stderr = String(result.stderr || "").trim();
598
+ throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
599
+ }
564
600
  return {
565
601
  value: parsed,
566
602
  path: "",
567
603
  sha256: sha256Text(stableJson(parsed)),
604
+ status: result.status,
568
605
  };
569
606
  }
570
607
 
@@ -879,6 +916,41 @@ function arrayOrSingleton(value) {
879
916
  return value && typeof value === "object" ? [value] : [];
880
917
  }
881
918
 
919
+ const KFD2_RESIDUAL_RISK_SCHEMA = "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk";
920
+
921
+ function normalizeKfd2ResidualRisk(entry, { claim = {}, claimIndex = 0, riskIndex = 0 } = {}) {
922
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
923
+ return entry;
924
+ }
925
+ const reason = optionalString(entry).trim();
926
+ if (!reason) {
927
+ return entry;
928
+ }
929
+ const claimId = optionalString(claim.id || `claim-${claimIndex + 1}`)
930
+ .toLowerCase()
931
+ .replace(/[^a-z0-9]+/g, "-")
932
+ .replace(/^-+|-+$/g, "") || `claim-${claimIndex + 1}`;
933
+ const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility)
934
+ ? claim.responsibility
935
+ : {};
936
+ return {
937
+ id: `${claimId}-residual-risk-${riskIndex + 1}`,
938
+ definedBy: KFD2_RESIDUAL_RISK_SCHEMA,
939
+ riskType: "manual-review-risk",
940
+ trustImpact: "downgrade-warning",
941
+ machineProvability: "not-machine-verifiable",
942
+ agentAction: "request-maintainer-review",
943
+ reason,
944
+ owner: optionalString(
945
+ responsibility.releaseDecisionOwner
946
+ || responsibility.owner
947
+ || responsibility.sourceOwner
948
+ || responsibility.sourceContractOwner
949
+ || "release maintainer",
950
+ ),
951
+ };
952
+ }
953
+
882
954
  function normalizeKfd2Claim(raw = {}, index = 0) {
883
955
  const claim = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
884
956
  const sourceBindings = arrayOrEmpty(claim.sourceBindings || claim.source_bindings || claim.sources || claim.declaredSources);
@@ -892,10 +964,13 @@ function normalizeKfd2Claim(raw = {}, index = 0) {
892
964
  ? claim.audit_boundary
893
965
  : {};
894
966
  const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility) ? claim.responsibility : {};
895
- const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(entry, {
896
- kind: "residualRisk",
897
- label: `kfd-2.claims[${index}].residualRisk[${riskIndex}]`,
898
- }));
967
+ const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(
968
+ normalizeKfd2ResidualRisk(entry, { claim, claimIndex: index, riskIndex }),
969
+ {
970
+ kind: "residualRisk",
971
+ label: `kfd-2.claims[${index}].residualRisk[${riskIndex}]`,
972
+ },
973
+ ));
899
974
  const downgradeReasons = arrayOrSingleton(claim.downgradeReasons || claim.downgrade_reasons || claim.downgradeReason || claim.downgrade_reason)
900
975
  .map((entry, reasonIndex) => validateKfd2TrustTaxonomyEntry(entry, {
901
976
  kind: "downgradeReason",
@@ -1494,6 +1569,7 @@ export function collectGitHubReleasePassport({
1494
1569
  outputDir = ".buildchain/release-passport",
1495
1570
  assetsJson = "",
1496
1571
  assetsDir = "",
1572
+ kfdArtifactSearchRoots = [],
1497
1573
  releaseJson = "",
1498
1574
  productName = "Buildchain",
1499
1575
  packageName = "@kungfu-tech/buildchain",
@@ -1568,6 +1644,11 @@ export function collectGitHubReleasePassport({
1568
1644
  .filter(Boolean)
1569
1645
  .map((witnessJson) => parseJsonInputWithMeta(witnessJson, undefined, { cwd, label: "kfd3ArtifactWitnessJsons entry" }))
1570
1646
  .filter((meta) => meta.value);
1647
+ const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
1648
+ command: kfd3ArtifactVerifyCommand,
1649
+ cwd,
1650
+ label: "KFD-3 artifact verify command",
1651
+ });
1571
1652
  const kfdSupportMatrixMeta = parseJsonInputWithMeta(
1572
1653
  kfdSupportMatrixJson,
1573
1654
  undefined,
@@ -1578,11 +1659,6 @@ export function collectGitHubReleasePassport({
1578
1659
  .map((gateJson) => parseJsonInputWithMeta(gateJson, undefined, { cwd, label: "kfdProductGateJsons entry" }))
1579
1660
  .filter((meta) => meta.value);
1580
1661
  const basePassportMeta = parseJsonInputWithMeta(basePassportJson, undefined, { cwd, label: "basePassportJson" });
1581
- const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
1582
- command: kfd3ArtifactVerifyCommand,
1583
- cwd,
1584
- label: "KFD-3 artifact verify command",
1585
- });
1586
1662
  const invariantPassportMetas = (invariantPassportJsons || [])
1587
1663
  .filter(Boolean)
1588
1664
  .map((passportJson) => parseJsonInputWithMeta(passportJson, undefined, { cwd, label: "invariantPassportJsons entry" }))
@@ -1591,6 +1667,22 @@ export function collectGitHubReleasePassport({
1591
1667
  command: invariantPassportCommand,
1592
1668
  cwd,
1593
1669
  label: "invariant passport command",
1670
+ acceptNonzeroJson: ({ status, value }) => {
1671
+ if (status !== 2) return false;
1672
+ try {
1673
+ const normalized = normalizeInvariantPassport(
1674
+ { value, path: "", sha256: sha256Text(stableJson(value)) },
1675
+ invariantPassportMetas.length,
1676
+ );
1677
+ return normalized.verdict === "unqualified"
1678
+ && normalized.coverage?.complete === false
1679
+ && normalized.releaseClaims?.verdict === "unqualified"
1680
+ && normalized.admission?.scope === "consumer-invariant-coverage"
1681
+ && normalized.admission?.result === "passed";
1682
+ } catch {
1683
+ return false;
1684
+ }
1685
+ },
1594
1686
  });
1595
1687
  if (invariantPassportCommandMeta.value) invariantPassportMetas.push(invariantPassportCommandMeta);
1596
1688
  const invariantPassports = createInvariantPassportGate(invariantPassportMetas);
@@ -1664,6 +1756,7 @@ export function collectGitHubReleasePassport({
1664
1756
  const kfd1 = createKfd1ReleaseGateEvidence({
1665
1757
  cwd,
1666
1758
  artifactRoot: assetsDir ? path.resolve(cwd, assetsDir) : "",
1759
+ artifactSearchRoots: kfdArtifactSearchRoots.map((root) => path.resolve(cwd, root)),
1667
1760
  artifacts: assets,
1668
1761
  witnesses: kfd1WitnessMetas.map((meta) => meta.value),
1669
1762
  });
@@ -2092,7 +2185,17 @@ function validateReleaseEvidenceContracts({
2092
2185
  for (const [index, value] of (passport?.githubArtifactAttestations || []).entries()) {
2093
2186
  try {
2094
2187
  const policy = normalizeGitHubArtifactAttestationPolicy(value);
2095
- if (policy.caller.sourceSha !== String(passport?.release?.sourceSha || "").toLowerCase()) {
2188
+ const release = passport?.release || {};
2189
+ const acceptedSourceShas = new Set([String(release.sourceSha || "").toLowerCase()]);
2190
+ if (
2191
+ release.treeEquivalent === true
2192
+ && release.builtSourceTreeSha
2193
+ && release.builtSourceTreeSha === release.promotionChannelTreeSha
2194
+ && release.builtSourceSha
2195
+ ) {
2196
+ acceptedSourceShas.add(String(release.builtSourceSha).toLowerCase());
2197
+ }
2198
+ if (!acceptedSourceShas.has(policy.caller.sourceSha)) {
2096
2199
  issues.push(issue(
2097
2200
  "error",
2098
2201
  `githubArtifactAttestations[${index}].caller.sourceSha`,
@@ -2490,8 +2593,15 @@ function validatePassportTrustSections({ passport, issues }) {
2490
2593
  ].filter(Boolean));
2491
2594
  for (const [index, entry] of (section.passports || []).entries()) {
2492
2595
  const prefix = `invariantPassports.passports[${index}]`;
2493
- if (entry.verdict !== "verified") issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified`));
2494
- if (entry.coverage?.complete !== true) issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage.complete must be true`));
2596
+ if (entry.admission?.scope !== "consumer-invariant-coverage" || entry.admission?.result !== "passed") {
2597
+ issues.push(issue("error", `${prefix}.admission`, `${prefix}.admission must record passed consumer invariant coverage`));
2598
+ }
2599
+ if (entry.verdict !== "verified" && !(entry.verdict === "unqualified" && entry.releaseClaims?.verdict === "unqualified")) {
2600
+ issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified or preserve one explicit unqualified releaseClaims verdict`));
2601
+ }
2602
+ if (entry.coverage?.complete !== true && !hasCompleteInvariantCoverage(entry)) {
2603
+ issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage must be complete or prove every required invariant coordinate verified`));
2604
+ }
2495
2605
  if (entry.source?.dirty !== false) issues.push(issue("error", `${prefix}.source.dirty`, `${prefix}.source.dirty must be false`));
2496
2606
  if (acceptedSourceShas.size > 0 && !acceptedSourceShas.has(entry.source?.revision)) {
2497
2607
  issues.push(issue("error", `${prefix}.source.revision`, `${prefix}.source.revision must match a release source identity`));
@@ -173,7 +173,7 @@ async function main() {
173
173
  artifactDigest: artifactSet.manifestSetDigest,
174
174
  nonce: `${required("GITHUB_RUN_ID")}:${required("GITHUB_RUN_ATTEMPT")}:${sourceSha}`,
175
175
  issuedAt: issuedAt.toISOString(),
176
- expiresAt: new Date(issuedAt.getTime() + 10 * 60 * 1000).toISOString(),
176
+ expiresAt: new Date(issuedAt.getTime() + 15 * 60 * 1000).toISOString(),
177
177
  qualification: {
178
178
  required: qualificationRequired,
179
179
  predicateId: process.env.BUILDCHAIN_CONSUMER_PREDICATE_ID || "",
@@ -453,7 +453,7 @@ function main() {
453
453
  const reviewRules = (environmentState.protection_rules || []).filter((rule) => rule.type === "required_reviewers");
454
454
  const runsOn = (block.match(/^\s{4}runs-on:\s*([^\n#]+)/m)?.[1] || "").trim().replace(/["']/g, "");
455
455
  const observedAt = new Date();
456
- const expiresAt = new Date(observedAt.getTime() + 10 * 60 * 1000);
456
+ const expiresAt = new Date(observedAt.getTime() + 15 * 60 * 1000);
457
457
  const receipt = evaluatePublicationControlPlaneSnapshot({
458
458
  repository,
459
459
  workflowPath,