@kungfu-tech/buildchain 2.8.5-alpha.0 → 2.8.5-alpha.2

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.
@@ -4,7 +4,7 @@
4
4
  "product": {
5
5
  "name": "Buildchain",
6
6
  "package": "@kungfu-tech/buildchain",
7
- "version": "2.8.5-alpha.0",
7
+ "version": "2.8.5-alpha.2",
8
8
  "repository": "https://github.com/kungfu-systems/buildchain"
9
9
  },
10
10
  "majorLine": "v2",
@@ -193,7 +193,7 @@
193
193
  "release-state SHA is recorded as a durable audit entrance"
194
194
  ],
195
195
  "breakingDigest": "sha256:b627e1b2ece9b6049517a942c5139e342e6077cdd3d8962d61cef8481b1f70ad",
196
- "auditDigest": "sha256:f1c2d70510f074102c2e9ff87b638b7b9b33edaa88aad8abc1e73e75e46a6af7"
196
+ "auditDigest": "sha256:3476a0837292b81a6efeb7c989dc1ee975b2bd8e8882273fd4eb5f865d330bc1"
197
197
  },
198
198
  {
199
199
  "contractVersion": 1,
@@ -218,7 +218,7 @@
218
218
  "KFD self contract witnesses record source/artifact hashes, self-hosting boundary, and responsibility state"
219
219
  ],
220
220
  "breakingDigest": "sha256:f46c1f67b94957ad956cf70db59a73fd5690bd67c91edf9c2d0eb8f3d1e3cf8d",
221
- "auditDigest": "sha256:6a99ee108c385837ee9f1dbb75848f6ea434b6474977e6e567ae81f8fc9a555c"
221
+ "auditDigest": "sha256:534707144fa2c440fe6958073a8a161790f12b8fb4695e3b06e43f43a6c87ffb"
222
222
  },
223
223
  {
224
224
  "contractVersion": 1,
@@ -242,7 +242,7 @@
242
242
  "prose-only public claims downgrade the release trust passport audit"
243
243
  ],
244
244
  "breakingDigest": "sha256:f0b636eb925ddbf45ff1a8872454ab7b4dd98857cac12bb75217830130e62a16",
245
- "auditDigest": "sha256:f1c2d70510f074102c2e9ff87b638b7b9b33edaa88aad8abc1e73e75e46a6af7"
245
+ "auditDigest": "sha256:3476a0837292b81a6efeb7c989dc1ee975b2bd8e8882273fd4eb5f865d330bc1"
246
246
  },
247
247
  {
248
248
  "contractVersion": 1,
@@ -271,7 +271,7 @@
271
271
  "KFD-3 passports expose releaseStatus, witness hashes, declared capability verification, reverse audit boundary, residual risk, and responsibility state"
272
272
  ],
273
273
  "breakingDigest": "sha256:33654f3444c893754aacc42dde3051f0a371bd17c6ef7c657c579a07159cd7c1",
274
- "auditDigest": "sha256:6a99ee108c385837ee9f1dbb75848f6ea434b6474977e6e567ae81f8fc9a555c"
274
+ "auditDigest": "sha256:534707144fa2c440fe6958073a8a161790f12b8fb4695e3b06e43f43a6c87ffb"
275
275
  },
276
276
  {
277
277
  "contractVersion": 1,
@@ -342,9 +342,9 @@
342
342
  "agents can discover supported Node APIs without importing internal file paths"
343
343
  ],
344
344
  "breakingDigest": "sha256:48f925608d3e2131d90936b07dc2a30341204cae3e6e785c0f77d61ad755c945",
345
- "auditDigest": "sha256:2d1c150464effb5e8ad5d5a6ef78ee71899b8bd74a46c7b7a7a01fec1e35c86a"
345
+ "auditDigest": "sha256:0660c503639f5ca6d23ba9c7492b31ee9b05f036ffe76ed5dc8b6cb3bfde367b"
346
346
  }
347
347
  ],
348
348
  "compatibilityDigest": "sha256:36421b592a782decbfe31a7d8d42d70ce1100a56a2c4f4a0ccc21f0617dc72bf",
349
- "contractDigest": "sha256:7b6cf45302dbcdf36d469e19095fe84931bf4a4f5cbe03ebeaae543f9a8121fc"
349
+ "contractDigest": "sha256:5901bfd2f57fc2cb2e6d4da7fb3c2d401111917db86bd404a3a27193d1a522fa"
350
350
  }
