@kungfu-tech/buildchain 2.9.1 → 2.10.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 +131 -0
- package/dist/site/buildchain-contract.json +6 -6
- package/dist/site/buildchain-site.json +83 -14
- package/dist/site/cli-registry.json +36 -0
- package/dist/site/kfd-claims.json +144 -9
- package/dist/site/manual-registry.json +11 -3
- package/dist/site/node-api-registry.json +16 -5
- package/dist/site/page-registry.json +68 -7
- package/dist/site/public-surface-audit.json +119 -5
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +15 -7
- package/docs/MAP.md +2 -0
- package/docs/cli.md +18 -0
- package/docs/kfd-support.md +163 -0
- package/docs/web-surface-deployments.md +9 -9
- package/package.json +2 -1
- package/packages/core/buildchain-kfd-claims.js +19 -0
- package/packages/core/index.js +15 -0
- package/packages/core/kfd3-surface-register.js +705 -0
- package/packages/core/public-surface-audit.js +1 -0
- package/scripts/check-inventory.mjs +14 -0
- package/scripts/generate-site-bundle.mjs +7 -0
- package/scripts/web-surface-core.mjs +0 -71
- package/scripts/web-surface.mjs +82 -28
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const KFD3_SURFACE_REGISTRY_CONTRACT = "kungfu-buildchain-kfd-3-surface-registry";
|
|
6
|
+
export const KFD3_SURFACE_DETECTION_CONTRACT = "kungfu-buildchain-kfd-3-surface-detection";
|
|
7
|
+
export const KFD3_SURFACE_AUDIT_CONTRACT = "kungfu-buildchain-kfd-3-surface-audit";
|
|
8
|
+
export const KFD3_CAPABILITY_QUERY_CONTRACT = "kungfu-buildchain-kfd-3-capability-query";
|
|
9
|
+
export const KFD3_DEFAULT_REGISTRY_PATH = "buildchain.kfd3.json";
|
|
10
|
+
|
|
11
|
+
const KIND_ALIASES = Object.freeze({
|
|
12
|
+
"node-api": "node-api",
|
|
13
|
+
node: "node-api",
|
|
14
|
+
npm: "node-api",
|
|
15
|
+
"python-api": "python-api",
|
|
16
|
+
python: "python-api",
|
|
17
|
+
wheel: "python-api",
|
|
18
|
+
cli: "cli",
|
|
19
|
+
command: "cli",
|
|
20
|
+
binary: "binary",
|
|
21
|
+
"standalone-binary": "binary",
|
|
22
|
+
docs: "documentation",
|
|
23
|
+
documentation: "documentation",
|
|
24
|
+
site: "site-bundle",
|
|
25
|
+
"site-bundle": "site-bundle",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function readText(filePath, fallback = "") {
|
|
29
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJsonFile(filePath, fallback = {}) {
|
|
33
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
34
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeJsonFile(filePath, value) {
|
|
38
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
39
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sha256Text(value) {
|
|
43
|
+
return crypto.createHash("sha256").update(String(value || "")).digest("hex");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sha256Json(value) {
|
|
47
|
+
return sha256Text(JSON.stringify(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sha256File(filePath) {
|
|
51
|
+
return fs.existsSync(filePath) && fs.statSync(filePath).isFile()
|
|
52
|
+
? crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")
|
|
53
|
+
: "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeKind(kind) {
|
|
57
|
+
const normalized = KIND_ALIASES[String(kind || "").trim().toLowerCase()];
|
|
58
|
+
if (!normalized) {
|
|
59
|
+
throw new Error(`unsupported KFD-3 surface kind: ${kind}`);
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeKinds(kinds = []) {
|
|
65
|
+
const selected = kinds.length ? kinds : ["node-api", "python-api", "cli", "binary", "documentation", "site-bundle"];
|
|
66
|
+
return [...new Set(selected.map(normalizeKind))].sort();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stableId(value) {
|
|
70
|
+
return String(value || "")
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/^@/, "")
|
|
73
|
+
.replace(/[^a-z0-9._/-]+/g, "-")
|
|
74
|
+
.replace(/\/+/g, "/")
|
|
75
|
+
.replace(/^-+|-+$/g, "") || "surface";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function surface({ id, kind, name, sourcePath = "", artifactPath = "", evidencePath = "", detectionMethod, policy = {}, facts = {} }) {
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
kind,
|
|
82
|
+
name: name || id,
|
|
83
|
+
visibility: "public",
|
|
84
|
+
participantFacing: true,
|
|
85
|
+
state: "detected",
|
|
86
|
+
sourcePath,
|
|
87
|
+
artifactPath: artifactPath || sourcePath,
|
|
88
|
+
evidencePath: evidencePath || sourcePath,
|
|
89
|
+
detectionMethod,
|
|
90
|
+
policy,
|
|
91
|
+
facts,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function listFilesRecursive(root, relDir, predicate = () => true) {
|
|
96
|
+
const absoluteDir = path.join(root, relDir);
|
|
97
|
+
if (!fs.existsSync(absoluteDir)) return [];
|
|
98
|
+
const out = [];
|
|
99
|
+
const visit = (dir) => {
|
|
100
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
101
|
+
const absolutePath = path.join(dir, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
visit(absolutePath);
|
|
104
|
+
} else if (entry.isFile()) {
|
|
105
|
+
const relPath = path.relative(root, absolutePath).replace(/\\/g, "/");
|
|
106
|
+
if (predicate(relPath, entry.name)) out.push(relPath);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
visit(absoluteDir);
|
|
111
|
+
return out.sort();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function detectNodeApiSurfaces({ cwd, packageJson = undefined } = {}) {
|
|
115
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
116
|
+
const pkg = packageJson || readJsonFile(pkgPath, {});
|
|
117
|
+
if (!pkg.name && !pkg.exports && !pkg.main && !pkg.types) return [];
|
|
118
|
+
const entries = [];
|
|
119
|
+
for (const [specifier, target] of Object.entries(pkg.exports || {})) {
|
|
120
|
+
if (specifier.startsWith("./site/") || specifier === "./package.json") continue;
|
|
121
|
+
const targetPath = typeof target === "string" ? target.replace(/^\.\//, "") : "";
|
|
122
|
+
entries.push(surface({
|
|
123
|
+
id: `node-api:${stableId(specifier === "." ? pkg.name : `${pkg.name}/${specifier.replace(/^\.\//, "")}`)}`,
|
|
124
|
+
kind: "node-api",
|
|
125
|
+
name: specifier === "." ? pkg.name : `${pkg.name}/${specifier.replace(/^\.\//, "")}`,
|
|
126
|
+
sourcePath: targetPath || "package.json",
|
|
127
|
+
detectionMethod: "package.json#exports",
|
|
128
|
+
facts: { packageName: pkg.name || "", export: specifier, target },
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
for (const [field, kind] of [["main", "main"], ["types", "types"]]) {
|
|
132
|
+
if (!pkg[field]) continue;
|
|
133
|
+
entries.push(surface({
|
|
134
|
+
id: `node-api:${kind}:${stableId(pkg[field])}`,
|
|
135
|
+
kind: "node-api",
|
|
136
|
+
name: `${pkg.name || "package"} ${kind}`,
|
|
137
|
+
sourcePath: String(pkg[field]).replace(/^\.\//, ""),
|
|
138
|
+
detectionMethod: `package.json#${field}`,
|
|
139
|
+
facts: { packageName: pkg.name || "", field, target: pkg[field] },
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
return entries;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function detectCliSurfaces({ cwd, packageJson = undefined } = {}) {
|
|
146
|
+
const pkg = packageJson || readJsonFile(path.join(cwd, "package.json"), {});
|
|
147
|
+
const entries = [];
|
|
148
|
+
const bins = typeof pkg.bin === "string"
|
|
149
|
+
? [[pkg.name || "cli", pkg.bin]]
|
|
150
|
+
: Object.entries(pkg.bin || {});
|
|
151
|
+
for (const [name, target] of bins) {
|
|
152
|
+
entries.push(surface({
|
|
153
|
+
id: `cli:${stableId(name)}`,
|
|
154
|
+
kind: "cli",
|
|
155
|
+
name,
|
|
156
|
+
sourcePath: String(target).replace(/^\.\//, ""),
|
|
157
|
+
detectionMethod: "package.json#bin",
|
|
158
|
+
facts: { packageName: pkg.name || "", binName: name, target },
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
for (const relPath of listFilesRecursive(cwd, "bin", (relPath) => /\.m?js$|\.c?js$|\.sh$|\.exe$/.test(relPath))) {
|
|
162
|
+
const name = path.basename(relPath).replace(/\.(mjs|cjs|js|sh|exe)$/i, "");
|
|
163
|
+
if (entries.some((entry) => entry.sourcePath === relPath)) continue;
|
|
164
|
+
entries.push(surface({
|
|
165
|
+
id: `cli:${stableId(name)}`,
|
|
166
|
+
kind: "cli",
|
|
167
|
+
name,
|
|
168
|
+
sourcePath: relPath,
|
|
169
|
+
detectionMethod: "bin-directory",
|
|
170
|
+
facts: { path: relPath },
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
return entries;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseMetadata(text) {
|
|
177
|
+
const metadata = {};
|
|
178
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
179
|
+
const match = line.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
|
|
180
|
+
if (match) metadata[match[1].toLowerCase()] = match[2].trim();
|
|
181
|
+
}
|
|
182
|
+
return metadata;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function parseEntryPoints(text) {
|
|
186
|
+
const points = [];
|
|
187
|
+
let group = "";
|
|
188
|
+
for (const rawLine of String(text || "").split(/\r?\n/)) {
|
|
189
|
+
const line = rawLine.trim();
|
|
190
|
+
if (!line || line.startsWith("#")) continue;
|
|
191
|
+
const groupMatch = line.match(/^\[([^\]]+)\]$/);
|
|
192
|
+
if (groupMatch) {
|
|
193
|
+
group = groupMatch[1];
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const entryMatch = line.match(/^([^=]+)=\s*(.+)$/);
|
|
197
|
+
if (entryMatch) {
|
|
198
|
+
points.push({ group, name: entryMatch[1].trim(), target: entryMatch[2].trim() });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return points;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function distInfoDirs(root) {
|
|
205
|
+
if (!fs.existsSync(root)) return [];
|
|
206
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
207
|
+
.filter((entry) => entry.isDirectory() && entry.name.endsWith(".dist-info"))
|
|
208
|
+
.map((entry) => path.join(root, entry.name))
|
|
209
|
+
.sort();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function detectPythonWheelSurfaces({ cwd, artifactPath = "" } = {}) {
|
|
213
|
+
const root = path.resolve(cwd, artifactPath || ".");
|
|
214
|
+
const entries = [];
|
|
215
|
+
for (const infoDir of distInfoDirs(root)) {
|
|
216
|
+
const relInfoDir = path.relative(cwd, infoDir).replace(/\\/g, "/") || path.basename(infoDir);
|
|
217
|
+
const metadata = parseMetadata(readText(path.join(infoDir, "METADATA")));
|
|
218
|
+
const recordText = readText(path.join(infoDir, "RECORD"));
|
|
219
|
+
const entryPoints = parseEntryPoints(readText(path.join(infoDir, "entry_points.txt")));
|
|
220
|
+
const topLevelText = readText(path.join(infoDir, "top_level.txt"));
|
|
221
|
+
const packageName = metadata.name || path.basename(infoDir).replace(/\.dist-info$/, "");
|
|
222
|
+
const topLevelPackages = [
|
|
223
|
+
...topLevelText.split(/\r?\n/).map((line) => line.trim()).filter(Boolean),
|
|
224
|
+
...recordText.split(/\r?\n/).map((line) => line.split(",")[0]).filter(Boolean)
|
|
225
|
+
.map((recordPath) => recordPath.split(/[\\/]/)[0])
|
|
226
|
+
.filter((segment) => segment && !segment.endsWith(".dist-info") && /^[A-Za-z_][A-Za-z0-9_]*$/.test(segment)),
|
|
227
|
+
];
|
|
228
|
+
for (const moduleName of [...new Set(topLevelPackages)].sort()) {
|
|
229
|
+
entries.push(surface({
|
|
230
|
+
id: `python-api:${stableId(packageName)}:${stableId(moduleName)}`,
|
|
231
|
+
kind: "python-api",
|
|
232
|
+
name: `${packageName}:${moduleName}`,
|
|
233
|
+
sourcePath: relInfoDir,
|
|
234
|
+
artifactPath: relInfoDir,
|
|
235
|
+
detectionMethod: "wheel-dist-info",
|
|
236
|
+
policy: { publicApiPolicy: "top-level-package-metadata" },
|
|
237
|
+
facts: { packageName, version: metadata.version || "", moduleName },
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
for (const point of entryPoints) {
|
|
241
|
+
entries.push(surface({
|
|
242
|
+
id: `cli:${stableId(packageName)}:${stableId(point.name)}`,
|
|
243
|
+
kind: "cli",
|
|
244
|
+
name: point.name,
|
|
245
|
+
sourcePath: `${relInfoDir}/entry_points.txt`,
|
|
246
|
+
artifactPath: `${relInfoDir}/entry_points.txt`,
|
|
247
|
+
detectionMethod: "wheel-entry-points",
|
|
248
|
+
facts: { packageName, version: metadata.version || "", entryPointGroup: point.group, target: point.target },
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return entries;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function detectBinarySurfaces({ cwd, artifactPath = "" } = {}) {
|
|
256
|
+
const roots = [artifactPath, "dist", "build", "target/release"].filter(Boolean);
|
|
257
|
+
const seen = new Set();
|
|
258
|
+
const entries = [];
|
|
259
|
+
for (const relRoot of roots) {
|
|
260
|
+
for (const relPath of listFilesRecursive(cwd, relRoot, (candidate, name) => {
|
|
261
|
+
return /\.(exe|dll|dylib|so|a|lib|zip|tar\.gz|tgz)$/i.test(candidate) || (!name.includes(".") && candidate.includes("/"));
|
|
262
|
+
})) {
|
|
263
|
+
if (seen.has(relPath)) continue;
|
|
264
|
+
seen.add(relPath);
|
|
265
|
+
entries.push(surface({
|
|
266
|
+
id: `binary:${stableId(relPath)}`,
|
|
267
|
+
kind: "binary",
|
|
268
|
+
name: path.basename(relPath),
|
|
269
|
+
sourcePath: relPath,
|
|
270
|
+
artifactPath: relPath,
|
|
271
|
+
detectionMethod: "artifact-path-scan",
|
|
272
|
+
facts: { path: relPath, sha256: sha256File(path.join(cwd, relPath)) },
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return entries;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function detectDocumentationSurfaces({ cwd } = {}) {
|
|
280
|
+
return [
|
|
281
|
+
...listFilesRecursive(cwd, "docs", (relPath) => relPath.endsWith(".md")),
|
|
282
|
+
"README.md",
|
|
283
|
+
"AGENTS.md",
|
|
284
|
+
]
|
|
285
|
+
.filter((relPath) => fs.existsSync(path.join(cwd, relPath)))
|
|
286
|
+
.map((relPath) => surface({
|
|
287
|
+
id: `doc:${stableId(relPath)}`,
|
|
288
|
+
kind: "documentation",
|
|
289
|
+
name: relPath,
|
|
290
|
+
sourcePath: relPath,
|
|
291
|
+
detectionMethod: "documentation-scan",
|
|
292
|
+
facts: { path: relPath, sha256: sha256File(path.join(cwd, relPath)) },
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function detectSiteBundleSurfaces({ cwd } = {}) {
|
|
297
|
+
return listFilesRecursive(cwd, "dist/site", (relPath) => relPath.endsWith(".json"))
|
|
298
|
+
.map((relPath) => surface({
|
|
299
|
+
id: `site-bundle:${stableId(relPath)}`,
|
|
300
|
+
kind: "site-bundle",
|
|
301
|
+
name: relPath,
|
|
302
|
+
sourcePath: relPath,
|
|
303
|
+
artifactPath: relPath,
|
|
304
|
+
detectionMethod: "buildchain-site-bundle",
|
|
305
|
+
facts: { path: relPath, sha256: sha256File(path.join(cwd, relPath)) },
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function uniqueSurfaces(entries) {
|
|
310
|
+
const byId = new Map();
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
if (!entry?.id || byId.has(entry.id)) continue;
|
|
313
|
+
byId.set(entry.id, entry);
|
|
314
|
+
}
|
|
315
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function detectKfd3Surfaces({ cwd = process.cwd(), kinds = [], artifactPath = "" } = {}) {
|
|
319
|
+
const resolvedCwd = path.resolve(cwd);
|
|
320
|
+
const selectedKinds = normalizeKinds(kinds);
|
|
321
|
+
const packageJson = readJsonFile(path.join(resolvedCwd, "package.json"), {});
|
|
322
|
+
const detectedAt = process.env.BUILDCHAIN_KFD3_DETECTED_AT || process.env.BUILDCHAIN_SURFACE_GENERATED_AT || new Date().toISOString();
|
|
323
|
+
const surfaces = [];
|
|
324
|
+
if (selectedKinds.includes("node-api")) surfaces.push(...detectNodeApiSurfaces({ cwd: resolvedCwd, packageJson }));
|
|
325
|
+
if (selectedKinds.includes("python-api")) surfaces.push(...detectPythonWheelSurfaces({ cwd: resolvedCwd, artifactPath }));
|
|
326
|
+
if (selectedKinds.includes("cli")) surfaces.push(...detectCliSurfaces({ cwd: resolvedCwd, packageJson }));
|
|
327
|
+
if (selectedKinds.includes("binary")) surfaces.push(...detectBinarySurfaces({ cwd: resolvedCwd, artifactPath }));
|
|
328
|
+
if (selectedKinds.includes("documentation")) surfaces.push(...detectDocumentationSurfaces({ cwd: resolvedCwd }));
|
|
329
|
+
if (selectedKinds.includes("site-bundle")) surfaces.push(...detectSiteBundleSurfaces({ cwd: resolvedCwd }));
|
|
330
|
+
const detected = uniqueSurfaces(surfaces);
|
|
331
|
+
return {
|
|
332
|
+
schemaVersion: 1,
|
|
333
|
+
contract: KFD3_SURFACE_DETECTION_CONTRACT,
|
|
334
|
+
cwd: resolvedCwd,
|
|
335
|
+
artifactPath,
|
|
336
|
+
detectedAt,
|
|
337
|
+
kinds: selectedKinds,
|
|
338
|
+
summary: {
|
|
339
|
+
surfaceCount: detected.length,
|
|
340
|
+
byKind: Object.fromEntries(selectedKinds.map((kind) => [kind, detected.filter((entry) => entry.kind === kind).length])),
|
|
341
|
+
},
|
|
342
|
+
surfaces: detected,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function readKfd3SurfaceRegistry({ cwd = process.cwd(), registryPath = KFD3_DEFAULT_REGISTRY_PATH } = {}) {
|
|
347
|
+
const filePath = path.resolve(cwd, registryPath);
|
|
348
|
+
const registry = readJsonFile(filePath, null);
|
|
349
|
+
if (!registry) {
|
|
350
|
+
return {
|
|
351
|
+
schemaVersion: 1,
|
|
352
|
+
contract: KFD3_SURFACE_REGISTRY_CONTRACT,
|
|
353
|
+
product: {},
|
|
354
|
+
registryPath,
|
|
355
|
+
surfaces: [],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
schemaVersion: registry.schemaVersion || 1,
|
|
360
|
+
contract: registry.contract || KFD3_SURFACE_REGISTRY_CONTRACT,
|
|
361
|
+
product: registry.product || {},
|
|
362
|
+
registryPath,
|
|
363
|
+
surfaces: Array.isArray(registry.surfaces) ? registry.surfaces : [],
|
|
364
|
+
policy: registry.policy || {},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function writeKfd3SurfaceRegistry({ cwd = process.cwd(), registryPath = KFD3_DEFAULT_REGISTRY_PATH, registry }) {
|
|
369
|
+
const next = {
|
|
370
|
+
schemaVersion: 1,
|
|
371
|
+
contract: KFD3_SURFACE_REGISTRY_CONTRACT,
|
|
372
|
+
product: registry.product || {},
|
|
373
|
+
registryPath,
|
|
374
|
+
surfaces: uniqueSurfaces(registry.surfaces || []).map((entry) => ({
|
|
375
|
+
...entry,
|
|
376
|
+
state: entry.state || "declared",
|
|
377
|
+
declaration: entry.declaration || {
|
|
378
|
+
owner: "product",
|
|
379
|
+
source: "buildchain kfd-3 register",
|
|
380
|
+
},
|
|
381
|
+
})),
|
|
382
|
+
policy: registry.policy || {
|
|
383
|
+
detectedButUnregistered: "warn",
|
|
384
|
+
declaredButMissing: "fail",
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
writeJsonFile(path.resolve(cwd, registryPath), next);
|
|
388
|
+
return next;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function registerKfd3Surfaces({
|
|
392
|
+
cwd = process.cwd(),
|
|
393
|
+
registryPath = KFD3_DEFAULT_REGISTRY_PATH,
|
|
394
|
+
kinds = [],
|
|
395
|
+
artifactPath = "",
|
|
396
|
+
product = {},
|
|
397
|
+
} = {}) {
|
|
398
|
+
const detection = detectKfd3Surfaces({ cwd, kinds, artifactPath });
|
|
399
|
+
const registry = readKfd3SurfaceRegistry({ cwd, registryPath });
|
|
400
|
+
const existing = new Map((registry.surfaces || []).map((entry) => [entry.id, entry]));
|
|
401
|
+
for (const detected of detection.surfaces) {
|
|
402
|
+
existing.set(detected.id, {
|
|
403
|
+
...detected,
|
|
404
|
+
...(existing.get(detected.id) || {}),
|
|
405
|
+
id: detected.id,
|
|
406
|
+
kind: detected.kind,
|
|
407
|
+
name: detected.name,
|
|
408
|
+
state: existing.get(detected.id)?.state || "declared",
|
|
409
|
+
sourcePath: detected.sourcePath,
|
|
410
|
+
artifactPath: detected.artifactPath,
|
|
411
|
+
evidencePath: detected.evidencePath,
|
|
412
|
+
detectionMethod: detected.detectionMethod,
|
|
413
|
+
facts: detected.facts,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const next = writeKfd3SurfaceRegistry({
|
|
417
|
+
cwd,
|
|
418
|
+
registryPath,
|
|
419
|
+
registry: {
|
|
420
|
+
...registry,
|
|
421
|
+
product: { ...registry.product, ...product },
|
|
422
|
+
surfaces: [...existing.values()],
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
return {
|
|
426
|
+
schemaVersion: 1,
|
|
427
|
+
contract: "kungfu-buildchain-kfd-3-surface-register",
|
|
428
|
+
registryPath,
|
|
429
|
+
registeredCount: detection.surfaces.length,
|
|
430
|
+
registrySurfaceCount: next.surfaces.length,
|
|
431
|
+
detection,
|
|
432
|
+
registry: next,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function auditKfd3Surfaces({
|
|
437
|
+
cwd = process.cwd(),
|
|
438
|
+
registryPath = KFD3_DEFAULT_REGISTRY_PATH,
|
|
439
|
+
kinds = [],
|
|
440
|
+
artifactPath = "",
|
|
441
|
+
} = {}) {
|
|
442
|
+
const detection = detectKfd3Surfaces({ cwd, kinds, artifactPath });
|
|
443
|
+
const registry = readKfd3SurfaceRegistry({ cwd, registryPath });
|
|
444
|
+
const detectedIds = new Set(detection.surfaces.map((entry) => entry.id));
|
|
445
|
+
const declaredIds = new Set(registry.surfaces.map((entry) => entry.id));
|
|
446
|
+
const detectedButUnregistered = detection.surfaces.filter((entry) => !declaredIds.has(entry.id));
|
|
447
|
+
const declaredButMissing = registry.surfaces.filter((entry) => !detectedIds.has(entry.id));
|
|
448
|
+
const enforced = registry.surfaces.filter((entry) => entry.state === "enforced" || entry.enforcement === "enforced");
|
|
449
|
+
const issues = [
|
|
450
|
+
...detectedButUnregistered.map((entry) => ({
|
|
451
|
+
level: "warning",
|
|
452
|
+
code: "detected-unregistered",
|
|
453
|
+
surfaceId: entry.id,
|
|
454
|
+
message: `Detected public surface is not declared: ${entry.id}`,
|
|
455
|
+
})),
|
|
456
|
+
...declaredButMissing.map((entry) => ({
|
|
457
|
+
level: "error",
|
|
458
|
+
code: "declared-missing",
|
|
459
|
+
surfaceId: entry.id,
|
|
460
|
+
message: `Declared public surface was not detected: ${entry.id}`,
|
|
461
|
+
})),
|
|
462
|
+
];
|
|
463
|
+
return {
|
|
464
|
+
schemaVersion: 1,
|
|
465
|
+
contract: KFD3_SURFACE_AUDIT_CONTRACT,
|
|
466
|
+
ok: issues.every((issue) => issue.level !== "error"),
|
|
467
|
+
status: issues.some((issue) => issue.level === "error") ? "failed" : detectedButUnregistered.length ? "partial" : "passed",
|
|
468
|
+
registryPath,
|
|
469
|
+
detection,
|
|
470
|
+
registry,
|
|
471
|
+
summary: {
|
|
472
|
+
detected: detection.surfaces.length,
|
|
473
|
+
declared: registry.surfaces.length,
|
|
474
|
+
enforced: enforced.length,
|
|
475
|
+
detectedButUnregistered: detectedButUnregistered.length,
|
|
476
|
+
declaredButMissing: declaredButMissing.length,
|
|
477
|
+
},
|
|
478
|
+
states: {
|
|
479
|
+
detected: detection.surfaces,
|
|
480
|
+
declared: registry.surfaces,
|
|
481
|
+
enforced,
|
|
482
|
+
},
|
|
483
|
+
comparison: {
|
|
484
|
+
detectedButUnregistered,
|
|
485
|
+
declaredButMissing,
|
|
486
|
+
},
|
|
487
|
+
issues,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function createKfd3SurfaceWitness({
|
|
492
|
+
cwd = process.cwd(),
|
|
493
|
+
registryPath = KFD3_DEFAULT_REGISTRY_PATH,
|
|
494
|
+
kind = "prebuild",
|
|
495
|
+
sourceSha = "",
|
|
496
|
+
artifactPath = "",
|
|
497
|
+
} = {}) {
|
|
498
|
+
const audit = auditKfd3Surfaces({ cwd, registryPath, artifactPath });
|
|
499
|
+
const registryDigest = `sha256:${sha256Json(audit.registry)}`;
|
|
500
|
+
const collaborationInterface = {
|
|
501
|
+
schemaVersion: 1,
|
|
502
|
+
contract: KFD3_SURFACE_REGISTRY_CONTRACT,
|
|
503
|
+
product: audit.registry.product,
|
|
504
|
+
surfaces: audit.registry.surfaces,
|
|
505
|
+
detectedSurfaces: audit.detection.surfaces,
|
|
506
|
+
audit,
|
|
507
|
+
};
|
|
508
|
+
return {
|
|
509
|
+
schemaVersion: 1,
|
|
510
|
+
id: audit.registry.product?.id || audit.registry.product?.name || "kfd-3-surface-registry",
|
|
511
|
+
standard: "kfd-3",
|
|
512
|
+
witnessKind: kind,
|
|
513
|
+
supportLevel: audit.status === "passed" ? "release" : "declared",
|
|
514
|
+
source: {
|
|
515
|
+
cwd: path.resolve(cwd),
|
|
516
|
+
sourceSha,
|
|
517
|
+
registryPath,
|
|
518
|
+
registryDigest,
|
|
519
|
+
},
|
|
520
|
+
sourceRegistry: {
|
|
521
|
+
id: audit.registry.product?.id || audit.registry.product?.name || "product-kfd-3-surface-registry",
|
|
522
|
+
path: registryPath,
|
|
523
|
+
sha256: sha256File(path.resolve(cwd, registryPath)),
|
|
524
|
+
},
|
|
525
|
+
collaborationInterfaceDigest: `sha256:${sha256Json(collaborationInterface)}`,
|
|
526
|
+
collaborationInterface,
|
|
527
|
+
auditBoundary: {
|
|
528
|
+
mode: audit.status === "passed" ? "closed-world" : "declared-boundary",
|
|
529
|
+
scope: "KFD-3 registered product public surfaces detected by Buildchain",
|
|
530
|
+
detectedButUnregisteredPolicy: "warn",
|
|
531
|
+
declaredButMissingPolicy: "fail",
|
|
532
|
+
},
|
|
533
|
+
residualRisk: audit.comparison.detectedButUnregistered.map((entry) => ({
|
|
534
|
+
surfaceId: entry.id,
|
|
535
|
+
riskType: "unregistered-detected-surface",
|
|
536
|
+
trustImpact: "manual-review-required",
|
|
537
|
+
machineProvability: "machine-detected",
|
|
538
|
+
agentAction: "register-or-exempt-surface",
|
|
539
|
+
message: `Detected public surface is not yet declared: ${entry.id}`,
|
|
540
|
+
})),
|
|
541
|
+
responsibility: {
|
|
542
|
+
registryFactsOwner: "product",
|
|
543
|
+
artifactVerificationOwner: "Buildchain KFD-3 surface audit",
|
|
544
|
+
releasePassportProofOwner: "Buildchain",
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function readJsonLocation(location) {
|
|
550
|
+
if (!location) return null;
|
|
551
|
+
if (/^https?:\/\//.test(location)) {
|
|
552
|
+
const response = await fetch(location);
|
|
553
|
+
if (!response.ok) {
|
|
554
|
+
throw new Error(`failed to fetch ${location}: HTTP ${response.status}`);
|
|
555
|
+
}
|
|
556
|
+
return response.json();
|
|
557
|
+
}
|
|
558
|
+
return readJsonFile(path.resolve(location), null);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function kfd2TrustForCapability(passport, surfaceId) {
|
|
562
|
+
const claims = passport?.["kfd-2"]?.claims || passport?.kfd2?.claims || [];
|
|
563
|
+
const related = claims.find((claim) => {
|
|
564
|
+
const haystack = JSON.stringify(claim);
|
|
565
|
+
return haystack.includes(surfaceId) || haystack.includes("kfd-3");
|
|
566
|
+
});
|
|
567
|
+
if (!related) {
|
|
568
|
+
return {
|
|
569
|
+
status: "unknown",
|
|
570
|
+
trustImpact: "passport-claim-not-found",
|
|
571
|
+
residualRisk: [],
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
status: related.verification?.result || related.releaseStatus || "declared",
|
|
576
|
+
trustImpact: related.trustProof?.releaseStatus || related.verification?.result || "review",
|
|
577
|
+
residualRisk: related.residualRisk || related.trustProof?.residualRisk || [],
|
|
578
|
+
claimId: related.id || "",
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function capabilitiesFromRegistry({ registry, audit, passport = null }) {
|
|
583
|
+
const detectedById = new Map((audit?.detection?.surfaces || []).map((entry) => [entry.id, entry]));
|
|
584
|
+
return (registry.surfaces || []).map((entry) => {
|
|
585
|
+
const detected = detectedById.get(entry.id);
|
|
586
|
+
return {
|
|
587
|
+
id: entry.id,
|
|
588
|
+
kind: entry.kind,
|
|
589
|
+
name: entry.name,
|
|
590
|
+
state: entry.state || "declared",
|
|
591
|
+
detected: Boolean(detected),
|
|
592
|
+
enforced: entry.state === "enforced" || entry.enforcement === "enforced",
|
|
593
|
+
sourcePath: entry.sourcePath,
|
|
594
|
+
artifactPath: entry.artifactPath,
|
|
595
|
+
evidencePath: entry.evidencePath,
|
|
596
|
+
kfd1Basis: {
|
|
597
|
+
registryPath: registry.registryPath || KFD3_DEFAULT_REGISTRY_PATH,
|
|
598
|
+
sourcePath: entry.sourcePath,
|
|
599
|
+
artifactPath: entry.artifactPath,
|
|
600
|
+
digest: `sha256:${sha256Json(entry)}`,
|
|
601
|
+
},
|
|
602
|
+
kfd2Trust: kfd2TrustForCapability(passport, entry.id),
|
|
603
|
+
residualRisk: detected ? [] : [{
|
|
604
|
+
riskType: "declared-surface-not-detected",
|
|
605
|
+
trustImpact: "verification-required",
|
|
606
|
+
machineProvability: "machine-detected",
|
|
607
|
+
agentAction: "run-buildchain-kfd-3-audit",
|
|
608
|
+
}],
|
|
609
|
+
};
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export async function queryKfd3Capabilities({
|
|
614
|
+
cwd = process.cwd(),
|
|
615
|
+
product = "",
|
|
616
|
+
registryPath = KFD3_DEFAULT_REGISTRY_PATH,
|
|
617
|
+
passportLocation = "",
|
|
618
|
+
artifactPath = "",
|
|
619
|
+
} = {}) {
|
|
620
|
+
const passport = await readJsonLocation(passportLocation);
|
|
621
|
+
if (passport) {
|
|
622
|
+
const kfd3 = passport["kfd-3"] || passport.kfd3 || {};
|
|
623
|
+
const interfaces = Array.isArray(kfd3.collaborationInterfaces)
|
|
624
|
+
? kfd3.collaborationInterfaces
|
|
625
|
+
: [kfd3.collaborationInterface].filter(Boolean);
|
|
626
|
+
const surfaces = interfaces.flatMap((entry) => entry?.surfaces || entry?.declaredSurfaces || []);
|
|
627
|
+
return {
|
|
628
|
+
schemaVersion: 1,
|
|
629
|
+
contract: KFD3_CAPABILITY_QUERY_CONTRACT,
|
|
630
|
+
product: product || passport.product?.name || passport.package?.name || "release-passport",
|
|
631
|
+
source: { type: "release-passport", location: passportLocation },
|
|
632
|
+
release: passport.release || {},
|
|
633
|
+
capabilities: surfaces.map((entry) => ({
|
|
634
|
+
id: entry.id || entry.name,
|
|
635
|
+
kind: entry.kind || "surface",
|
|
636
|
+
name: entry.name || entry.id,
|
|
637
|
+
state: entry.state || "declared",
|
|
638
|
+
detected: true,
|
|
639
|
+
enforced: kfd3.releaseStatus === "enforced",
|
|
640
|
+
kfd1Basis: { sourcePath: entry.sourcePath || "", artifactPath: entry.artifactPath || "", digest: entry.digest || "" },
|
|
641
|
+
kfd2Trust: kfd2TrustForCapability(passport, entry.id || entry.name || ""),
|
|
642
|
+
residualRisk: entry.residualRisk || [],
|
|
643
|
+
})),
|
|
644
|
+
kfd: {
|
|
645
|
+
kfd1: passport["kfd-1"]?.result || passport.kfd1?.result || "unknown",
|
|
646
|
+
kfd2: passport["kfd-2"]?.result || passport.kfd2?.result || "unknown",
|
|
647
|
+
kfd3: kfd3.result || kfd3.releaseStatus || "unknown",
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const registry = readKfd3SurfaceRegistry({ cwd, registryPath });
|
|
653
|
+
const hasRegistry = fs.existsSync(path.resolve(cwd, registryPath));
|
|
654
|
+
const audit = hasRegistry ? auditKfd3Surfaces({ cwd, registryPath, artifactPath }) : null;
|
|
655
|
+
if (hasRegistry) {
|
|
656
|
+
return {
|
|
657
|
+
schemaVersion: 1,
|
|
658
|
+
contract: KFD3_CAPABILITY_QUERY_CONTRACT,
|
|
659
|
+
product: product || registry.product?.name || registry.product?.id || "local-product",
|
|
660
|
+
source: { type: "surface-registry", path: registryPath },
|
|
661
|
+
status: audit.status,
|
|
662
|
+
summary: audit.summary,
|
|
663
|
+
capabilities: capabilitiesFromRegistry({ registry, audit }),
|
|
664
|
+
kfd: {
|
|
665
|
+
kfd1: "registry-facts",
|
|
666
|
+
kfd2: passport ? "passport-trust" : "not-attached",
|
|
667
|
+
kfd3: audit.status,
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!product || product === "buildchain" || product === "@kungfu-tech/buildchain") {
|
|
673
|
+
const claimsPath = path.join(cwd, "dist/site/kfd-claims.json");
|
|
674
|
+
const claims = readJsonFile(claimsPath, null);
|
|
675
|
+
if (claims?.collaborationSurfaces) {
|
|
676
|
+
const registryFromClaims = {
|
|
677
|
+
product: claims.product || { name: "Buildchain" },
|
|
678
|
+
registryPath: "dist/site/kfd-claims.json",
|
|
679
|
+
surfaces: [
|
|
680
|
+
...Object.values(claims.collaborationSurfaces.groups || {}).flat(),
|
|
681
|
+
...(claims.collaborationSurfaces.additionalSurfaces || []),
|
|
682
|
+
],
|
|
683
|
+
};
|
|
684
|
+
return {
|
|
685
|
+
schemaVersion: 1,
|
|
686
|
+
contract: KFD3_CAPABILITY_QUERY_CONTRACT,
|
|
687
|
+
product: "Buildchain",
|
|
688
|
+
source: { type: "buildchain-site-kfd-claims", path: "dist/site/kfd-claims.json" },
|
|
689
|
+
status: claims.publicSurfaceReverseAudit?.status || "declared",
|
|
690
|
+
summary: {
|
|
691
|
+
declared: registryFromClaims.surfaces.length,
|
|
692
|
+
publicSurfaceCount: claims.collaborationSurfaces.publicSurfaceCount,
|
|
693
|
+
},
|
|
694
|
+
capabilities: capabilitiesFromRegistry({ registry: registryFromClaims, audit: { detection: { surfaces: registryFromClaims.surfaces } } }),
|
|
695
|
+
kfd: {
|
|
696
|
+
kfd1: "self-contract-registry",
|
|
697
|
+
kfd2: "public-claim-registry",
|
|
698
|
+
kfd3: claims.publicSurfaceReverseAudit?.status || "declared",
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
throw new Error(`no KFD-3 registry, passport, or known product facts found for ${product || "local product"}`);
|
|
705
|
+
}
|