@kungfu-tech/kfd 1.0.0-alpha.14 → 1.0.0-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,657 @@
1
+ // Registry conformance check: the registry, decision documents, and release
2
+ // impact ledger must agree, so a release cannot ship evidence that lies about
3
+ // its contents or versioning surface.
4
+ import { readFileSync, existsSync } from "node:fs";
5
+ import crypto from "node:crypto";
6
+ import { generatedSiteBundle } from "./update-site-bundle.mjs";
7
+
8
+ const fail = (msg) => { console.error(`check: ${msg}`); process.exitCode = 1; };
9
+ const sha256File = (filePath) => crypto.createHash("sha256").update(readFileSync(filePath)).digest("hex");
10
+ const requireFields = (value, required, label) => {
11
+ for (const field of required ?? []) {
12
+ if (value?.[field] === undefined) fail(`${label} missing required field ${field}`);
13
+ }
14
+ };
15
+ const asSet = (items) => new Set(items ?? []);
16
+ const requireSameEnum = (actual, expected, label) => {
17
+ const actualSet = asSet(actual);
18
+ const expectedSet = asSet(expected);
19
+ for (const value of expectedSet) {
20
+ if (!actualSet.has(value)) fail(`${label} missing value ${value}`);
21
+ }
22
+ for (const value of actualSet) {
23
+ if (!expectedSet.has(value)) fail(`${label} has unregistered value ${value}`);
24
+ }
25
+ };
26
+ const registry = JSON.parse(readFileSync("registry.json", "utf8"));
27
+ const standardsMetadata = JSON.parse(readFileSync("standards.json", "utf8"));
28
+ const standardsSchema = JSON.parse(readFileSync("schemas/kfd-standards.schema.json", "utf8"));
29
+ const releaseImpact = JSON.parse(readFileSync("release-impact.json", "utf8"));
30
+ const packageJson = JSON.parse(readFileSync("package.json", "utf8"));
31
+ const siteBundle = JSON.parse(readFileSync("site/kfd-site.json", "utf8"));
32
+ const expectedSiteBundle = generatedSiteBundle();
33
+ const kfd1WitnessPath = ".buildchain/kfd-1/contract-world.witness.json";
34
+ const kfd1Witness = existsSync(kfd1WitnessPath) ? JSON.parse(readFileSync(kfd1WitnessPath, "utf8")) : undefined;
35
+ const kfd2ClaimPath = ".buildchain/kfd-2/public-release-trust.claim.json";
36
+ const kfd2Claim = existsSync(kfd2ClaimPath) ? JSON.parse(readFileSync(kfd2ClaimPath, "utf8")) : undefined;
37
+ const kfd3InterfacePath = ".buildchain/kfd-3/collaboration-interface.json";
38
+ const kfd3PrebuildWitnessPath = ".buildchain/kfd-3/collaboration-interface.prebuild.json";
39
+ const kfd3ArtifactWitnessPath = ".buildchain/kfd-3/collaboration-interface.artifact.json";
40
+ const kfd3Interface = existsSync(kfd3InterfacePath) ? JSON.parse(readFileSync(kfd3InterfacePath, "utf8")) : undefined;
41
+ const kfd3PrebuildWitness = existsSync(kfd3PrebuildWitnessPath) ? JSON.parse(readFileSync(kfd3PrebuildWitnessPath, "utf8")) : undefined;
42
+ const kfd3ArtifactWitness = existsSync(kfd3ArtifactWitnessPath) ? JSON.parse(readFileSync(kfd3ArtifactWitnessPath, "utf8")) : undefined;
43
+ const hashablePath = (filePath) => String(filePath || "").split("#", 1)[0];
44
+
45
+ if (registry.schemaVersion !== 1) fail(`unsupported schemaVersion ${registry.schemaVersion}`);
46
+ if (registry.contract !== "kfd-registry") fail(`unexpected contract ${registry.contract}`);
47
+ if (standardsMetadata.schemaVersion !== 1) fail(`unsupported standards schemaVersion ${standardsMetadata.schemaVersion}`);
48
+ if (standardsMetadata.contract !== "kfd-standards-metadata") fail(`unexpected standards contract ${standardsMetadata.contract}`);
49
+ if (standardsMetadata.metadataSchema?.id !== "https://kfd.libkungfu.dev/schemas/kfd-standards.schema.json") {
50
+ fail("standards metadataSchema.id must be the canonical KFD standards schema URI");
51
+ }
52
+ if (standardsMetadata.metadataSchema?.path !== "schemas/kfd-standards.schema.json") {
53
+ fail("standards metadataSchema.path must be schemas/kfd-standards.schema.json");
54
+ }
55
+ if (standardsSchema.$id !== standardsMetadata.metadataSchema?.id) {
56
+ fail("standards schema $id must match standards metadataSchema.id");
57
+ }
58
+ if (standardsSchema.properties?.contract?.const !== "kfd-standards-metadata") {
59
+ fail("standards schema must describe the kfd-standards-metadata contract");
60
+ }
61
+ requireFields(standardsMetadata, standardsSchema.required, "standards metadata");
62
+ requireFields(standardsMetadata.metadataSchema, standardsSchema.properties?.metadataSchema?.required, "standards metadataSchema");
63
+ requireFields(standardsMetadata.source, standardsSchema.properties?.source?.required, "standards source");
64
+ if (siteBundle.schemaVersion !== 1) fail(`unsupported site bundle schemaVersion ${siteBundle.schemaVersion}`);
65
+ if (siteBundle.contract !== "kfd-site-bundle") fail(`unexpected site bundle contract ${siteBundle.contract}`);
66
+ if (JSON.stringify(siteBundle) !== JSON.stringify(expectedSiteBundle)) {
67
+ fail("site/kfd-site.json must match the generated README.md homepage bundle; run npm run update:site-bundle");
68
+ }
69
+ if (siteBundle.source?.homepageTextSource !== "README.md") fail("site bundle homepageTextSource must be README.md");
70
+ if (siteBundle.source?.registry !== "registry.json") fail("site bundle registry source must be registry.json");
71
+ if (siteBundle.source?.decisionsDir !== "decisions") fail("site bundle decisionsDir must be decisions");
72
+ if (siteBundle.homepage?.title !== "KFD — Kung Fu Decisions") fail("site bundle homepage title must match README H1 text");
73
+ if (!Array.isArray(siteBundle.homepage?.sections) || siteBundle.homepage.sections.length === 0) {
74
+ fail("site bundle homepage.sections must expose generated README sections");
75
+ }
76
+ if (!siteBundle.homepage?.displayPlan?.firstScreen?.include?.includes("foundation-triad")) {
77
+ fail("site bundle homepage displayPlan firstScreen must include foundation-triad");
78
+ }
79
+ for (const requiredSection of ["foundation-triad", "foundation-model", "product-proof-path", "agent-quickstart", "decision-metadata"]) {
80
+ if (!siteBundle.homepage.sections.some((entry) => entry.id === requiredSection && entry.sourcePath === "README.md" && entry.markdown)) {
81
+ fail(`site bundle homepage.sections must include README projection ${requiredSection}`);
82
+ }
83
+ }
84
+ if (siteBundle.homepage.sections.some((entry) => entry.id === "homepage-content-contract")) {
85
+ fail("site bundle homepage.sections must not render the renderer contract as homepage content");
86
+ }
87
+ if (
88
+ siteBundle.homepage?.rendererContract?.id !== "homepage-content-contract" ||
89
+ siteBundle.homepage?.rendererContract?.renderAsHomepageContent !== false ||
90
+ !siteBundle.homepage?.rendererContract?.markdown
91
+ ) {
92
+ fail("site bundle homepage.rendererContract must expose the README renderer contract outside homepage.sections");
93
+ }
94
+ if (siteBundle.homepage?.currentDecisions?.source !== "registry.json") fail("site bundle currentDecisions source must be registry.json");
95
+ if (siteBundle.decisionPages?.source !== "registry.json") fail("site bundle decisionPages source must be registry.json");
96
+ if (siteBundle.decisionPages?.bodySource !== "registry.entries[].path") fail("site bundle decision page body source must be registry.entries[].path");
97
+ if (siteBundle.decisionPages?.metadata?.licenseBoundary?.license !== "Apache-2.0") {
98
+ fail("site bundle decision metadata licenseBoundary.license must be Apache-2.0");
99
+ }
100
+ if (siteBundle.decisionPages?.metadata?.licenseBoundary?.licenseFile !== "LICENSE") {
101
+ fail("site bundle decision metadata licenseBoundary.licenseFile must be LICENSE");
102
+ }
103
+ if (siteBundle.decisionPages?.metadata?.licenseBoundary?.officialStatusAndTrademarks !== "TRADEMARKS.md") {
104
+ fail("site bundle decision metadata licenseBoundary.officialStatusAndTrademarks must be TRADEMARKS.md");
105
+ }
106
+ const sitePublicFactSource = siteBundle.decisionPages?.metadata?.publicFactSource;
107
+ if (sitePublicFactSource?.kind !== "git-repository") fail("site bundle decision metadata publicFactSource.kind must be git-repository");
108
+ if (sitePublicFactSource?.host !== "github") fail("site bundle decision metadata publicFactSource.host must be github");
109
+ if (sitePublicFactSource?.repository !== "kungfu-systems/kfd") fail("site bundle decision metadata publicFactSource.repository must be kungfu-systems/kfd");
110
+ if (sitePublicFactSource?.url !== "https://github.com/kungfu-systems/kfd") fail("site bundle decision metadata publicFactSource.url must be the KFD GitHub repository");
111
+ if (sitePublicFactSource?.loadBearingCoordinate !== "commit-addressed repository contents") {
112
+ fail("site bundle decision metadata publicFactSource.loadBearingCoordinate must be commit-addressed repository contents");
113
+ }
114
+ if (sitePublicFactSource?.stableRenderedIndex !== "https://kfd.libkungfu.dev") {
115
+ fail("site bundle decision metadata publicFactSource.stableRenderedIndex must be https://kfd.libkungfu.dev");
116
+ }
117
+ for (const requiredPath of ["decisions/kfd-N.md", "registry.json", "standards.json"]) {
118
+ if (!sitePublicFactSource?.canonicalPaths?.includes(requiredPath)) {
119
+ fail(`site bundle decision metadata publicFactSource.canonicalPaths must include ${requiredPath}`);
120
+ }
121
+ }
122
+ for (const requiredFile of ["README.md", "TRADEMARKS.md", "decisions", "registry.json", "standards.json", "kfd.release.json", "schemas", "site", "buildchain.release-propagation.json", "release-impact.json", ".buildchain/kfd-1/contract-world.witness.json", ".buildchain/kfd-2", ".buildchain/kfd-3", "docs"]) {
123
+ if (!Array.isArray(packageJson.files) || !packageJson.files.includes(requiredFile)) {
124
+ fail(`package.json files[] must include ${requiredFile}`);
125
+ }
126
+ }
127
+ for (const requiredExport of ["./TRADEMARKS.md", "./registry.json", "./standards.json", "./kfd.release.json", "./site/kfd-site.json", "./buildchain.release-propagation.json", "./release-impact.json", "./buildchain/kfd-1/contract-world.witness.json", "./buildchain/kfd-2/public-release-trust.claim.json", "./buildchain/kfd-3/collaboration-interface.json", "./buildchain/kfd-3/collaboration-interface.prebuild.json", "./buildchain/kfd-3/collaboration-interface.artifact.json", "./schemas/*.json", "./schemas/*/*.json"]) {
128
+ if (!packageJson.exports || !packageJson.exports[requiredExport]) {
129
+ fail(`package.json exports must include ${requiredExport}`);
130
+ }
131
+ }
132
+
133
+ const seen = new Set();
134
+ const statuses = new Set(["draft", "active", "superseded"]);
135
+ const kinds = new Set(["principle", "procedure"]);
136
+ const superseded = new Map();
137
+ for (const e of registry.entries) {
138
+ if (!Number.isInteger(e.number) || e.number < 1) fail(`bad number ${e.number}`);
139
+ if (seen.has(e.number)) fail(`duplicate number ${e.number}`);
140
+ seen.add(e.number);
141
+ if (e.id !== `KFD-${e.number}`) fail(`id ${e.id} does not match number ${e.number}`);
142
+ if (e.slug !== `kfd-${e.number}`) fail(`${e.id} slug must be kfd-${e.number}, not ${e.slug}`);
143
+ if (e.path !== `decisions/kfd-${e.number}.md`) {
144
+ fail(`${e.id} path must be decisions/kfd-${e.number}.md, not ${e.path}`);
145
+ }
146
+ if (!kinds.has(e.kind)) fail(`bad kind ${e.kind} on ${e.id}`);
147
+ if (!statuses.has(e.status)) fail(`bad status ${e.status} on ${e.id}`);
148
+ if (e.status === "superseded") {
149
+ if (!Array.isArray(e.supersededBy) || e.supersededBy.length === 0) {
150
+ fail(`${e.id} is superseded but does not declare supersededBy`);
151
+ } else {
152
+ superseded.set(e.id, e.supersededBy);
153
+ }
154
+ }
155
+ if (!existsSync(e.path)) fail(`missing document ${e.path} for ${e.id}`);
156
+ else {
157
+ const doc = readFileSync(e.path, "utf8");
158
+ if (!doc.startsWith(`# ${e.id}:`)) fail(`${e.path} heading does not open with "# ${e.id}:"`);
159
+ if (!doc.includes(`Status: ${e.status}`)) fail(`${e.path} status line does not say ${e.status}`);
160
+ if (!doc.includes(`Kind: ${e.kind}`)) fail(`${e.path} kind line does not say ${e.kind}`);
161
+ if (e.status === "superseded") {
162
+ for (const successor of superseded.get(e.id) ?? []) {
163
+ if (!doc.includes(successor)) fail(`${e.path} does not cite successor ${successor}`);
164
+ }
165
+ }
166
+ }
167
+ const standard = standardsMetadata.standards?.[e.slug];
168
+ if (!standard) fail(`standards metadata missing ${e.slug}`);
169
+ else {
170
+ requireFields(standard, standardsSchema.$defs?.standard?.required, `${e.id} standards metadata`);
171
+ if (standard.key !== e.slug) fail(`${e.id} standard key must be ${e.slug}`);
172
+ if (standard.id !== e.id) fail(`${e.id} standard id mismatch in standards metadata`);
173
+ if (standard.number !== e.number) fail(`${e.id} standard number mismatch in standards metadata`);
174
+ if (standard.label !== e.id) fail(`${e.id} standard label must be ${e.id}`);
175
+ if (standard.title !== e.title) fail(`${e.id} standard title must match registry title`);
176
+ if (standard.kind !== e.kind) fail(`${e.id} standard kind must match registry kind`);
177
+ if (standard.status !== e.status) fail(`${e.id} standard status must match registry status`);
178
+ if (standard.document?.path !== e.path) fail(`${e.id} standard document path must match registry path`);
179
+ if (standard.document?.url !== e.url) fail(`${e.id} standard document url must match registry url`);
180
+ if (!/^[0-9a-f]{64}$/.test(standard.document?.sha256 || "")) {
181
+ fail(`${e.id} standard document sha256 is required`);
182
+ } else if (existsSync(e.path) && standard.document.sha256 !== sha256File(e.path)) {
183
+ fail(`${e.id} standard document sha256 does not match ${e.path}`);
184
+ }
185
+ if (standard.metadataSchemaVersion !== standardsMetadata.metadataSchema?.version) {
186
+ fail(`${e.id} standard metadataSchemaVersion must match metadata schema version`);
187
+ }
188
+ if (standard.schemaIds?.metadata !== standardsMetadata.metadataSchema?.id) {
189
+ fail(`${e.id} standard schemaIds.metadata must match metadata schema id`);
190
+ }
191
+ if (standard.schemaPaths?.metadata !== standardsMetadata.metadataSchema?.path) {
192
+ fail(`${e.id} standard schemaPaths.metadata must match metadata schema path`);
193
+ }
194
+ for (const [name, schemaPath] of Object.entries(standard.schemaPaths ?? {})) {
195
+ if (!existsSync(schemaPath)) fail(`${e.id} schemaPaths.${name} points to missing ${schemaPath}`);
196
+ else if (name !== "metadata") {
197
+ const schemaDoc = JSON.parse(readFileSync(schemaPath, "utf8"));
198
+ if (standard.schemaIds?.[name] && schemaDoc.$id !== standard.schemaIds[name]) {
199
+ fail(`${e.id} schemaPaths.${name} $id must match schemaIds.${name}`);
200
+ }
201
+ }
202
+ }
203
+ for (const [name, iface] of Object.entries(standard.interfaces ?? {})) {
204
+ requireFields(
205
+ iface,
206
+ standardsSchema.$defs?.standard?.properties?.interfaces?.additionalProperties?.required,
207
+ `${e.id} interfaces.${name}`
208
+ );
209
+ if (iface.schemaId !== standard.schemaIds?.[name]) fail(`${e.id} interfaces.${name}.schemaId must match schemaIds.${name}`);
210
+ if (iface.schemaPath !== standard.schemaPaths?.[name]) fail(`${e.id} interfaces.${name}.schemaPath must match schemaPaths.${name}`);
211
+ if (!Number.isInteger(iface.schemaVersion) || iface.schemaVersion < 1) {
212
+ fail(`${e.id} interfaces.${name}.schemaVersion must be a positive integer`);
213
+ }
214
+ if (!iface.compatibilityRule) fail(`${e.id} interfaces.${name}.compatibilityRule is required`);
215
+ if (existsSync(iface.schemaPath)) {
216
+ const schemaDoc = JSON.parse(readFileSync(iface.schemaPath, "utf8"));
217
+ if (schemaDoc.properties?.schemaVersion?.const !== iface.schemaVersion) {
218
+ fail(`${e.id} interfaces.${name}.schemaVersion must match schema properties.schemaVersion.const`);
219
+ }
220
+ if (schemaDoc.properties?.contract?.const !== iface.contract) {
221
+ fail(`${e.id} interfaces.${name}.contract must match schema properties.contract.const`);
222
+ }
223
+ }
224
+ }
225
+ }
226
+ }
227
+ for (const key of Object.keys(standardsMetadata.standards ?? {})) {
228
+ if (!registry.entries.some((e) => e.slug === key)) fail(`standards metadata contains unknown ${key}`);
229
+ }
230
+ const kfd1 = standardsMetadata.standards?.["kfd-1"];
231
+ if (kfd1?.schemaIds?.contractWorld !== "https://kfd.libkungfu.dev/schemas/kfd-1/contract-world.schema.json") {
232
+ fail("KFD-1 standards metadata must expose the canonical contractWorld schema URI");
233
+ }
234
+ if (kfd1?.schemaIds?.witness !== "https://kfd.libkungfu.dev/schemas/kfd-1/witness.schema.json") {
235
+ fail("KFD-1 standards metadata must expose the canonical witness schema URI");
236
+ }
237
+ for (const concept of ["factSource", "contractWorld", "weldedSurfaceRegister", "witness"]) {
238
+ if (!kfd1?.concepts?.[concept]) fail(`KFD-1 standards metadata missing concept ${concept}`);
239
+ }
240
+ for (const iface of ["contractWorld", "witness"]) {
241
+ if (!kfd1?.interfaces?.[iface]) fail(`KFD-1 standards metadata missing interface ${iface}`);
242
+ }
243
+ const kfd2 = standardsMetadata.standards?.["kfd-2"];
244
+ if (kfd2?.schemaIds?.trustTaxonomy !== "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json") {
245
+ fail("KFD-2 standards metadata must expose the canonical trustTaxonomy schema URI");
246
+ }
247
+ if (kfd2?.schemaIds?.releaseClaims !== "https://kfd.libkungfu.dev/schemas/kfd-2/release-claims.schema.json") {
248
+ fail("KFD-2 standards metadata must expose the canonical releaseClaims schema URI");
249
+ }
250
+ if (kfd2?.schemaIds?.releaseTrustPassport !== "https://kfd.libkungfu.dev/schemas/kfd-2/release-trust-passport.schema.json") {
251
+ fail("KFD-2 standards metadata must expose the canonical releaseTrustPassport schema URI");
252
+ }
253
+ if (kfd2?.schemaPaths?.releaseClaims !== "schemas/kfd-2/release-claims.schema.json") {
254
+ fail("KFD-2 standards metadata must expose the releaseClaims schema path");
255
+ }
256
+ if (kfd2?.schemaPaths?.trustTaxonomy !== "schemas/kfd-2/trust-taxonomy.schema.json") {
257
+ fail("KFD-2 standards metadata must expose the trustTaxonomy schema path");
258
+ }
259
+ if (kfd2?.schemaPaths?.releaseTrustPassport !== "schemas/kfd-2/release-trust-passport.schema.json") {
260
+ fail("KFD-2 standards metadata must expose the releaseTrustPassport schema path");
261
+ }
262
+ const kfd2TrustTaxonomySchema = JSON.parse(readFileSync("schemas/kfd-2/trust-taxonomy.schema.json", "utf8"));
263
+ const kfd2ClaimsSchema = JSON.parse(readFileSync("schemas/kfd-2/release-claims.schema.json", "utf8"));
264
+ const kfd2TrustPassportSchema = JSON.parse(readFileSync("schemas/kfd-2/release-trust-passport.schema.json", "utf8"));
265
+ if (kfd2TrustTaxonomySchema.properties?.contract?.const !== "kfd-2-trust-taxonomy") {
266
+ fail("KFD-2 trustTaxonomy schema must describe the kfd-2-trust-taxonomy contract");
267
+ }
268
+ if (kfd2ClaimsSchema.properties?.contract?.const !== "kfd-2-release-claims") {
269
+ fail("KFD-2 releaseClaims schema must describe the kfd-2-release-claims contract");
270
+ }
271
+ if (kfd2TrustPassportSchema.properties?.contract?.const !== "kfd-2-release-trust-passport") {
272
+ fail("KFD-2 releaseTrustPassport schema must describe the kfd-2-release-trust-passport contract");
273
+ }
274
+ const taxonomyMeta = kfd2TrustTaxonomySchema["x-kfd"];
275
+ if (taxonomyMeta?.extensionPolicy?.unknownValuePolicy !== "schema-validation-fail") {
276
+ fail("KFD-2 trust taxonomy must fail unknown values");
277
+ }
278
+ if (taxonomyMeta?.extensionPolicy?.standardAction !== "open-kfd-extension-issue") {
279
+ fail("KFD-2 trust taxonomy must tell agents to open a KFD extension issue for missing values");
280
+ }
281
+ if (taxonomyMeta?.extensionPolicy?.requestPath?.repository !== "https://github.com/kungfu-systems/kfd") {
282
+ fail("KFD-2 trust taxonomy extension path must target the KFD GitHub repository");
283
+ }
284
+ if (taxonomyMeta?.extensionPolicy?.requestPath?.kind !== "github-issue") {
285
+ fail("KFD-2 trust taxonomy extension path must be a GitHub issue");
286
+ }
287
+ for (const enumName of ["riskType", "trustImpact", "machineProvability", "agentAction"]) {
288
+ requireSameEnum(
289
+ kfd2TrustTaxonomySchema.$defs?.[enumName]?.enum,
290
+ taxonomyMeta?.allowedValues?.[enumName],
291
+ `KFD-2 trust taxonomy ${enumName}`
292
+ );
293
+ }
294
+ const residualRiskRef = "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk";
295
+ const downgradeReasonRef = "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/downgradeReason";
296
+ const checkResidualRisk = (risk, label) => {
297
+ if (risk?.definedBy !== residualRiskRef) fail(`${label}.definedBy must reference the KFD-2 trust taxonomy residualRisk definition`);
298
+ for (const [field, enumName] of [
299
+ ["riskType", "riskType"],
300
+ ["trustImpact", "trustImpact"],
301
+ ["machineProvability", "machineProvability"],
302
+ ["agentAction", "agentAction"],
303
+ ]) {
304
+ if (!kfd2TrustTaxonomySchema.$defs?.[enumName]?.enum?.includes(risk?.[field])) {
305
+ fail(`${label}.${field} must be a KFD-2 trust taxonomy value`);
306
+ }
307
+ }
308
+ if (!risk?.reason) fail(`${label}.reason is required`);
309
+ if (!risk?.owner) fail(`${label}.owner is required`);
310
+ };
311
+ if (kfd2ClaimsSchema.$defs?.claim?.properties?.residualRisk?.items?.$ref !== residualRiskRef) {
312
+ fail("KFD-2 releaseClaims residualRisk must reference the KFD-2 trust taxonomy residualRisk definition");
313
+ }
314
+ if (kfd2TrustPassportSchema.$defs?.claimResult?.properties?.residualRisk?.items?.$ref !== residualRiskRef) {
315
+ fail("KFD-2 releaseTrustPassport claim residualRisk must reference the KFD-2 trust taxonomy residualRisk definition");
316
+ }
317
+ if (kfd2TrustPassportSchema.properties?.downgradeReasons?.items?.$ref !== downgradeReasonRef) {
318
+ fail("KFD-2 releaseTrustPassport downgradeReasons must reference the KFD-2 trust taxonomy downgradeReason definition");
319
+ }
320
+ for (const concept of ["facts", "releaseClaim", "releaseClaims", "evidenceBinding", "auditBoundary", "residualRisk", "riskType", "trustImpact", "machineProvability", "agentAction", "extensionRequest", "releaseTrustPassport", "responsibilityState", "trust"]) {
321
+ if (!kfd2?.concepts?.[concept]) fail(`KFD-2 standards metadata missing concept ${concept}`);
322
+ }
323
+ for (const iface of ["trustTaxonomy", "releaseClaims", "releaseTrustPassport"]) {
324
+ if (!kfd2?.interfaces?.[iface]) fail(`KFD-2 standards metadata missing interface ${iface}`);
325
+ }
326
+ const kfd3 = standardsMetadata.standards?.["kfd-3"];
327
+ if (kfd3?.schemaIds?.collaborationInterface !== "https://kfd.libkungfu.dev/schemas/kfd-3/collaboration-interface.schema.json") {
328
+ fail("KFD-3 standards metadata must expose the canonical collaborationInterface schema URI");
329
+ }
330
+ if (kfd3?.schemaIds?.witness !== "https://kfd.libkungfu.dev/schemas/kfd-3/witness.schema.json") {
331
+ fail("KFD-3 standards metadata must expose the canonical witness schema URI");
332
+ }
333
+ if (kfd3?.schemaPaths?.collaborationInterface !== "schemas/kfd-3/collaboration-interface.schema.json") {
334
+ fail("KFD-3 standards metadata must expose the collaborationInterface schema path");
335
+ }
336
+ if (kfd3?.schemaPaths?.witness !== "schemas/kfd-3/witness.schema.json") {
337
+ fail("KFD-3 standards metadata must expose the witness schema path");
338
+ }
339
+ const kfd3CollaborationSchema = JSON.parse(readFileSync("schemas/kfd-3/collaboration-interface.schema.json", "utf8"));
340
+ const kfd3WitnessSchema = JSON.parse(readFileSync("schemas/kfd-3/witness.schema.json", "utf8"));
341
+ if (kfd3CollaborationSchema.properties?.contract?.const !== "kfd-3-collaboration-interface") {
342
+ fail("KFD-3 collaborationInterface schema must describe the kfd-3-collaboration-interface contract");
343
+ }
344
+ if (kfd3WitnessSchema.properties?.contract?.const !== "kfd-3-witness") {
345
+ fail("KFD-3 witness schema must describe the kfd-3-witness contract");
346
+ }
347
+ if (kfd3WitnessSchema.properties?.residualRisk?.items?.$ref !== residualRiskRef) {
348
+ fail("KFD-3 witness residualRisk must reference the KFD-2 trust taxonomy residualRisk definition");
349
+ }
350
+ if (!kfd3CollaborationSchema.properties?.extensionRequests) {
351
+ fail("KFD-3 collaborationInterface schema must expose extensionRequests");
352
+ }
353
+ if (!kfd3CollaborationSchema.properties?.factSources) {
354
+ fail("KFD-3 collaborationInterface schema must expose factSources");
355
+ }
356
+ if (!kfd3CollaborationSchema.$defs?.extensionRequest?.properties?.requestPath?.properties?.kind?.enum?.includes("github-issue")) {
357
+ fail("KFD-3 extensionRequest.requestPath.kind must support github-issue");
358
+ }
359
+ for (const concept of ["participant", "collaborationInterface", "minimalEntrypoint", "closure", "choicePath", "extensionRequest", "extensionPath"]) {
360
+ if (!kfd3?.concepts?.[concept]) fail(`KFD-3 standards metadata missing concept ${concept}`);
361
+ }
362
+ for (const iface of ["collaborationInterface", "witness"]) {
363
+ if (!kfd3?.interfaces?.[iface]) fail(`KFD-3 standards metadata missing interface ${iface}`);
364
+ }
365
+ for (const [id, successors] of superseded) {
366
+ for (const successor of successors) {
367
+ if (!registry.entries.some((e) => e.id === successor)) fail(`${id} cites missing successor ${successor}`);
368
+ }
369
+ }
370
+ const siteCommitments = new Map((siteBundle.homepage?.foundationTriad?.commitments ?? []).map((item) => [item.id, item]));
371
+ for (const e of registry.entries) {
372
+ if (!siteCommitments.has(e.id)) fail(`site bundle foundationTriad missing ${e.id}`);
373
+ }
374
+ const siteLayers = new Map((siteBundle.homepage?.foundationModel?.layers ?? []).map((item) => [item.decision, item]));
375
+ for (const e of registry.entries) {
376
+ if (!siteLayers.has(e.id)) fail(`site bundle foundationModel missing ${e.id}`);
377
+ }
378
+ const boundary = siteBundle.renderingBoundary ?? {};
379
+ if (!Array.isArray(boundary.ownedByKfd) || !boundary.ownedByKfd.includes("homepage title and text")) {
380
+ fail("site bundle renderingBoundary.ownedByKfd must include homepage title and text");
381
+ }
382
+ if (!Array.isArray(boundary.ownedByKfd) || !boundary.ownedByKfd.includes("homepage section projection from README.md")) {
383
+ fail("site bundle renderingBoundary.ownedByKfd must include homepage section projection from README.md");
384
+ }
385
+ if (!Array.isArray(boundary.ownedBySite) || !boundary.ownedBySite.includes("CSS")) {
386
+ fail("site bundle renderingBoundary.ownedBySite must include CSS");
387
+ }
388
+ if (!kfd1Witness) {
389
+ fail(`missing KFD-1 release witness ${kfd1WitnessPath}`);
390
+ } else {
391
+ if (kfd1Witness.standard !== "kfd-1") fail("KFD-1 release witness standard must be kfd-1");
392
+ if (kfd1Witness.contractWorld?.schemaId !== "https://kfd.libkungfu.dev/schemas/kfd-1/contract-world.schema.json") {
393
+ fail("KFD-1 release witness contractWorld.schemaId must be canonical");
394
+ }
395
+ for (const field of ["canonicalPolicy", "registry"]) {
396
+ const entry = kfd1Witness[field];
397
+ if (!entry?.path || !entry?.sha256) fail(`KFD-1 release witness ${field} must include path and sha256`);
398
+ else if (!existsSync(entry.path)) fail(`KFD-1 release witness ${field} points to missing ${entry.path}`);
399
+ else if (entry.sha256 !== sha256File(entry.path)) fail(`KFD-1 release witness ${field}.sha256 does not match ${entry.path}`);
400
+ }
401
+ if (!Array.isArray(kfd1Witness.surfaces) || kfd1Witness.surfaces.length === 0) {
402
+ fail("KFD-1 release witness surfaces[] is required");
403
+ } else {
404
+ const witnessedSurfaceNames = new Set(kfd1Witness.surfaces.map((surface) => surface.name));
405
+ for (const requiredSurface of [
406
+ "readme",
407
+ "kfd-2-trust-taxonomy-schema",
408
+ "kfd-site-bundle",
409
+ "kfd-doc-map",
410
+ "kfd-2-release-trust-doc",
411
+ "kfd-3-collaboration-interface-doc",
412
+ "release-impact-ledger",
413
+ "kfd-check-gate"
414
+ ]) {
415
+ if (!witnessedSurfaceNames.has(requiredSurface)) {
416
+ fail(`KFD-1 release witness must include self-proof surface ${requiredSurface}`);
417
+ }
418
+ }
419
+ for (const [index, surface] of kfd1Witness.surfaces.entries()) {
420
+ if (!surface.name) fail(`KFD-1 release witness surfaces[${index}].name is required`);
421
+ if (!surface.artifactPath) fail(`KFD-1 release witness surfaces[${index}].artifactPath is required`);
422
+ else if (!existsSync(surface.artifactPath)) fail(`KFD-1 release witness surface ${surface.name || index} points to missing ${surface.artifactPath}`);
423
+ else {
424
+ const actual = sha256File(surface.artifactPath);
425
+ if (surface.expectedSha256 !== actual) {
426
+ fail(`KFD-1 release witness surface ${surface.name || index} expectedSha256 does not match ${surface.artifactPath}`);
427
+ }
428
+ if (surface.sourceSha256 && surface.sourceSha256 !== actual) {
429
+ fail(`KFD-1 release witness surface ${surface.name || index} sourceSha256 does not match ${surface.artifactPath}`);
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ const checkKfd2Pointer = (entry, label) => {
437
+ if (!entry?.path) {
438
+ fail(`${label} must include path`);
439
+ return;
440
+ }
441
+ if (!entry?.sha256) {
442
+ fail(`${label} must include sha256`);
443
+ return;
444
+ }
445
+ const filePath = hashablePath(entry.path);
446
+ if (!existsSync(filePath)) {
447
+ fail(`${label} points to missing ${filePath}`);
448
+ } else if (entry.sha256 !== sha256File(filePath)) {
449
+ fail(`${label}.sha256 does not match ${filePath}`);
450
+ }
451
+ };
452
+
453
+ if (!kfd2Claim) {
454
+ fail(`missing KFD-2 public release trust claim ${kfd2ClaimPath}`);
455
+ } else {
456
+ if (kfd2Claim.id !== "kfd-public-release-trust") fail("KFD-2 release trust claim id must be kfd-public-release-trust");
457
+ if (kfd2Claim.public !== true) fail("KFD-2 release trust claim must be public");
458
+ if (!kfd2Claim.claim) fail("KFD-2 release trust claim must include claim text");
459
+ for (const field of ["sourceBindings", "machineEvidence", "artifacts", "residualRisk"]) {
460
+ if (!Array.isArray(kfd2Claim[field])) fail(`KFD-2 release trust claim ${field} must be an array`);
461
+ }
462
+ if (!Array.isArray(kfd2Claim.sourceBindings) || kfd2Claim.sourceBindings.length === 0) {
463
+ fail("KFD-2 release trust claim sourceBindings[] is required");
464
+ } else {
465
+ kfd2Claim.sourceBindings.forEach((entry, index) => checkKfd2Pointer(entry, `KFD-2 release trust claim sourceBindings[${index}]`));
466
+ }
467
+ if (!Array.isArray(kfd2Claim.machineEvidence) || kfd2Claim.machineEvidence.length === 0) {
468
+ fail("KFD-2 release trust claim machineEvidence[] is required");
469
+ } else {
470
+ kfd2Claim.machineEvidence.forEach((entry, index) => checkKfd2Pointer(entry, `KFD-2 release trust claim machineEvidence[${index}]`));
471
+ }
472
+ if (!kfd2Claim.hashes || Object.keys(kfd2Claim.hashes).length === 0) fail("KFD-2 release trust claim hashes must be non-empty");
473
+ for (const entry of [...(kfd2Claim.sourceBindings ?? []), ...(kfd2Claim.machineEvidence ?? [])]) {
474
+ if (entry?.id && kfd2Claim.hashes?.[entry.id] !== entry.sha256) {
475
+ fail(`KFD-2 release trust claim hashes.${entry.id} must match its bound sha256`);
476
+ }
477
+ }
478
+ if (!Array.isArray(kfd2Claim.artifacts) || kfd2Claim.artifacts.length === 0) {
479
+ fail("KFD-2 release trust claim artifacts[] is required");
480
+ } else {
481
+ kfd2Claim.artifacts.forEach((entry, index) => checkKfd2Pointer(entry, `KFD-2 release trust claim artifacts[${index}]`));
482
+ }
483
+ if (kfd2Claim.verification?.result !== "passed") fail("KFD-2 release trust claim verification.result must be passed");
484
+ if (kfd2Claim.verification?.command !== "node scripts/check.mjs") fail("KFD-2 release trust claim verification.command must be node scripts/check.mjs");
485
+ if (!kfd2Claim.auditBoundary?.scope) fail("KFD-2 release trust claim auditBoundary.scope is required");
486
+ if (kfd2Claim.auditBoundary?.enumerability !== "closed-world") fail("KFD-2 release trust claim auditBoundary.enumerability must be closed-world");
487
+ if (!kfd2Claim.responsibility?.owner) fail("KFD-2 release trust claim responsibility.owner is required");
488
+ if (!Array.isArray(kfd2Claim.residualRisk)) fail("KFD-2 release trust claim residualRisk must be an array");
489
+ }
490
+
491
+ const kfd3SurfaceGroups = ["docs", "schemas", "standardsMetadata", "packageExports", "siteConsumptionContracts"];
492
+ const kfd3SurfaceIds = (witness) => {
493
+ const ids = new Set();
494
+ for (const surface of witness?.surfaces ?? []) {
495
+ if (surface?.id) ids.add(surface.id);
496
+ }
497
+ for (const group of kfd3SurfaceGroups) {
498
+ for (const surface of witness?.[group] ?? []) {
499
+ if (surface?.id) ids.add(surface.id);
500
+ }
501
+ }
502
+ return ids;
503
+ };
504
+ const checkPointer = (entry, label, field = "path") => {
505
+ if (!entry?.[field]) {
506
+ fail(`${label} must include ${field}`);
507
+ return;
508
+ }
509
+ const filePath = hashablePath(entry[field]);
510
+ if (filePath.includes("*")) {
511
+ return;
512
+ }
513
+ if (!existsSync(filePath)) {
514
+ fail(`${label} points to missing ${filePath}`);
515
+ } else if (entry.sha256 && entry.sha256 !== sha256File(filePath)) {
516
+ fail(`${label}.sha256 does not match ${filePath}`);
517
+ }
518
+ };
519
+ const checkGroupedSurfaces = (witness, label) => {
520
+ for (const group of kfd3SurfaceGroups) {
521
+ if (!Array.isArray(witness?.[group]) || witness[group].length === 0) {
522
+ fail(`${label}.${group} must include at least one surface`);
523
+ continue;
524
+ }
525
+ for (const [index, surface] of witness[group].entries()) {
526
+ if (!surface?.id) fail(`${label}.${group}[${index}].id is required`);
527
+ checkPointer(surface, `${label}.${group}[${index}]`, "sourcePath");
528
+ }
529
+ }
530
+ };
531
+
532
+ if (!kfd3Interface) {
533
+ fail(`missing KFD-3 collaboration interface ${kfd3InterfacePath}`);
534
+ } else {
535
+ if (kfd3Interface.contract !== "kfd-3-collaboration-interface") fail("KFD-3 collaboration interface contract must be kfd-3-collaboration-interface");
536
+ if (kfd3Interface.standard !== "kfd-3") fail("KFD-3 collaboration interface standard must be kfd-3");
537
+ if (!Array.isArray(kfd3Interface.factSources) || kfd3Interface.factSources.length === 0) fail("KFD-3 collaboration interface factSources[] is required");
538
+ if (!kfd3Interface.factSources.some((entry) =>
539
+ entry.id === "public-kfd-fact-source" &&
540
+ entry.kind === "git-repository" &&
541
+ entry.host === "github" &&
542
+ entry.repository === "kungfu-systems/kfd" &&
543
+ entry.url === "https://github.com/kungfu-systems/kfd" &&
544
+ entry.loadBearingCoordinate === "commit-addressed repository contents" &&
545
+ entry.stableRenderedIndex === "https://kfd.libkungfu.dev" &&
546
+ entry.canonicalPaths?.includes("decisions/kfd-N.md") &&
547
+ entry.canonicalPaths?.includes("registry.json") &&
548
+ entry.canonicalPaths?.includes("standards.json")
549
+ )) {
550
+ fail("KFD-3 collaboration interface must declare the public KFD GitHub fact source");
551
+ }
552
+ if (!Array.isArray(kfd3Interface.participants) || kfd3Interface.participants.length === 0) fail("KFD-3 collaboration interface participants[] is required");
553
+ if (!Array.isArray(kfd3Interface.minimalEntrypoints) || kfd3Interface.minimalEntrypoints.length === 0) fail("KFD-3 collaboration interface minimalEntrypoints[] is required");
554
+ if (!kfd3Interface.minimalEntrypoints.some((entry) => entry.id === "official-status-and-trademarks" && entry.surface === "TRADEMARKS.md")) {
555
+ fail("KFD-3 collaboration interface must expose TRADEMARKS.md as an official-status-and-trademarks entrypoint");
556
+ }
557
+ if (!Array.isArray(kfd3Interface.surfaces) || kfd3Interface.surfaces.length === 0) fail("KFD-3 collaboration interface surfaces[] is required");
558
+ if (!kfd3Interface.surfaces.some((entry) => entry.id === "official-status-and-trademarks" && entry.discoverability?.path === "TRADEMARKS.md")) {
559
+ fail("KFD-3 collaboration interface must expose TRADEMARKS.md as a participant-facing surface");
560
+ }
561
+ if (!Array.isArray(kfd3Interface.extensionRequests) || kfd3Interface.extensionRequests.length === 0) fail("KFD-3 collaboration interface extensionRequests[] is required");
562
+ if (!kfd3Interface.extensionRequests.some((entry) => entry.id === "kfd-2-trust-taxonomy-extension" && entry.requestPath?.kind === "github-issue" && String(entry.requestPath?.target || "").startsWith("https://github.com/kungfu-systems/kfd/issues/new"))) {
563
+ fail("KFD-3 collaboration interface must declare the KFD-2 trust taxonomy GitHub issue extension path");
564
+ }
565
+ if (kfd3Interface.closure?.classificationMode !== "closed-world") fail("KFD-3 collaboration interface closure must be closed-world");
566
+ if (kfd3Interface.closure?.unclassifiedEntrypointsPolicy !== "fail") fail("KFD-3 collaboration interface unclassified entrypoint policy must fail");
567
+ }
568
+
569
+ const kfd3InterfaceDigest = existsSync(kfd3InterfacePath) ? `sha256:${sha256File(kfd3InterfacePath)}` : "";
570
+ if (!kfd3PrebuildWitness) {
571
+ fail(`missing KFD-3 pre-build witness ${kfd3PrebuildWitnessPath}`);
572
+ } else {
573
+ if (kfd3PrebuildWitness.contract !== "kungfu-buildchain-kfd-3-collaboration-interface-prebuild-witness") {
574
+ fail("KFD-3 pre-build witness contract must match Buildchain KFD-3 prebuild witness");
575
+ }
576
+ if (kfd3PrebuildWitness.standard !== "kfd-3") fail("KFD-3 pre-build witness standard must be kfd-3");
577
+ if (kfd3PrebuildWitness.sourceRegistry?.path !== kfd3InterfacePath) fail("KFD-3 pre-build witness sourceRegistry.path must point to the collaboration interface");
578
+ if (kfd3PrebuildWitness.sourceRegistry?.sha256 !== sha256File(kfd3InterfacePath)) fail("KFD-3 pre-build witness sourceRegistry.sha256 does not match the collaboration interface");
579
+ if (kfd3PrebuildWitness.collaborationInterfaceDigest !== kfd3InterfaceDigest) fail("KFD-3 pre-build witness collaborationInterfaceDigest does not match the collaboration interface");
580
+ for (const [index, risk] of (kfd3PrebuildWitness.residualRisk ?? []).entries()) {
581
+ checkResidualRisk(risk, `KFD-3 pre-build witness residualRisk[${index}]`);
582
+ }
583
+ for (const [index, risk] of (kfd3PrebuildWitness.auditBoundary?.nonExhaustivelyEnumerableSurfaces ?? []).entries()) {
584
+ checkResidualRisk(risk, `KFD-3 pre-build witness auditBoundary.nonExhaustivelyEnumerableSurfaces[${index}]`);
585
+ }
586
+ checkGroupedSurfaces(kfd3PrebuildWitness, "KFD-3 pre-build witness");
587
+ }
588
+ if (!kfd3ArtifactWitness) {
589
+ fail(`missing KFD-3 artifact witness ${kfd3ArtifactWitnessPath}`);
590
+ } else {
591
+ if (kfd3ArtifactWitness.contract !== "kfd-3-witness") fail("KFD-3 artifact witness must also satisfy the KFD-3 witness contract");
592
+ if (kfd3ArtifactWitness.standard !== "kfd-3") fail("KFD-3 artifact witness standard must be kfd-3");
593
+ if (kfd3ArtifactWitness.collaborationInterface?.schemaId !== "https://kfd.libkungfu.dev/schemas/kfd-3/collaboration-interface.schema.json") {
594
+ fail("KFD-3 artifact witness collaborationInterface.schemaId must be canonical");
595
+ }
596
+ if (kfd3ArtifactWitness.collaborationInterface?.digest !== kfd3InterfaceDigest) {
597
+ fail("KFD-3 artifact witness collaborationInterface.digest does not match the collaboration interface");
598
+ }
599
+ if (kfd3ArtifactWitness.sourceRegistry?.path !== kfd3InterfacePath) fail("KFD-3 artifact witness sourceRegistry.path must point to the collaboration interface");
600
+ if (kfd3ArtifactWitness.sourceRegistry?.sha256 !== sha256File(kfd3InterfacePath)) fail("KFD-3 artifact witness sourceRegistry.sha256 does not match the collaboration interface");
601
+ for (const [index, risk] of (kfd3ArtifactWitness.residualRisk ?? []).entries()) {
602
+ checkResidualRisk(risk, `KFD-3 artifact witness residualRisk[${index}]`);
603
+ }
604
+ if (kfd3ArtifactWitness.closure?.classificationMode !== "closed-world") fail("KFD-3 artifact witness closure must be closed-world");
605
+ if (!Array.isArray(kfd3ArtifactWitness.closure?.unclassifiedEntrypoints) || kfd3ArtifactWitness.closure.unclassifiedEntrypoints.length !== 0) {
606
+ fail("KFD-3 artifact witness must have zero unclassifiedEntrypoints");
607
+ }
608
+ for (const [section, entries] of Object.entries(kfd3ArtifactWitness.evidence ?? {})) {
609
+ if (Array.isArray(entries)) {
610
+ for (const [index, entry] of entries.entries()) {
611
+ checkPointer(entry, `KFD-3 artifact witness evidence.${section}[${index}]`);
612
+ }
613
+ }
614
+ }
615
+ checkGroupedSurfaces(kfd3ArtifactWitness, "KFD-3 artifact witness");
616
+ }
617
+ if (kfd3PrebuildWitness && kfd3ArtifactWitness) {
618
+ const declared = kfd3SurfaceIds(kfd3PrebuildWitness);
619
+ const exposed = kfd3SurfaceIds(kfd3ArtifactWitness);
620
+ for (const id of declared) {
621
+ if (!exposed.has(id)) fail(`KFD-3 artifact witness missing declared surface ${id}`);
622
+ }
623
+ for (const id of exposed) {
624
+ if (!declared.has(id)) fail(`KFD-3 artifact witness exposes undeclared surface ${id}`);
625
+ }
626
+ }
627
+
628
+ const impactLevels = new Set(["patch", "minor", "major"]);
629
+ const requiredSurfaces = new Set(["kfd-content", "kfd-registry-schema", "kfd-standards-metadata", "kfd-package-structure", "kfd-2-public-release-trust-claim"]);
630
+
631
+ if (releaseImpact.schemaVersion !== 1) fail(`unsupported release-impact schemaVersion ${releaseImpact.schemaVersion}`);
632
+ if (releaseImpact.contract !== "kungfu-buildchain-impact") fail(`unexpected release-impact contract ${releaseImpact.contract}`);
633
+ if (!releaseImpact.versionImpact || !impactLevels.has(releaseImpact.versionImpact.final)) {
634
+ fail("release-impact versionImpact.final must be patch, minor, or major");
635
+ }
636
+ if (!releaseImpact.versionImpact?.rationale) {
637
+ fail("release-impact versionImpact.rationale is required");
638
+ }
639
+ if (!Array.isArray(releaseImpact.surfaceImpacts) || releaseImpact.surfaceImpacts.length === 0) {
640
+ fail("release-impact surfaceImpacts[] is required");
641
+ } else {
642
+ const seenSurfaces = new Set();
643
+ for (const [index, surface] of releaseImpact.surfaceImpacts.entries()) {
644
+ if (!surface.id) fail(`release-impact surfaceImpacts[${index}].id is required`);
645
+ else seenSurfaces.add(surface.id);
646
+ if (!impactLevels.has(surface.impact)) {
647
+ fail(`release-impact surfaceImpacts[${index}].impact must be patch, minor, or major`);
648
+ }
649
+ if (!surface.rationale) fail(`release-impact surfaceImpacts[${index}].rationale is required`);
650
+ }
651
+ for (const surfaceId of requiredSurfaces) {
652
+ if (!seenSurfaces.has(surfaceId)) fail(`release-impact missing surface ${surfaceId}`);
653
+ }
654
+ }
655
+
656
+ if (process.exitCode) process.exit(process.exitCode);
657
+ console.log(`check: ${registry.entries.length} entries ok; release impact ok`);