@@ -50,7 +50,7 @@
50
50
  "specifier": "@kungfu-tech/buildchain/kfd-gate",
51
51
  "export": "./kfd-gate",
52
52
  "target": "./packages/core/kfd-gate.js",
53
- "digest": "sha256:6a99ee108c385837ee9f1dbb75848f6ea434b6474977e6e567ae81f8fc9a555c"
53
+ "digest": "sha256:534707144fa2c440fe6958073a8a161790f12b8fb4695e3b06e43f43a6c87ffb"
54
54
  },
55
55
  {
56
56
  "specifier": "@kungfu-tech/buildchain/release-candidate",
@@ -62,7 +62,7 @@
62
62
  "specifier": "@kungfu-tech/buildchain/release-passport",
63
63
  "export": "./release-passport",
64
64
  "target": "./packages/core/release-passport.js",
65
- "digest": "sha256:f1c2d70510f074102c2e9ff87b638b7b9b33edaa88aad8abc1e73e75e46a6af7"
65
+ "digest": "sha256:3476a0837292b81a6efeb7c989dc1ee975b2bd8e8882273fd4eb5f865d330bc1"
66
66
  },
67
67
  {
68
68
  "specifier": "@kungfu-tech/buildchain/release-propagation",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.8.5-alpha.0",
3
+ "version": "2.8.5-alpha.2",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -98,7 +98,7 @@
98
98
  "vitest": "^4.1.9"
99
99
  },
100
100
  "dependencies": {
101
- "@kungfu-tech/kfd": "1.0.0-alpha.3",
101
+ "@kungfu-tech/kfd": "github:kungfu-systems/kfd#1731900b81150ae29095329892b8d7d5670f2314",
102
102
  "smol-toml": "1.7.0"
103
103
  }
104
104
  }
@@ -58,6 +58,14 @@ function readJsonPackageExport(exportPath) {
58
58
  return JSON.parse(fs.readFileSync(require.resolve(exportPath), "utf8"));
59
59
  }
60
60
 
