@kungfu-tech/buildchain 2.11.13 → 2.11.14-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.
- package/bin/buildchain.mjs +32 -1
- package/dist/site/buildchain-contract.json +5 -5
- package/dist/site/buildchain-site.json +13 -13
- package/dist/site/capability-registry.json +1 -1
- package/dist/site/cli-registry.json +12 -0
- package/dist/site/kfd-claims.json +19 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +6 -6
- package/dist/site/page-registry.json +6 -6
- package/dist/site/public-surface-audit.json +39 -4
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/docs/MAP.md +1 -1
- package/docs/cli.md +12 -1
- package/docs/kfd-support.md +33 -0
- package/package.json +1 -1
- package/packages/core/buildchain-layout.js +11 -0
- package/packages/core/index.js +12 -0
- package/packages/core/kfd.js +16 -1
- package/packages/core/kfd2-product-claims.js +454 -0
- package/scripts/generate-site-bundle.mjs +1 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
BUILDCHAIN_KFD2_CLAIMS_DIR,
|
|
8
|
+
BUILDCHAIN_KFD2_CLAIM_ARGS_PATH,
|
|
9
|
+
BUILDCHAIN_KFD2_DIR,
|
|
10
|
+
BUILDCHAIN_KFD2_REGISTRY_PATH,
|
|
11
|
+
BUILDCHAIN_KFD2_RELEASE_CLAIMS_PATH,
|
|
12
|
+
toPosixPath,
|
|
13
|
+
} from "./buildchain-layout.js";
|
|
14
|
+
import {
|
|
15
|
+
discoverConfiguredVersionStateFiles,
|
|
16
|
+
loadBuildchainConfig,
|
|
17
|
+
} from "./buildchain-config.js";
|
|
18
|
+
|
|
19
|
+
export const KFD2_PRODUCT_CLAIMS_REGISTRY_CONTRACT =
|
|
20
|
+
"kungfu-buildchain-kfd-2-product-claims-registry/v1";
|
|
21
|
+
export const KFD2_PRODUCT_CLAIMS_VALIDATION_CONTRACT =
|
|
22
|
+
"kungfu-buildchain-kfd-2-product-claims-registry-validation";
|
|
23
|
+
export const KFD2_PRODUCT_CLAIMS_OUTPUT_CONTRACT =
|
|
24
|
+
"kungfu-buildchain-kfd-2-product-claims-output";
|
|
25
|
+
|
|
26
|
+
const CLAIM_ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
27
|
+
const CLAIM_STATUSES = new Set([
|
|
28
|
+
"declared",
|
|
29
|
+
"audited",
|
|
30
|
+
"enforced",
|
|
31
|
+
"not-applicable",
|
|
32
|
+
]);
|
|
33
|
+
const ENUMERABILITY = new Set([
|
|
34
|
+
"closed-world",
|
|
35
|
+
"declared-open",
|
|
36
|
+
"sampled",
|
|
37
|
+
"manual",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function issue(code, message, pathValue = "") {
|
|
41
|
+
return {
|
|
42
|
+
level: "error",
|
|
43
|
+
code,
|
|
44
|
+
message,
|
|
45
|
+
...(pathValue ? { path: pathValue } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderJson(value) {
|
|
50
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sha256Buffer(value) {
|
|
54
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sha256File(filePath) {
|
|
58
|
+
return sha256Buffer(fs.readFileSync(filePath));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readJsonFile(filePath) {
|
|
62
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveRepoPath(cwd, relativePath, label = "path") {
|
|
66
|
+
if (!relativePath || typeof relativePath !== "string") {
|
|
67
|
+
throw new Error(`${label} must be a repository-relative path`);
|
|
68
|
+
}
|
|
69
|
+
const root = path.resolve(cwd);
|
|
70
|
+
const resolved = path.resolve(root, relativePath);
|
|
71
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
|
|
72
|
+
throw new Error(`${label} escapes repository root: ${relativePath}`);
|
|
73
|
+
}
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function repoRelative(cwd, filePath) {
|
|
78
|
+
return toPosixPath(path.relative(path.resolve(cwd), path.resolve(filePath)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function gitHead(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
84
|
+
cwd,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
87
|
+
}).trim();
|
|
88
|
+
} catch {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function dottedValue(value, key) {
|
|
94
|
+
return String(key || "")
|
|
95
|
+
.split(".")
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.reduce((current, part) => current?.[part], value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function configuredVersion(cwd) {
|
|
101
|
+
const loaded = loadBuildchainConfig(cwd);
|
|
102
|
+
const files = discoverConfiguredVersionStateFiles(cwd, loaded);
|
|
103
|
+
const versions = files.map((file) => {
|
|
104
|
+
if (file.type === "json" || file.type === "toml") {
|
|
105
|
+
return dottedValue(file.content, file.key);
|
|
106
|
+
}
|
|
107
|
+
return file.source.match(file.pattern)?.groups?.version || "";
|
|
108
|
+
}).filter(Boolean);
|
|
109
|
+
if (new Set(versions).size > 1) {
|
|
110
|
+
throw new Error(`configured version files disagree: ${versions.join(", ")}`);
|
|
111
|
+
}
|
|
112
|
+
if (versions.length > 0) return versions[0];
|
|
113
|
+
for (const candidate of ["package.json", "lerna.json"]) {
|
|
114
|
+
const candidatePath = path.join(cwd, candidate);
|
|
115
|
+
if (!fs.existsSync(candidatePath)) continue;
|
|
116
|
+
const value = readJsonFile(candidatePath).version;
|
|
117
|
+
if (typeof value === "string" && value) return value;
|
|
118
|
+
}
|
|
119
|
+
return "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function pointerFor(cwd, input, label) {
|
|
123
|
+
const filePath = resolveRepoPath(cwd, input?.path, label);
|
|
124
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
125
|
+
throw new Error(`${label} does not name a readable file: ${input?.path || ""}`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
kind: input.kind || "file",
|
|
129
|
+
path: repoRelative(cwd, filePath),
|
|
130
|
+
sha256: sha256File(filePath),
|
|
131
|
+
...(input.schemaId ? { schemaId: input.schemaId } : {}),
|
|
132
|
+
...(input.digest ? { digest: input.digest } : {}),
|
|
133
|
+
...(input.specifier ? { specifier: input.specifier } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readKfd2ProductClaimsRegistry({
|
|
138
|
+
cwd = process.cwd(),
|
|
139
|
+
registryPath = BUILDCHAIN_KFD2_REGISTRY_PATH,
|
|
140
|
+
} = {}) {
|
|
141
|
+
const resolvedPath = resolveRepoPath(cwd, registryPath, "registryPath");
|
|
142
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
143
|
+
throw new Error(`KFD-2 product claims registry not found: ${registryPath}`);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
path: repoRelative(cwd, resolvedPath),
|
|
147
|
+
sha256: sha256File(resolvedPath),
|
|
148
|
+
registry: readJsonFile(resolvedPath),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function validateKfd2ProductClaimsRegistry(registry = {}) {
|
|
153
|
+
const issues = [];
|
|
154
|
+
if (registry?.schema !== KFD2_PRODUCT_CLAIMS_REGISTRY_CONTRACT) {
|
|
155
|
+
issues.push(issue(
|
|
156
|
+
"kfd-2.product-registry.schema",
|
|
157
|
+
`registry.schema must be ${KFD2_PRODUCT_CLAIMS_REGISTRY_CONTRACT}`,
|
|
158
|
+
"schema",
|
|
159
|
+
));
|
|
160
|
+
}
|
|
161
|
+
if (registry?.kfd?.standard !== "kfd-2") {
|
|
162
|
+
issues.push(issue("kfd-2.product-registry.standard", "registry.kfd.standard must be kfd-2", "kfd.standard"));
|
|
163
|
+
}
|
|
164
|
+
if (registry?.kfd?.contract !== "kfd-2-release-claims") {
|
|
165
|
+
issues.push(issue(
|
|
166
|
+
"kfd-2.product-registry.contract",
|
|
167
|
+
"registry.kfd.contract must be kfd-2-release-claims",
|
|
168
|
+
"kfd.contract",
|
|
169
|
+
));
|
|
170
|
+
}
|
|
171
|
+
if (!registry?.product?.name) {
|
|
172
|
+
issues.push(issue("kfd-2.product-registry.product", "registry.product.name is required", "product.name"));
|
|
173
|
+
}
|
|
174
|
+
const claims = Array.isArray(registry?.claims) ? registry.claims : [];
|
|
175
|
+
if (claims.length === 0) {
|
|
176
|
+
issues.push(issue("kfd-2.product-registry.claims", "registry.claims must be a non-empty array", "claims"));
|
|
177
|
+
}
|
|
178
|
+
const ids = new Set();
|
|
179
|
+
for (const [index, claim] of claims.entries()) {
|
|
180
|
+
const label = `claims[${index}]`;
|
|
181
|
+
if (!CLAIM_ID_RE.test(String(claim?.id || ""))) {
|
|
182
|
+
issues.push(issue("kfd-2.product-registry.claim-id", `${label}.id must match ${CLAIM_ID_RE}`, `${label}.id`));
|
|
183
|
+
} else if (ids.has(claim.id)) {
|
|
184
|
+
issues.push(issue("kfd-2.product-registry.duplicate-claim", `${label}.id duplicates ${claim.id}`, `${label}.id`));
|
|
185
|
+
}
|
|
186
|
+
ids.add(claim?.id);
|
|
187
|
+
if (!claim?.statement) issues.push(issue("kfd-2.product-registry.statement", `${label}.statement is required`, `${label}.statement`));
|
|
188
|
+
if (!claim?.source?.path) issues.push(issue("kfd-2.product-registry.source", `${label}.source.path is required`, `${label}.source.path`));
|
|
189
|
+
if (!Array.isArray(claim?.evidence) || claim.evidence.length === 0) {
|
|
190
|
+
issues.push(issue("kfd-2.product-registry.evidence", `${label}.evidence must be non-empty`, `${label}.evidence`));
|
|
191
|
+
}
|
|
192
|
+
if (!Array.isArray(claim?.artifacts) || claim.artifacts.length === 0) {
|
|
193
|
+
issues.push(issue("kfd-2.product-registry.artifacts", `${label}.artifacts must be non-empty`, `${label}.artifacts`));
|
|
194
|
+
}
|
|
195
|
+
if (!claim?.auditBoundary?.scope) {
|
|
196
|
+
issues.push(issue("kfd-2.product-registry.audit-boundary", `${label}.auditBoundary.scope is required`, `${label}.auditBoundary.scope`));
|
|
197
|
+
}
|
|
198
|
+
if (!ENUMERABILITY.has(String(claim?.auditBoundary?.enumerability || ""))) {
|
|
199
|
+
issues.push(issue("kfd-2.product-registry.enumerability", `${label}.auditBoundary.enumerability is invalid`, `${label}.auditBoundary.enumerability`));
|
|
200
|
+
}
|
|
201
|
+
if (!Array.isArray(claim?.residualRisk)) {
|
|
202
|
+
issues.push(issue("kfd-2.product-registry.residual-risk", `${label}.residualRisk must be an array`, `${label}.residualRisk`));
|
|
203
|
+
}
|
|
204
|
+
for (const key of ["sourceOwner", "verificationOwner", "releaseDecisionOwner"]) {
|
|
205
|
+
if (!claim?.responsibility?.[key]) {
|
|
206
|
+
issues.push(issue("kfd-2.product-registry.responsibility", `${label}.responsibility.${key} is required`, `${label}.responsibility.${key}`));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!CLAIM_STATUSES.has(String(claim?.status || ""))) {
|
|
210
|
+
issues.push(issue("kfd-2.product-registry.status", `${label}.status is invalid`, `${label}.status`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
schemaVersion: 1,
|
|
215
|
+
contract: KFD2_PRODUCT_CLAIMS_VALIDATION_CONTRACT,
|
|
216
|
+
ok: issues.length === 0,
|
|
217
|
+
claimCount: claims.length,
|
|
218
|
+
issues,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function assertValidRegistry(registry) {
|
|
223
|
+
const validation = validateKfd2ProductClaimsRegistry(registry);
|
|
224
|
+
if (!validation.ok) {
|
|
225
|
+
throw new Error(`KFD-2 product claims registry is invalid:\n${validation.issues.map((entry) => `- ${entry.path || entry.code}: ${entry.message}`).join("\n")}`);
|
|
226
|
+
}
|
|
227
|
+
return validation;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function canonicalClaim(cwd, claim, defaultCheckCommand) {
|
|
231
|
+
const source = pointerFor(cwd, claim.source, `${claim.id}.source`);
|
|
232
|
+
return {
|
|
233
|
+
id: claim.id,
|
|
234
|
+
statement: claim.statement,
|
|
235
|
+
category: claim.category || "kfd-2",
|
|
236
|
+
source,
|
|
237
|
+
evidence: claim.evidence.map((entry, index) => ({
|
|
238
|
+
type: entry.type || "file",
|
|
239
|
+
pointer: pointerFor(cwd, { ...entry, kind: entry.kind || "file" }, `${claim.id}.evidence[${index}]`),
|
|
240
|
+
description: entry.description || "",
|
|
241
|
+
})),
|
|
242
|
+
verification: {
|
|
243
|
+
command: claim.verification?.command || defaultCheckCommand,
|
|
244
|
+
expectedResult: claim.verification?.expectedResult || "pass",
|
|
245
|
+
},
|
|
246
|
+
auditBoundary: {
|
|
247
|
+
scope: claim.auditBoundary.scope,
|
|
248
|
+
enumerability: claim.auditBoundary.enumerability,
|
|
249
|
+
exclusions: claim.auditBoundary.exclusions || [],
|
|
250
|
+
},
|
|
251
|
+
residualRisk: claim.residualRisk,
|
|
252
|
+
responsibility: claim.responsibility,
|
|
253
|
+
status: claim.status,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function passportClaim(cwd, claim, registrySha256, defaultCheckCommand) {
|
|
258
|
+
const source = pointerFor(cwd, claim.source, `${claim.id}.source`);
|
|
259
|
+
const machineEvidence = claim.evidence.map((entry, index) => ({
|
|
260
|
+
type: entry.type || "file",
|
|
261
|
+
pointer: pointerFor(cwd, { ...entry, kind: entry.kind || "file" }, `${claim.id}.evidence[${index}]`),
|
|
262
|
+
description: entry.description || "",
|
|
263
|
+
}));
|
|
264
|
+
const artifacts = claim.artifacts.map((artifact, index) => {
|
|
265
|
+
const pointer = pointerFor(cwd, { kind: "file", path: artifact.path }, `${claim.id}.artifacts[${index}]`);
|
|
266
|
+
return {
|
|
267
|
+
name: artifact.name || path.basename(artifact.path),
|
|
268
|
+
path: pointer.path,
|
|
269
|
+
sha256: pointer.sha256,
|
|
270
|
+
expectedPackagePath: artifact.expectedPackagePath || "",
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
id: claim.id,
|
|
275
|
+
public: true,
|
|
276
|
+
claim: claim.statement,
|
|
277
|
+
sourceBindings: [{
|
|
278
|
+
role: "claim-source",
|
|
279
|
+
kind: source.kind,
|
|
280
|
+
path: source.path,
|
|
281
|
+
sha256: source.sha256,
|
|
282
|
+
}],
|
|
283
|
+
machineEvidence,
|
|
284
|
+
hashes: {
|
|
285
|
+
registrySha256,
|
|
286
|
+
sourceSha256: source.sha256,
|
|
287
|
+
evidenceSha256: machineEvidence.map((entry) => ({
|
|
288
|
+
path: entry.pointer.path,
|
|
289
|
+
sha256: entry.pointer.sha256,
|
|
290
|
+
})),
|
|
291
|
+
artifactSha256: artifacts.map((entry) => ({ path: entry.path, sha256: entry.sha256 })),
|
|
292
|
+
},
|
|
293
|
+
artifacts,
|
|
294
|
+
verification: {
|
|
295
|
+
result: claim.residualRisk.length === 0 ? "passed" : "passed-with-residual-risk",
|
|
296
|
+
command: claim.verification?.command || defaultCheckCommand,
|
|
297
|
+
expectedResult: claim.verification?.expectedResult || "pass",
|
|
298
|
+
},
|
|
299
|
+
auditBoundary: {
|
|
300
|
+
scope: claim.auditBoundary.scope,
|
|
301
|
+
enumerability: claim.auditBoundary.enumerability,
|
|
302
|
+
exclusions: claim.auditBoundary.exclusions || [],
|
|
303
|
+
},
|
|
304
|
+
responsibility: claim.responsibility,
|
|
305
|
+
residualRisk: claim.residualRisk,
|
|
306
|
+
canonicalStatus: claim.status,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function outputPaths(cwd, outputDir, claimIds) {
|
|
311
|
+
const resolvedOutputDir = resolveRepoPath(cwd, outputDir, "outputDir");
|
|
312
|
+
return {
|
|
313
|
+
outputDir: resolvedOutputDir,
|
|
314
|
+
releaseClaims: path.join(resolvedOutputDir, path.basename(BUILDCHAIN_KFD2_RELEASE_CLAIMS_PATH)),
|
|
315
|
+
claimArgs: path.join(resolvedOutputDir, path.basename(BUILDCHAIN_KFD2_CLAIM_ARGS_PATH)),
|
|
316
|
+
claimsDir: path.join(resolvedOutputDir, path.basename(BUILDCHAIN_KFD2_CLAIMS_DIR)),
|
|
317
|
+
claims: claimIds.map((id) => path.join(resolvedOutputDir, path.basename(BUILDCHAIN_KFD2_CLAIMS_DIR), `${id}.json`)),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function renderKfd2ProductClaimOutputs({
|
|
322
|
+
cwd = process.cwd(),
|
|
323
|
+
registryPath = BUILDCHAIN_KFD2_REGISTRY_PATH,
|
|
324
|
+
outputDir = BUILDCHAIN_KFD2_DIR,
|
|
325
|
+
version = "",
|
|
326
|
+
channel = "",
|
|
327
|
+
tag = "",
|
|
328
|
+
sourceSha = "",
|
|
329
|
+
} = {}) {
|
|
330
|
+
const source = readKfd2ProductClaimsRegistry({ cwd, registryPath });
|
|
331
|
+
const validation = assertValidRegistry(source.registry);
|
|
332
|
+
const releaseVersion = version || source.registry.releaseDefaults?.version || configuredVersion(cwd);
|
|
333
|
+
if (!releaseVersion) throw new Error("KFD-2 product claims require a release version");
|
|
334
|
+
const defaultCheckCommand = source.registry.buildchain?.checkCommand || "buildchain kfd 2 product-claims check";
|
|
335
|
+
const releaseClaims = {
|
|
336
|
+
schemaVersion: 1,
|
|
337
|
+
contract: source.registry.kfd.contract,
|
|
338
|
+
standard: source.registry.kfd.standard,
|
|
339
|
+
product: source.registry.product,
|
|
340
|
+
release: {
|
|
341
|
+
version: releaseVersion,
|
|
342
|
+
channel: channel || source.registry.releaseDefaults?.channel || "local",
|
|
343
|
+
tag: tag || `${source.registry.releaseDefaults?.tagPrefix || "v"}${releaseVersion}`,
|
|
344
|
+
sourceSha: sourceSha || source.registry.releaseDefaults?.sourceSha || gitHead(cwd) || "unknown",
|
|
345
|
+
},
|
|
346
|
+
claims: source.registry.claims.map((claim) => canonicalClaim(cwd, claim, defaultCheckCommand)),
|
|
347
|
+
schemaEvolution: {
|
|
348
|
+
interfaceVersion: source.registry.kfd.interfaceVersion || 1,
|
|
349
|
+
compatibilityRule: "Compatible additions may keep schemaVersion 1; required-field or semantic changes require a new KFD-owned interface version.",
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
const claims = source.registry.claims.map((claim) => passportClaim(
|
|
353
|
+
cwd,
|
|
354
|
+
claim,
|
|
355
|
+
source.sha256,
|
|
356
|
+
defaultCheckCommand,
|
|
357
|
+
));
|
|
358
|
+
const paths = outputPaths(cwd, outputDir, claims.map((claim) => claim.id));
|
|
359
|
+
const claimArgs = `${paths.claims.map((claimPath) => `--kfd-2-claim-json ${repoRelative(cwd, claimPath)}`).join("\n")}\n`;
|
|
360
|
+
const files = [
|
|
361
|
+
{ path: repoRelative(cwd, paths.releaseClaims), content: renderJson(releaseClaims) },
|
|
362
|
+
...claims.map((claim, index) => ({ path: repoRelative(cwd, paths.claims[index]), content: renderJson(claim) })),
|
|
363
|
+
{ path: repoRelative(cwd, paths.claimArgs), content: claimArgs },
|
|
364
|
+
];
|
|
365
|
+
return {
|
|
366
|
+
schemaVersion: 1,
|
|
367
|
+
contract: KFD2_PRODUCT_CLAIMS_OUTPUT_CONTRACT,
|
|
368
|
+
ok: true,
|
|
369
|
+
registry: {
|
|
370
|
+
path: source.path,
|
|
371
|
+
sha256: source.sha256,
|
|
372
|
+
contract: source.registry.schema,
|
|
373
|
+
},
|
|
374
|
+
validation,
|
|
375
|
+
releaseClaims,
|
|
376
|
+
claims,
|
|
377
|
+
outputDir: repoRelative(cwd, paths.outputDir),
|
|
378
|
+
files,
|
|
379
|
+
summary: {
|
|
380
|
+
claimCount: claims.length,
|
|
381
|
+
releaseClaimsSha256: sha256Buffer(Buffer.from(renderJson(releaseClaims))),
|
|
382
|
+
passportClaimStatuses: claims.map((claim) => ({
|
|
383
|
+
id: claim.id,
|
|
384
|
+
status: claim.residualRisk.length === 0 ? "passed" : "downgraded",
|
|
385
|
+
residualRisk: claim.residualRisk.length,
|
|
386
|
+
})),
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function compareOutputs(cwd, rendered) {
|
|
392
|
+
const issues = [];
|
|
393
|
+
const expectedPaths = new Set(rendered.files.map((entry) => entry.path));
|
|
394
|
+
for (const entry of rendered.files) {
|
|
395
|
+
const filePath = resolveRepoPath(cwd, entry.path, "output path");
|
|
396
|
+
if (!fs.existsSync(filePath)) {
|
|
397
|
+
issues.push(issue("kfd-2.product-output.missing", `missing generated output: ${entry.path}`, entry.path));
|
|
398
|
+
} else if (fs.readFileSync(filePath, "utf8") !== entry.content) {
|
|
399
|
+
issues.push(issue("kfd-2.product-output.drift", `generated output is stale: ${entry.path}`, entry.path));
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const claimsDir = resolveRepoPath(cwd, path.join(rendered.outputDir, "claims"), "claims directory");
|
|
403
|
+
if (fs.existsSync(claimsDir)) {
|
|
404
|
+
for (const entry of fs.readdirSync(claimsDir, { withFileTypes: true })) {
|
|
405
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
406
|
+
const relativePath = repoRelative(cwd, path.join(claimsDir, entry.name));
|
|
407
|
+
if (!expectedPaths.has(relativePath)) {
|
|
408
|
+
issues.push(issue("kfd-2.product-output.unexpected", `unexpected generated claim: ${relativePath}`, relativePath));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return issues;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function checkKfd2ProductClaimOutputs(options = {}) {
|
|
416
|
+
const rendered = renderKfd2ProductClaimOutputs(options);
|
|
417
|
+
const issues = compareOutputs(path.resolve(options.cwd || process.cwd()), rendered);
|
|
418
|
+
return {
|
|
419
|
+
...rendered,
|
|
420
|
+
ok: issues.length === 0,
|
|
421
|
+
status: issues.length === 0 ? "current" : "mismatched",
|
|
422
|
+
issues,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function writeKfd2ProductClaimOutputs(options = {}) {
|
|
427
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
428
|
+
const rendered = renderKfd2ProductClaimOutputs({ ...options, cwd });
|
|
429
|
+
for (const entry of rendered.files) {
|
|
430
|
+
const filePath = resolveRepoPath(cwd, entry.path, "output path");
|
|
431
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
432
|
+
fs.writeFileSync(filePath, entry.content);
|
|
433
|
+
}
|
|
434
|
+
const expectedPaths = new Set(rendered.files.map((entry) => entry.path));
|
|
435
|
+
const claimsDir = resolveRepoPath(cwd, path.join(rendered.outputDir, "claims"), "claims directory");
|
|
436
|
+
const removed = [];
|
|
437
|
+
if (fs.existsSync(claimsDir)) {
|
|
438
|
+
for (const entry of fs.readdirSync(claimsDir, { withFileTypes: true })) {
|
|
439
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
440
|
+
const filePath = path.join(claimsDir, entry.name);
|
|
441
|
+
const relativePath = repoRelative(cwd, filePath);
|
|
442
|
+
if (!expectedPaths.has(relativePath)) {
|
|
443
|
+
fs.unlinkSync(filePath);
|
|
444
|
+
removed.push(relativePath);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
...rendered,
|
|
450
|
+
status: "written",
|
|
451
|
+
written: rendered.files.map((entry) => entry.path),
|
|
452
|
+
removed,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
@@ -406,6 +406,7 @@ function cliCommandMeta(id) {
|
|
|
406
406
|
"kfd-1-witness": { group: "kfd-trust", purpose: "Generate Buildchain's KFD-1 self contract-world witness." },
|
|
407
407
|
"kfd-2": { group: "kfd-trust", purpose: "Inspect KFD-2 trust taxonomy, public claims, and schema command families." },
|
|
408
408
|
"kfd-2-claims": { group: "kfd-trust", purpose: "Generate Buildchain's KFD-2 public trust claim evidence." },
|
|
409
|
+
"kfd-2-product-claims": { group: "kfd-trust", purpose: "Validate and render product-owned KFD-2 claims in the canonical .buildchain/kfd layout." },
|
|
409
410
|
"kfd-2-schema": { group: "kfd-trust", purpose: "Print the default KFD-2 schema exposed by the KFD package standards metadata." },
|
|
410
411
|
"kfd-2-taxonomy": { group: "kfd-trust", purpose: "Validate KFD-2 trust taxonomy entries from the KFD package standards metadata." },
|
|
411
412
|
"kfd-2-trust-assessment": { group: "kfd-trust", purpose: "Expose and validate the KFD package foundation KFD-2 trust assessment." },
|