61
+ function tryReadJsonPackageExport(exportPath) {
62
+ try {
63
+ return readJsonPackageExport(exportPath);
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ }
68
+
61
69
  function loadKfdPackageMetadata() {
62
70
  const packageJson = readJsonPackageExport("@kungfu-tech/kfd/package.json");
63
71
  const standards = readJsonPackageExport("@kungfu-tech/kfd/standards.json");
@@ -148,6 +156,50 @@ export function resolveKfd3Metadata({ requireSchemas = false } = {}) {
148
156
  };
149
157
  }
150
158
 
159
+ export function resolveKfd2Metadata({ requireTrustTaxonomy = false } = {}) {
160
+ const { packageJson, standards } = loadKfdPackageMetadata();
161
+ const entries = Object.entries(standards.standards || {});
162
+ const entry = entries.find(([key, value]) => {
163
+ const names = [key, value?.key, value?.id, value?.label]
164
+ .filter(Boolean)
165
+ .map((name) => String(name).toLowerCase());
166
+ return names.includes("kfd-2");
167
+ });
168
+ if (!entry) {
169
+ throw new Error("KFD metadata package does not expose KFD-2 metadata");
170
+ }
171
+ const [key, standard] = entry;
172
+ const schemaIds = { ...(standard.schemaIds || {}) };
173
+ const schemaPaths = { ...(standard.schemaPaths || {}) };
174
+ const trustTaxonomyPath = schemaPaths.trustTaxonomy || "";
175
+ const trustTaxonomy = trustTaxonomyPath
176
+ ? tryReadJsonPackageExport(`@kungfu-tech/kfd/${trustTaxonomyPath}`)
177
+ : undefined;
178
+ if (requireTrustTaxonomy && (!schemaIds.trustTaxonomy || !trustTaxonomyPath || !trustTaxonomy)) {
179
+ throw new Error("KFD metadata package does not expose the KFD-2 trust taxonomy schema");
180
+ }
181
+ return {
182
+ key: standard.key || key,
183
+ id: standard.id || standard.label || key,
184
+ label: standard.label || standard.id || key,
185
+ title: standard.title || "",
186
+ status: standard.status || "",
187
+ revision: standard.revision || 0,
188
+ package: {
189
+ name: packageJson.name,
190
+ version: packageJson.version,
191
+ repository: packageJson.repository?.url || "",
192
+ },
193
+ schemaIds,
194
+ schemaPaths,
195
+ hasTrustTaxonomySchema: Boolean(schemaIds.trustTaxonomy && trustTaxonomyPath && trustTaxonomy),
196
+ trustTaxonomy,
197
+ concepts: { ...(standard.concepts || {}) },
198
+ metadataSchema: standards.metadataSchema || {},
199
+ source: standards.source || {},
200
+ };
201
+ }
202
+
151
203
  function normalizeHash(value, label, { required = false } = {}) {
152
204
  const text = optionalString(value).replace(/^sha256:/, "").trim();
153
205
  if (!text && !required) {
@@ -391,6 +443,7 @@ function normalizeStringObjectArray(value, label) {
391
443
  throw new Error(`${label}[${index}] must be a string or object`);
392
444
  }
393
445
  return {
446
+ ...entry,
394
447
  id: optionalString(entry.id || entry.name || entry.surface || entry.path || entry.kind || `entry-${index}`),
395
448
  kind: optionalString(entry.kind || entry.type),
396
449
  reason: optionalString(entry.reason || entry.rationale || entry.note || entry.description),
@@ -400,6 +453,110 @@ function normalizeStringObjectArray(value, label) {
400
453
  .filter((entry) => entry.id || entry.reason);
401
454
  }
402
455
 
456
+ const KFD2_TAXONOMY_FIELDS = ["riskType", "trustImpact", "machineProvability", "agentAction"];
457
+
458
+ function kfdExtensionGuidance({ taxonomy = undefined, extensionRequests = [] } = {}) {
459
+ const requestPath = taxonomy?.["x-kfd"]?.extensionPolicy?.requestPath || {};
460
+ const kfd3Request = Array.isArray(extensionRequests)
461
+ ? extensionRequests.find((entry) => entry?.requestPath?.kind === "github-issue")
462
+ : undefined;
463
+ return {
464
+ action: taxonomy?.["x-kfd"]?.extensionPolicy?.standardAction || "open-kfd-extension-issue",
465
+ repository: requestPath.repository || kfd3Request?.requestPath?.target || "https://github.com/kungfu-systems/kfd",
466
+ issueUrl: requestPath.issueUrl || kfd3Request?.requestPath?.template || "https://github.com/kungfu-systems/kfd/issues/new?title=KFD-2%20trust%20taxonomy%20extension%20request",
467
+ appliesTo: requestPath.appliesTo || KFD2_TAXONOMY_FIELDS,
468
+ };
469
+ }
470
+
471
+ function taxonomyAllowedValues(taxonomy = {}) {
472
+ const xKfd = taxonomy?.["x-kfd"]?.allowedValues || {};
473
+ const defs = taxonomy?.$defs || {};
474
+ return {
475
+ riskType: new Set(xKfd.riskType || defs.riskType?.enum || []),
476
+ trustImpact: new Set(xKfd.trustImpact || defs.trustImpact?.enum || []),
477
+ machineProvability: new Set(xKfd.machineProvability || defs.machineProvability?.enum || []),
478
+ agentAction: new Set(xKfd.agentAction || defs.agentAction?.enum || []),
479
+ };
480
+ }
481
+
482
+ function validateTaxonomyField(entry, field, { allowed, label, guidance }) {
483
+ const value = optionalString(entry[field]).trim();
484
+ if (!value) {
485
+ throw new Error(`${label}.${field} is required by KFD-2 trust taxonomy; do not invent a local private value, ${guidance.action} at ${guidance.issueUrl}`);
486
+ }
487
+ if (!allowed[field]?.has(value)) {
488
+ throw new Error(`${label}.${field}=${value} is not in KFD-2 trust taxonomy; unknown taxonomy values fail validation, ${guidance.action} at ${guidance.issueUrl}`);
489
+ }
490
+ }
491
+
492
+ export function validateKfd2TrustTaxonomyEntry(entry, {
493
+ kind = "residualRisk",
494
+ label = kind,
495
+ metadata = resolveKfd2Metadata({ requireTrustTaxonomy: true }),
496
+ extensionRequests = [],
497
+ } = {}) {
498
+ const taxonomy = metadata.trustTaxonomy;
499
+ const taxonomyContract = taxonomy?.contract || taxonomy?.["x-kfd"]?.contract;
500
+ if (!taxonomy || taxonomyContract !== "kfd-2-trust-taxonomy") {
501
+ const guidance = kfdExtensionGuidance({ taxonomy, extensionRequests });
502
+ throw new Error(`${label} requires KFD-2 trust taxonomy from @kungfu-tech/kfd standards.json; ${guidance.action} at ${guidance.issueUrl}`);
503
+ }
504
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
505
+ throw new Error(`${label} must be an object validated by KFD-2 trust taxonomy`);
506
+ }
507
+ const guidance = kfdExtensionGuidance({ taxonomy, extensionRequests });
508
+ const defs = taxonomy.$defs || {};
509
+ const required = defs[kind]?.required || (kind === "residualRisk"
510
+ ? ["id", "definedBy", "riskType", "trustImpact", "machineProvability", "agentAction", "reason", "owner"]
511
+ : ["id", "riskType", "trustImpact", "reason"]);
512
+ for (const field of required) {
513
+ if (!optionalString(entry[field]).trim()) {
514
+ throw new Error(`${label}.${field} is required by KFD-2 trust taxonomy; do not add a private local taxonomy value, ${guidance.action} at ${guidance.issueUrl}`);
515
+ }
516
+ }
517
+ const allowed = taxonomyAllowedValues(taxonomy);
518
+ for (const field of KFD2_TAXONOMY_FIELDS) {
519
+ if (field in entry || required.includes(field)) {
520
+ validateTaxonomyField(entry, field, { allowed, label, guidance });
521
+ }
522
+ }
523
+ if (kind === "residualRisk") {
524
+ const expectedDefinedBy = `${metadata.schemaIds.trustTaxonomy}#/$defs/residualRisk`;
525
+ if (entry.definedBy !== expectedDefinedBy) {
526
+ throw new Error(`${label}.definedBy must be ${expectedDefinedBy}; ${guidance.action} at ${guidance.issueUrl}`);
527
+ }
528
+ }
529
+ return {
530
+ ...entry,
531
+ taxonomy: {
532
+ standard: metadata.key,
533
+ schemaId: metadata.schemaIds.trustTaxonomy,
534
+ schemaPath: metadata.schemaPaths.trustTaxonomy,
535
+ contract: taxonomyContract,
536
+ package: metadata.package,
537
+ },
538
+ extensionRequest: entry.extensionRequest || kfdExtensionGuidance({ taxonomy, extensionRequests }),
539
+ };
540
+ }
541
+
542
+ function normalizeKfd2TaxonomyEntries(value, {
543
+ kind = "residualRisk",
544
+ label = kind,
545
+ extensionRequests = [],
546
+ } = {}) {
547
+ const entries = normalizeStringObjectArray(value, label);
548
+ if (entries.length === 0) {
549
+ return [];
550
+ }
551
+ const metadata = resolveKfd2Metadata({ requireTrustTaxonomy: true });
552
+ return entries.map((entry, index) => validateKfd2TrustTaxonomyEntry(entry, {
553
+ kind,
554
+ label: `${label}[${index}]`,
555
+ metadata,
556
+ extensionRequests,
557
+ }));
558
+ }
559
+
403
560
  function normalizeKfd3AuditBoundary(value = {}, { closure = {}, label = "auditBoundary" } = {}) {
404
561
  const boundary = value && typeof value === "object" && !Array.isArray(value) ? value : {};
405
562
  const sourceClosure = closure && typeof closure === "object" && !Array.isArray(closure) ? closure : {};
@@ -444,9 +601,10 @@ function normalizeKfd3AuditBoundary(value = {}, { closure = {}, label = "auditBo
444
601
  };
445
602
  }
446
603
 
447
- function normalizeKfd3ResidualRisk(value = [], { auditBoundary = {} } = {}) {
604
+ function normalizeKfd3ResidualRisk(value = [], { auditBoundary = {}, extensionRequests = [] } = {}) {
448
605
  const explicitRisks = normalizeStringObjectArray(value, "residualRisk");
449
606
  const boundaryRisks = (auditBoundary.nonExhaustivelyEnumerableSurfaces || []).map((entry) => ({
607
+ ...entry,
450
608
  id: entry.id,
451
609
  kind: entry.kind || "non-exhaustive-surface",
452
610
  reason: entry.reason || "Surface cannot be exhaustively enumerated by the release passport.",
@@ -456,7 +614,11 @@ function normalizeKfd3ResidualRisk(value = [], { auditBoundary = {} } = {}) {
456
614
  for (const entry of [...explicitRisks, ...boundaryRisks]) {
457
615
  byId.set(entry.id || entry.reason, entry);
458
616
  }
459
- return [...byId.values()];
617
+ return normalizeKfd2TaxonomyEntries([...byId.values()], {
618
+ kind: "residualRisk",
619
+ label: "residualRisk",
620
+ extensionRequests,
621
+ });
460
622
  }
461
623
 
462
624
  function normalizeKfd3Responsibility(value = {}, { registry = {}, artifactVerifyCommand = "" } = {}) {
@@ -502,6 +664,14 @@ export function normalizeKfd3CollaborationInterfacePrebuildWitness(witness, { me
502
664
  }
503
665
  const registry = normalizeRegistry(witness.registry || witness.sourceRegistry || witness.source_registry || witness.collaborationInterface?.sourceRegistry || witness.collaboration_interface?.sourceRegistry);
504
666
  const closure = witness.collaborationInterface?.closure || witness.collaboration_interface?.closure || {};
667
+ const extensionRequests = normalizeStringObjectArray(
668
+ witness.extensionRequests ||
669
+ witness.extension_requests ||
670
+ witness.collaborationInterface?.extensionRequests ||
671
+ witness.collaboration_interface?.extensionRequests ||
672
+ [],
673
+ "extensionRequests",
674
+ );
505
675
  const auditBoundary = normalizeKfd3AuditBoundary(witness.auditBoundary || witness.audit_boundary || closure, { closure });
506
676
  const expectedArtifactVerification = witness.expectedArtifactVerification && typeof witness.expectedArtifactVerification === "object" && !Array.isArray(witness.expectedArtifactVerification)
507
677
  ? { ...witness.expectedArtifactVerification, command: optionalString(witness.expectedArtifactVerification.command) }
@@ -534,8 +704,9 @@ export function normalizeKfd3CollaborationInterfacePrebuildWitness(witness, { me
534
704
  auditBoundary,
535
705
  residualRisk: normalizeKfd3ResidualRisk(
536
706
  witness.residualRisk || witness.residual_risk || witness.auditBoundary?.residualRisk || witness.audit_boundary?.residual_risk || [],
537
- { auditBoundary },
707
+ { auditBoundary, extensionRequests },
538
708
  ),
709
+ extensionRequests,
539
710
  responsibility: normalizeKfd3Responsibility(witness.responsibility || witness.owners, {
540
711
  registry,
541
712
  artifactVerifyCommand: expectedArtifactVerification.command,
@@ -559,6 +730,10 @@ export function normalizeKfd3CollaborationInterfaceArtifactWitness(witness, { me
559
730
  throw new Error(`KFD-3 artifact witness standard must match ${metadata.key}`);
560
731
  }
561
732
  const exposedSurfaces = artifactSurfacesFromWitness(witness).map((surface, index) => normalizeKfd3Surface(surface, index, "exposedSurfaces"));
733
+ const extensionRequests = normalizeStringObjectArray(
734
+ witness.extensionRequests || witness.extension_requests || [],
735
+ "extensionRequests",
736
+ );
562
737
  return {
563
738
  schemaVersion: 1,
564
739
  contract: KFD3_ARTIFACT_WITNESS_CONTRACT,
@@ -574,6 +749,12 @@ export function normalizeKfd3CollaborationInterfaceArtifactWitness(witness, { me
574
749
  witness.collaboration_interface?.digest,
575
750
  ),
576
751
  exposedSurfaces,
752
+ residualRisk: normalizeKfd2TaxonomyEntries(witness.residualRisk || witness.residual_risk || [], {
753
+ kind: "residualRisk",
754
+ label: "artifactWitness.residualRisk",
755
+ extensionRequests,
756
+ }),
757
+ extensionRequests,
577
758
  verifier: witness.verifier && typeof witness.verifier === "object" && !Array.isArray(witness.verifier)
578
759
  ? { ...witness.verifier }
579
760
  : {},
@@ -1004,7 +1185,11 @@ export function createKfd3CollaborationInterfaceReleaseGateEvidence({
1004
1185
  reasons.push(`support-level-${supportLevel}`);
1005
1186
  }
1006
1187
  const status = reasons.length === 0 ? "passed" : supportLevel === "release" ? "failed" : "downgraded";
1007
- const residualRisk = prebuildWitness.residualRisk || [];
1188
+ const residualRiskMap = new Map();
1189
+ for (const risk of [...(prebuildWitness.residualRisk || []), ...(artifactWitness?.residualRisk || [])]) {
1190
+ residualRiskMap.set(risk.id || risk.reason, risk);
1191
+ }
1192
+ const residualRisk = [...residualRiskMap.values()];
1008
1193
  const releaseStatus = kfd3ReleaseStatus({ comparisonStatus: status, supportLevel, residualRisk });
1009
1194
  const trustResult = kfd3TrustResult(status);
1010
1195
  const declaredCapabilityVerification = {
@@ -10,6 +10,7 @@ import {
10
10
  createKfd3CollaborationInterfaceReleaseGateEvidence,
11
11
  resolveKfd1Metadata,
12
12
  resolveKfd3Metadata,
13
+ validateKfd2TrustTaxonomyEntry,
13
14
  validateKfd1ReleaseGateEvidence,
14
15
  validateKfd3CollaborationInterfaceReleaseGateEvidence,
15
16
  } from "./kfd-gate.js";
@@ -625,6 +626,13 @@ function arrayOrEmpty(value) {
625
626
  return Array.isArray(value) ? value : [];
626
627
  }
627
628
 
629
+ function arrayOrSingleton(value) {
630
+ if (Array.isArray(value)) {
631
+ return value;
632
+ }
633
+ return value && typeof value === "object" ? [value] : [];
634
+ }
635
+
628
636
  function normalizeKfd2Claim(raw = {}, index = 0) {
629
637
  const claim = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
630
638
  const sourceBindings = arrayOrEmpty(claim.sourceBindings || claim.source_bindings || claim.sources || claim.declaredSources);
@@ -638,7 +646,15 @@ function normalizeKfd2Claim(raw = {}, index = 0) {
638
646
  ? claim.audit_boundary
639
647
  : {};
640
648
  const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility) ? claim.responsibility : {};
641
- const residualRisk = arrayOrEmpty(claim.residualRisk || claim.residual_risk);
649
+ const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(entry, {
650
+ kind: "residualRisk",
651
+ label: `kfd-2.claims[${index}].residualRisk[${riskIndex}]`,
652
+ }));
653
+ const downgradeReasons = arrayOrSingleton(claim.downgradeReasons || claim.downgrade_reasons || claim.downgradeReason || claim.downgrade_reason)
654
+ .map((entry, reasonIndex) => validateKfd2TrustTaxonomyEntry(entry, {
655
+ kind: "downgradeReason",
656
+ label: `kfd-2.claims[${index}].downgradeReasons[${reasonIndex}]`,
657
+ }));
642
658
  const trustProof = claim.trustProof && typeof claim.trustProof === "object" && !Array.isArray(claim.trustProof)
643
659
  ? claim.trustProof
644
660
  : undefined;
@@ -668,6 +684,7 @@ function normalizeKfd2Claim(raw = {}, index = 0) {
668
684
  auditBoundary,
669
685
  responsibility,
670
686
  residualRisk,
687
+ downgradeReasons,
671
688
  ...(trustProof ? { trustProof } : {}),
672
689
  proseOnly,
673
690
  missingBindings,
@@ -789,6 +806,7 @@ function createKfd2ReleaseTrustPassportAudit({ explicitClaims = [], kfd1Section
789
806
  }
790
807
  const failed = claims.filter((claim) => claim.public && claim.status === "failed");
791
808
  const downgraded = claims.filter((claim) => claim.public && (claim.status === "downgraded" || claim.proseOnly));
809
+ const downgradeReasons = claims.flatMap((claim) => arrayOrEmpty(claim.downgradeReasons));
792
810
  return {
793
811
  schemaVersion: 1,
794
812
  contract: KFD2_RELEASE_TRUST_PASSPORT_CONTRACT,
@@ -798,6 +816,7 @@ function createKfd2ReleaseTrustPassportAudit({ explicitClaims = [], kfd1Section
798
816
  scope: "public release claims visible to humans or agents",
799
817
  policy: "public claims must bind declared sources, machine-readable evidence, hashes, artifact coordinates, verification results, audit boundaries, responsibility state, and residual risk",
800
818
  },
819
+ downgradeReasons,
801
820
  claims,
802
821
  summary: {
803
822
  claimCount: claims.length,
@@ -1307,6 +1326,18 @@ function validateKfd2ReleaseTrustPassportAudit(section, issues) {
1307
1326
  if (claims.length === 0) {
1308
1327
  issues.push(issue("error", "kfd-2.claims.empty", "kfd-2 audit must enumerate at least one public release claim"));
1309
1328
  }
1329
+ for (const [reasonIndex, reason] of arrayOrEmpty(section.downgradeReasons).entries()) {
1330
+ try {
1331
+ validateKfd2TrustTaxonomyEntry(reason, {
1332
+ kind: "downgradeReason",
1333
+ label: `kfd-2.downgradeReasons[${reasonIndex}]`,
1334
+ });
1335
+ } catch (error) {
1336
+ issues.push(issue("error", `kfd-2.downgradeReasons[${reasonIndex}]`, error.message, {
1337
+ id: reason?.id || "",
1338
+ }));
1339
+ }
1340
+ }
1310
1341
  for (const [index, claim] of claims.entries()) {
1311
1342
  if (!claim.id || !claim.claim) {
1312
1343
  issues.push(issue("error", `kfd-2.claims[${index}].identity`, "public release claim must include id and statement"));
@@ -1326,6 +1357,30 @@ function validateKfd2ReleaseTrustPassportAudit(section, issues) {
1326
1357
  proseOnly: Boolean(claim.proseOnly),
1327
1358
  }));
1328
1359
  }
1360
+ for (const [riskIndex, risk] of arrayOrSingleton(claim.residualRisk || claim.residual_risk).entries()) {
1361
+ try {
1362
+ validateKfd2TrustTaxonomyEntry(risk, {
1363
+ kind: "residualRisk",
1364
+ label: `kfd-2.claims[${index}].residualRisk[${riskIndex}]`,
1365
+ });
1366
+ } catch (error) {
1367
+ issues.push(issue("error", `kfd-2.claims[${index}].residualRisk[${riskIndex}]`, error.message, {
1368
+ id: risk?.id || claim.id || "",
1369
+ }));
1370
+ }
1371
+ }
1372
+ for (const [reasonIndex, reason] of arrayOrSingleton(claim.downgradeReasons || claim.downgrade_reasons || claim.downgradeReason || claim.downgrade_reason).entries()) {
1373
+ try {
1374
+ validateKfd2TrustTaxonomyEntry(reason, {
1375
+ kind: "downgradeReason",
1376
+ label: `kfd-2.claims[${index}].downgradeReasons[${reasonIndex}]`,
1377
+ });
1378
+ } catch (error) {
1379
+ issues.push(issue("error", `kfd-2.claims[${index}].downgradeReasons[${reasonIndex}]`, error.message, {
1380
+ id: reason?.id || claim.id || "",
1381
+ }));
1382
+ }
1383
+ }
1329
1384
  if (String(claim.id || "").startsWith("kfd-3:")) {
1330
1385
  const trustProof = claim.trustProof && typeof claim.trustProof === "object" && !Array.isArray(claim.trustProof)
1331
1386
  ? claim.trustProof
@@ -1360,6 +1415,19 @@ function validateKfd2ReleaseTrustPassportAudit(section, issues) {
1360
1415
  issues.push(issue("error", `kfd-2.claims[${index}].trustProof.residualRisk`, "KFD-2 trust proof must preserve residual risk as an array", {
1361
1416
  id: claim.id || "",
1362
1417
  }));
1418
+ } else {
1419
+ for (const [riskIndex, risk] of trustProof.residualRisk.entries()) {
1420
+ try {
1421
+ validateKfd2TrustTaxonomyEntry(risk, {
1422
+ kind: "residualRisk",
1423
+ label: `kfd-2.claims[${index}].trustProof.residualRisk[${riskIndex}]`,
1424
+ });
1425
+ } catch (error) {
1426
+ issues.push(issue("error", `kfd-2.claims[${index}].trustProof.residualRisk[${riskIndex}]`, error.message, {
1427
+ id: risk?.id || claim.id || "",
1428
+ }));
1429
+ }
1430
+ }
1363
1431
  }
1364
1432
  if (!trustProof.responsibility?.registryFactsOwner || !trustProof.responsibility?.artifactVerificationOwner || !trustProof.responsibility?.releasePassportProofOwner) {
1365
1433
  issues.push(issue("error", `kfd-2.claims[${index}].trustProof.responsibility`, "KFD-2 trust proof must preserve KFD-3 responsibility state", {