@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -1
  4. package/actions/promote-buildchain-ref/README.md +90 -19
  5. package/actions/report-buildchain-issue/README.md +20 -2
  6. package/actions/run-lifecycle/README.md +5 -0
  7. package/actions/validate-config/README.md +19 -1
  8. package/bin/buildchain.mjs +56 -2
  9. package/bin/internal/command-registry.mjs +82 -0
  10. package/bin/internal/trust-release-cli.mjs +1 -0
  11. package/contracts/auditable-demo-media-profiles-v1.json +116 -1
  12. package/contracts/buildchain-v2-residuals-v1.json +146 -0
  13. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
  14. package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
  15. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
  16. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
  17. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
  18. package/dist/site/buildchain-contract.json +188 -46
  19. package/dist/site/buildchain-site.json +181 -90
  20. package/dist/site/capability-registry.json +5 -5
  21. package/dist/site/cli-registry.json +860 -98
  22. package/dist/site/controller-registry.json +118 -5
  23. package/dist/site/kfd-claims.json +233 -19
  24. package/dist/site/kfd-upstream-aggregate.json +1 -1
  25. package/dist/site/manual-registry.json +20 -20
  26. package/dist/site/node-api-registry.json +374 -59
  27. package/dist/site/page-registry.json +156 -65
  28. package/dist/site/public-surface-audit.json +231 -22
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/release-provenance.json +3 -0
  31. package/dist/site/site-manifest.json +24 -24
  32. package/dist/site/workflow-registry.json +575 -79
  33. package/docs/MAP.md +22 -4
  34. package/docs/auditable-demo.md +54 -13
  35. package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
  36. package/docs/cli.md +119 -12
  37. package/docs/consumer-issue-reporting.md +20 -2
  38. package/docs/dev-alpha-candidate-patrol.md +24 -10
  39. package/docs/github-governance-authority.md +24 -31
  40. package/docs/lifecycle-protocol.md +20 -2
  41. package/docs/migration-inventory.md +24 -5
  42. package/docs/observed-evidence-patrol.md +28 -12
  43. package/docs/ownership.md +19 -1
  44. package/docs/publication-artifacts.md +158 -18
  45. package/docs/publication-authority.md +4 -4
  46. package/docs/publish-transaction.md +78 -31
  47. package/docs/release-candidate.md +24 -0
  48. package/docs/release-flow.md +50 -32
  49. package/docs/release-governance.md +39 -32
  50. package/docs/release-passport.md +4 -0
  51. package/docs/release-propagation.md +57 -12
  52. package/docs/reusable-build-surface.md +218 -90
  53. package/docs/runtime-train-validation.md +20 -2
  54. package/docs/shifu-gate-profiles.md +7 -1
  55. package/docs/stable-candidate-patrol.md +6 -6
  56. package/docs/toolkit-observability.md +22 -0
  57. package/docs/versioning.md +46 -24
  58. package/docs/web-surface-deployments.md +82 -1
  59. package/package.json +7 -5
  60. package/packages/core/README.md +60 -4
  61. package/packages/core/artifact-signing.js +1 -0
  62. package/packages/core/buildchain-contract.js +9 -4
  63. package/packages/core/cache-evidence.js +1 -0
  64. package/packages/core/controller-evidence.js +2 -0
  65. package/packages/core/diagnostics.js +105 -30
  66. package/packages/core/github-governance-authority.js +25 -17
  67. package/packages/core/index.js +40 -0
  68. package/packages/core/paper.js +3226 -0
  69. package/packages/core/public-surface-audit.js +3 -0
  70. package/packages/core/publication-artifact.js +1 -6
  71. package/packages/core/publication-authority.js +1 -0
  72. package/packages/core/publication-control-plane-audit.js +28 -0
  73. package/packages/core/publication-reproducibility.js +947 -0
  74. package/packages/core/publication-sealed-bundle.js +190 -0
  75. package/packages/core/publish-transaction.js +147 -17
  76. package/packages/core/release-candidate.js +234 -0
  77. package/packages/core/release-passport.js +519 -228
  78. package/packages/core/release-propagation.js +175 -1
  79. package/packages/core/stable-release-gate.js +4 -1
  80. package/scripts/artifact-signing-delegation.mjs +268 -0
  81. package/scripts/audit-github-governance.mjs +32 -13
  82. package/scripts/audit-publication-control-plane.mjs +17 -8
  83. package/scripts/auditable-demo.mjs +266 -7
  84. package/scripts/aws-codebuild-toolchain.mjs +285 -0
  85. package/scripts/aws-macos-jit-core.mjs +378 -0
  86. package/scripts/aws-macos-jit.mjs +123 -0
  87. package/scripts/aws-runner-burst-core.mjs +248 -0
  88. package/scripts/aws-runner-burst.mjs +79 -0
  89. package/scripts/aws-windows-jit-core.mjs +374 -0
  90. package/scripts/aws-windows-jit.mjs +121 -0
  91. package/scripts/build-contract-core.mjs +69 -6
  92. package/scripts/build-standalone-binary.mjs +23 -0
  93. package/scripts/buildchain-contract-lock.mjs +1 -1
  94. package/scripts/buildchain-patrol.mjs +1 -1
  95. package/scripts/check-internal-architecture.mjs +69 -2
  96. package/scripts/check-inventory.mjs +70 -2
  97. package/scripts/check-javascript-syntax.mjs +31 -0
  98. package/scripts/check-maintainability.mjs +371 -0
  99. package/scripts/compiler-cache-evidence.mjs +221 -0
  100. package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
  101. package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
  102. package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
  103. package/scripts/gate-profile-core.mjs +6 -1
  104. package/scripts/generate-channel-promotion-workflow.mjs +3 -12
  105. package/scripts/generate-release-candidate-passport.mjs +4 -0
  106. package/scripts/generate-site-bundle.mjs +84 -18
  107. package/scripts/import-artifact-signing-results.mjs +64 -5
  108. package/scripts/inspect-artifact-signing-requests.mjs +52 -14
  109. package/scripts/maintainability-metrics.mjs +339 -0
  110. package/scripts/npm-publish-transaction.mjs +57 -6
  111. package/scripts/observed-evidence.mjs +151 -33
  112. package/scripts/paper.mjs +645 -0
  113. package/scripts/promotion-channel-router.mjs +16 -4
  114. package/scripts/publication-reproducibility.mjs +62 -0
  115. package/scripts/reconcile-github-governance.mjs +8 -1
  116. package/scripts/release-propagation.mjs +39 -0
  117. package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
  118. package/scripts/resolve-build-contract.mjs +6 -0
  119. package/scripts/route-offline-runners.mjs +236 -0
  120. package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
  121. package/scripts/run-lifecycle-core.mjs +62 -1
  122. package/scripts/runtime-ref-core.mjs +36 -4
  123. package/scripts/seal-artifact-signing-requests.mjs +64 -12
  124. package/scripts/stable-candidate-qualification.mjs +50 -0
  125. package/scripts/verify-artifact-signing-results.mjs +26 -1
  126. package/scripts/web-surface-core.mjs +272 -86
  127. package/scripts/web-surface-production-decision.mjs +19 -3
@@ -31,6 +31,7 @@ export const IMPACT_LEDGER_CONTRACT = "kungfu-buildchain-impact";
31
31
  export const AGENT_INDEX_CONTRACT = "kungfu-buildchain-agent-index";
32
32
  export const PRODUCT_MECHANISM_CONTRACT = "kungfu-buildchain-product-mechanism";
33
33
  export const RELEASE_CHECK_REPORT_CONTRACT = "kungfu-buildchain-release-check-report";
34
+ export const RELEASE_EVIDENCE_ATTACHMENT_CONTRACT = "kungfu-buildchain-release-evidence-attachment";
34
35
  export const KFD2_RELEASE_TRUST_PASSPORT_CONTRACT = "kungfu-buildchain-kfd-2-release-trust-passport-audit";
35
36
  export const KFD2_TRUST_PROOF_CONTRACT = "kungfu-buildchain-kfd-2-trust-proof";
36
37
  export const INVARIANT_PASSPORT_GATE_CONTRACT = "buildchain.invariant-passport-gate/v1";
@@ -42,6 +43,7 @@ const CONTRACTS = new Set([
42
43
  IMPACT_LEDGER_CONTRACT,
43
44
  AGENT_INDEX_CONTRACT,
44
45
  PRODUCT_MECHANISM_CONTRACT,
46
+ RELEASE_EVIDENCE_ATTACHMENT_CONTRACT,
45
47
  ]);
46
48
 
47
49
  function nowIso() {
@@ -249,6 +251,65 @@ function writeTextFile(filePath, value) {
249
251
  fs.writeFileSync(filePath, value.endsWith("\n") ? value : `${value}\n`);
250
252
  }
251
253
 
254
+ function normalizeReleaseEvidenceAttachment(meta, {
255
+ cwd,
256
+ expectedSourceSha,
257
+ expectedTag,
258
+ expectedChannel,
259
+ index,
260
+ } = {}) {
261
+ const document = meta?.value;
262
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
263
+ throw new Error(`releaseEvidenceJsons[${index}] must be a JSON object`);
264
+ }
265
+ const inputPath = resolveJsonInputPath(meta.path, { cwd });
266
+ if (!inputPath) {
267
+ throw new Error(`releaseEvidenceJsons[${index}] must be an existing JSON file path`);
268
+ }
269
+ if (Number(document.schemaVersion) !== 1) {
270
+ throw new Error(`releaseEvidenceJsons[${index}].schemaVersion must be 1`);
271
+ }
272
+ const id = nonEmptyString(document.id, `releaseEvidenceJsons[${index}].id`);
273
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
274
+ throw new Error(`releaseEvidenceJsons[${index}].id must use only letters, digits, dot, underscore, or hyphen`);
275
+ }
276
+ const contract = nonEmptyString(document.contract, `releaseEvidenceJsons[${index}].contract`);
277
+ const release = document.release;
278
+ if (!release || typeof release !== "object" || Array.isArray(release)) {
279
+ throw new Error(`releaseEvidenceJsons[${index}].release must be a JSON object`);
280
+ }
281
+ const sourceSha = nonEmptyString(release.sourceSha, `releaseEvidenceJsons[${index}].release.sourceSha`);
282
+ const tag = nonEmptyString(release.tag, `releaseEvidenceJsons[${index}].release.tag`);
283
+ const channel = nonEmptyString(release.channel, `releaseEvidenceJsons[${index}].release.channel`);
284
+ if (!/^[0-9a-f]{40}$/i.test(sourceSha)) {
285
+ throw new Error(`releaseEvidenceJsons[${index}].release.sourceSha must be a full 40-character Git SHA`);
286
+ }
287
+ if (!expectedSourceSha || sourceSha !== expectedSourceSha) {
288
+ throw new Error(`releaseEvidenceJsons[${index}].release.sourceSha must match the release passport source SHA`);
289
+ }
290
+ if (!expectedTag || tag !== expectedTag) {
291
+ throw new Error(`releaseEvidenceJsons[${index}].release.tag must match the release passport tag`);
292
+ }
293
+ if (!expectedChannel || channel !== expectedChannel) {
294
+ throw new Error(`releaseEvidenceJsons[${index}].release.channel must match the release passport channel`);
295
+ }
296
+ const relativePath = `release-evidence-${id}.json`;
297
+ return {
298
+ document,
299
+ inputPath,
300
+ reference: {
301
+ schemaVersion: 1,
302
+ contract: RELEASE_EVIDENCE_ATTACHMENT_CONTRACT,
303
+ id,
304
+ kind: optionalString(document.kind || "product-release-evidence"),
305
+ documentContract: contract,
306
+ path: relativePath,
307
+ sha256: sha256Text(stableJson(document)),
308
+ release: { sourceSha, tag, channel },
309
+ },
310
+ };
311
+ }
312
+
252
313
  function inferPlatformFromName(name) {
253
314
  const lower = String(name || "").toLowerCase();
254
315
  if (lower.includes("apple-darwin") || lower.includes("darwin") || lower.includes("macos")) {
@@ -1165,6 +1226,7 @@ export function createReleasePassport({
1165
1226
  kfdSupport = undefined,
1166
1227
  kfdSupportEvidencePath = "",
1167
1228
  invariantPassports = undefined,
1229
+ releaseEvidence = [],
1168
1230
  kfdAgentHubEvidence = undefined,
1169
1231
  kfdAgentHubEvidencePath = "",
1170
1232
  controllerReceipts = [],
@@ -1366,6 +1428,7 @@ export function createReleasePassport({
1366
1428
  ...(normalizedKfdSupport ? { kfdSupport: normalizedKfdSupport } : {}),
1367
1429
  ...(normalizedKfdAgentHub ? { kfdAgentHub: normalizedKfdAgentHub } : {}),
1368
1430
  ...(invariantPassports ? { invariantPassports } : {}),
1431
+ ...(releaseEvidence.length > 0 ? { releaseEvidence } : {}),
1369
1432
  ...(normalizedControllerReceipts.length > 0 ? { controllerReceipts: normalizedControllerReceipts } : {}),
1370
1433
  ...(normalizedGitHubArtifactAttestations.length > 0
1371
1434
  ? { githubArtifactAttestations: normalizedGitHubArtifactAttestations }
@@ -1414,6 +1477,7 @@ export function createReleasePassport({
1414
1477
  kfdSupport: normalizedKfdSupport ? optionalString(kfdSupportEvidencePath || "kfd-support.json") : "",
1415
1478
  kfdAgentHub: normalizedKfdAgentHub ? optionalString(kfdAgentHubEvidencePath || "kfd-agent-hub-evidence.json") : "",
1416
1479
  invariantPassports: invariantPassports ? "invariantPassports" : "",
1480
+ releaseEvidence: releaseEvidence.map((entry) => entry.path),
1417
1481
  impact: impactPath,
1418
1482
  checkReport: checkReportPath,
1419
1483
  agentIndex: agentIndexPath,
@@ -1458,6 +1522,7 @@ export function collectGitHubReleasePassport({
1458
1522
  kfdProductGateJsons = [],
1459
1523
  invariantPassportJsons = [],
1460
1524
  invariantPassportCommand = "",
1525
+ releaseEvidenceJsons = [],
1461
1526
  kfdAgentHubEvidenceJson = "",
1462
1527
  controllerReceiptReferences = [],
1463
1528
  githubArtifactAttestationPolicyJsons = [],
@@ -1533,6 +1598,15 @@ export function collectGitHubReleasePassport({
1533
1598
  });
1534
1599
  if (invariantPassportCommandMeta.value) invariantPassportMetas.push(invariantPassportCommandMeta);
1535
1600
  const invariantPassports = createInvariantPassportGate(invariantPassportMetas);
1601
+ const releaseEvidenceMetas = (releaseEvidenceJsons || [])
1602
+ .filter(Boolean)
1603
+ .map((evidenceJson) =>
1604
+ parseJsonInputWithMeta(evidenceJson, undefined, {
1605
+ cwd,
1606
+ label: "releaseEvidenceJsons entry",
1607
+ }),
1608
+ )
1609
+ .filter((meta) => meta.value);
1536
1610
  const kfdAgentHubEvidenceMeta = parseJsonInputWithMeta(
1537
1611
  kfdAgentHubEvidenceJson,
1538
1612
  undefined,
@@ -1560,6 +1634,28 @@ export function collectGitHubReleasePassport({
1560
1634
  ];
1561
1635
  const resolvedTag = tag || release.tag_name || release.name || "";
1562
1636
  const resolvedOutputDir = path.resolve(cwd, outputDir);
1637
+ const resolvedChannel = optionalString(releaseExtra.channel || publish.channel);
1638
+ const releaseEvidenceAttachments = releaseEvidenceMetas.map((meta, index) =>
1639
+ normalizeReleaseEvidenceAttachment(meta, {
1640
+ cwd,
1641
+ expectedSourceSha: sourceSha,
1642
+ expectedTag: resolvedTag,
1643
+ expectedChannel: resolvedChannel,
1644
+ index,
1645
+ }),
1646
+ );
1647
+ const releaseEvidenceIds = new Set();
1648
+ for (const { reference } of releaseEvidenceAttachments) {
1649
+ if (releaseEvidenceIds.has(reference.id)) {
1650
+ throw new Error(`release evidence attachment id must be unique: ${reference.id}`);
1651
+ }
1652
+ releaseEvidenceIds.add(reference.id);
1653
+ }
1654
+ for (const { inputPath, reference } of releaseEvidenceAttachments) {
1655
+ const destinationPath = path.join(resolvedOutputDir, reference.path);
1656
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
1657
+ fs.copyFileSync(inputPath, destinationPath);
1658
+ }
1563
1659
  const resolvedCheckedAt = optionalString(checkedAt) || nowIso();
1564
1660
  const productMechanism = defaultProductMechanism({ repository, productName });
1565
1661
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: resolvedTag, sourceSha, workflow });
@@ -1638,6 +1734,7 @@ export function collectGitHubReleasePassport({
1638
1734
  kfdSupport,
1639
1735
  kfdSupportEvidencePath: kfdSupport ? "kfd-support.json" : "",
1640
1736
  invariantPassports,
1737
+ releaseEvidence: releaseEvidenceAttachments.map(({ reference }) => reference),
1641
1738
  kfdAgentHubEvidence: kfdAgentHubEvidenceMeta.value
1642
1739
  ? { ...kfdAgentHubEvidenceMeta, path: "kfd-agent-hub-evidence.json" }
1643
1740
  : undefined,
@@ -1661,6 +1758,7 @@ export function collectGitHubReleasePassport({
1661
1758
  productMechanism,
1662
1759
  kfdAgentHubEvidence: kfdAgentHubEvidenceMeta.value,
1663
1760
  kfdSupportEvidence: kfdSupport,
1761
+ releaseEvidenceDocuments: releaseEvidenceAttachments,
1664
1762
  checkedAt: resolvedCheckedAt,
1665
1763
  });
1666
1764
  const files = {
@@ -1682,7 +1780,9 @@ export function collectGitHubReleasePassport({
1682
1780
  schemaVersion: 1,
1683
1781
  contract: "kungfu-buildchain-release-passport-collection",
1684
1782
  outputDir: resolvedOutputDir,
1685
- files: Object.keys(files).concat("llms.txt"),
1783
+ files: Object.keys(files)
1784
+ .concat(releaseEvidenceAttachments.map(({ reference }) => reference.path))
1785
+ .concat("llms.txt"),
1686
1786
  passport,
1687
1787
  artifactEvidence,
1688
1788
  checkReport,
@@ -1957,18 +2057,17 @@ function findEvidenceForArtifact(artifact, evidenceIndex) {
1957
2057
  return undefined;
1958
2058
  }
1959
2059
 
1960
- export function createReleaseCheckReport({
2060
+ function validateReleaseEvidenceContracts({
1961
2061
  passport,
1962
2062
  artifactEvidence,
1963
- publishEvidence,
1964
2063
  impact,
1965
2064
  agentIndex,
1966
2065
  productMechanism,
1967
2066
  kfdAgentHubEvidence,
1968
2067
  kfdSupportEvidence,
1969
- checkedAt = nowIso(),
1970
- } = {}) {
1971
- const issues = [];
2068
+ checkedAt,
2069
+ issues,
2070
+ }) {
1972
2071
  validateContract(passport, RELEASE_PASSPORT_CONTRACT, "passport", issues);
1973
2072
  validateContract(artifactEvidence, ARTIFACT_EVIDENCE_CONTRACT, "artifactEvidence", issues);
1974
2073
  validateContract(impact, IMPACT_LEDGER_CONTRACT, "impact", issues);
@@ -2026,79 +2125,142 @@ export function createReleaseCheckReport({
2026
2125
  ));
2027
2126
  }
2028
2127
  }
2128
+ }
2029
2129
 
2030
- const tag = passport?.release?.tag || "";
2031
- if (!tag) {
2032
- issues.push(issue("error", "release.tag", "release.tag is required"));
2033
- }
2034
- if (!passport?.release?.sourceSha) {
2035
- issues.push(issue("warning", "release.sourceSha", "release.sourceSha is recommended"));
2036
- }
2037
- const artifacts = Array.isArray(passport?.artifacts) ? passport.artifacts : [];
2038
- const normalizedPublishEvidence = normalizePublishEvidence(publishEvidence);
2039
- const publishEvidenceSupplied =
2040
- Boolean(passport?.evidence?.publishEvidence) ||
2041
- (publishEvidence && typeof publishEvidence === "object" && Object.keys(publishEvidence).length > 0);
2130
+ function validateReleaseEvidenceAttachments({
2131
+ passport,
2132
+ artifactEvidence,
2133
+ normalizedPublishEvidence,
2134
+ releaseEvidenceDocuments,
2135
+ issues,
2136
+ }) {
2042
2137
  const evidenceArtifacts = [
2043
2138
  ...(Array.isArray(artifactEvidence?.artifacts) ? artifactEvidence.artifacts : []),
2044
2139
  ...(normalizedPublishEvidence?.artifacts || []),
2045
2140
  ];
2046
- if (publishEvidenceSupplied || passport?.packageSet) {
2047
- const checkPublishField = (value, label) => {
2048
- if (!value) {
2049
- issues.push(issue("error", `publishEvidence.${label}`, `publishEvidence.${label} is required`));
2050
- }
2051
- };
2052
- if (Number(normalizedPublishEvidence?.schema) !== 1) {
2053
- issues.push(issue("error", "publishEvidence.schema", "publishEvidence.schema must be 1"));
2054
- }
2055
- checkPublishField(normalizedPublishEvidence?.version, "version");
2056
- checkPublishField(normalizedPublishEvidence?.channel, "channel");
2057
- checkPublishField(normalizedPublishEvidence?.sourceSha, "sourceSha");
2058
- checkPublishField(normalizedPublishEvidence?.releaseSha, "releaseSha");
2059
- checkPublishField(normalizedPublishEvidence?.targetRef, "targetRef");
2060
- checkPublishField(normalizedPublishEvidence?.releaseMaterialSha, "releaseMaterialSha");
2061
- checkPublishField(normalizedPublishEvidence?.publishToolingSha, "publishToolingSha");
2062
- if (!Array.isArray(normalizedPublishEvidence?.artifacts) || normalizedPublishEvidence.artifacts.length === 0) {
2063
- issues.push(issue("error", "publishEvidence.artifacts", "publishEvidence.artifacts must include published artifacts"));
2064
- }
2065
- if ((normalizedPublishEvidence?.artifacts || []).some((artifact) => artifact.action)) {
2066
- try {
2067
- const validation = validateTransactionPublishEvidence({
2068
- evidence: publishEvidence,
2069
- version: normalizedPublishEvidence.version,
2070
- channel: normalizedPublishEvidence.channel,
2071
- sourceSha: normalizedPublishEvidence.sourceSha,
2072
- releaseSha: normalizedPublishEvidence.releaseSha,
2073
- targetRef: normalizedPublishEvidence.targetRef,
2074
- releaseMaterialSha: normalizedPublishEvidence.releaseMaterialSha,
2075
- publishToolingSha: normalizedPublishEvidence.publishToolingSha,
2076
- });
2077
- for (const error of validation.errors) {
2078
- issues.push(issue("error", "publishEvidence.artifactProvenance", error));
2079
- }
2080
- } catch (error) {
2081
- issues.push(issue(
2082
- "error",
2083
- "publishEvidence.artifactProvenance",
2084
- `publish artifact provenance is invalid: ${error.message}`,
2085
- ));
2141
+ const releaseEvidence = Array.isArray(passport?.releaseEvidence) ? passport.releaseEvidence : [];
2142
+ const releaseEvidenceByCoordinate = new Map(
2143
+ releaseEvidenceDocuments.map((entry) => [
2144
+ `${entry?.reference?.id || ""}\0${entry?.reference?.path || ""}`,
2145
+ entry,
2146
+ ]),
2147
+ );
2148
+ const releaseEvidenceIds = new Set();
2149
+ const releaseEvidencePaths = new Set();
2150
+ for (const [index, reference] of releaseEvidence.entries()) {
2151
+ const label = `releaseEvidence[${index}]`;
2152
+ if (!reference || typeof reference !== "object" || Array.isArray(reference)) {
2153
+ issues.push(issue("error", `${label}.object`, `${label} must be a JSON object`));
2154
+ continue;
2155
+ }
2156
+ if (reference.contract !== RELEASE_EVIDENCE_ATTACHMENT_CONTRACT) {
2157
+ issues.push(issue("error", `${label}.contract`, `${label}.contract must be ${RELEASE_EVIDENCE_ATTACHMENT_CONTRACT}`));
2158
+ }
2159
+ if (!reference.id || !reference.path || !reference.sha256 || !reference.documentContract) {
2160
+ issues.push(issue("error", `${label}.identity`, `${label} must include id, path, sha256, and documentContract`));
2161
+ continue;
2162
+ }
2163
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(reference.id)) {
2164
+ issues.push(issue("error", `${label}.id`, `${label}.id must use only letters, digits, dot, underscore, or hyphen`));
2165
+ }
2166
+ const expectedPath = `release-evidence-${reference.id}.json`;
2167
+ if (reference.path !== expectedPath) {
2168
+ issues.push(issue("error", `${label}.path`, `${label}.path must be ${expectedPath}`));
2169
+ }
2170
+ if (releaseEvidenceIds.has(reference.id)) {
2171
+ issues.push(issue("error", `${label}.id`, `${label}.id must be unique`));
2172
+ }
2173
+ if (releaseEvidencePaths.has(reference.path)) {
2174
+ issues.push(issue("error", `${label}.path`, `${label}.path must be unique`));
2175
+ }
2176
+ releaseEvidenceIds.add(reference.id);
2177
+ releaseEvidencePaths.add(reference.path);
2178
+ const loaded = releaseEvidenceByCoordinate.get(`${reference.id}\0${reference.path}`);
2179
+ if (!loaded?.document) {
2180
+ issues.push(issue("error", `${label}.missing`, `${label} document is not independently retrievable`, {
2181
+ id: reference.id,
2182
+ path: reference.path,
2183
+ }));
2184
+ continue;
2185
+ }
2186
+ const document = loaded.document;
2187
+ if (document.contract !== reference.documentContract) {
2188
+ issues.push(issue("error", `${label}.documentContract`, `${label} document contract differs from its passport reference`));
2189
+ }
2190
+ const digest = sha256Text(stableJson(document));
2191
+ if (digest !== reference.sha256) {
2192
+ issues.push(issue("error", `${label}.sha256`, `${label} document digest differs from its passport reference`));
2193
+ }
2194
+ for (const field of ["sourceSha", "tag", "channel"]) {
2195
+ const expected = passport?.release?.[field] || "";
2196
+ const declared = document?.release?.[field] || "";
2197
+ if (!declared || declared !== expected || reference?.release?.[field] !== expected) {
2198
+ issues.push(issue("error", `${label}.release.${field}`, `${label} ${field} must match the release passport`));
2086
2199
  }
2087
2200
  }
2088
- if (passport?.release?.sourceSha && normalizedPublishEvidence?.sourceSha && passport.release.sourceSha !== normalizedPublishEvidence.sourceSha) {
2089
- issues.push(issue("error", "publishEvidence.sourceSha.mismatch", "publishEvidence.sourceSha must match passport.release.sourceSha"));
2201
+ }
2202
+ return { evidenceArtifacts, releaseEvidence };
2203
+ }
2204
+
2205
+ function validatePublishEvidenceSection({ passport, publishEvidence, normalizedPublishEvidence, issues }) {
2206
+ const publishEvidenceSupplied =
2207
+ Boolean(passport?.evidence?.publishEvidence) ||
2208
+ (publishEvidence && typeof publishEvidence === "object" && Object.keys(publishEvidence).length > 0);
2209
+ if (!publishEvidenceSupplied && !passport?.packageSet) {
2210
+ return;
2211
+ }
2212
+ const checkPublishField = (value, label) => {
2213
+ if (!value) {
2214
+ issues.push(issue("error", `publishEvidence.${label}`, `publishEvidence.${label} is required`));
2090
2215
  }
2091
- if (passport?.release?.releaseSha && normalizedPublishEvidence?.releaseSha && passport.release.releaseSha !== normalizedPublishEvidence.releaseSha) {
2092
- issues.push(issue("error", "publishEvidence.releaseSha.mismatch", "publishEvidence.releaseSha must match passport.release.releaseSha"));
2216
+ };
2217
+ if (Number(normalizedPublishEvidence?.schema) !== 1) {
2218
+ issues.push(issue("error", "publishEvidence.schema", "publishEvidence.schema must be 1"));
2219
+ }
2220
+ for (const field of ["version", "channel", "sourceSha", "releaseSha", "targetRef", "releaseMaterialSha", "publishToolingSha"]) {
2221
+ checkPublishField(normalizedPublishEvidence?.[field], field);
2222
+ }
2223
+ if (!Array.isArray(normalizedPublishEvidence?.artifacts) || normalizedPublishEvidence.artifacts.length === 0) {
2224
+ issues.push(issue("error", "publishEvidence.artifacts", "publishEvidence.artifacts must include published artifacts"));
2225
+ }
2226
+ if ((normalizedPublishEvidence?.artifacts || []).some((artifact) => artifact.action)) {
2227
+ try {
2228
+ const validation = validateTransactionPublishEvidence({
2229
+ evidence: publishEvidence,
2230
+ version: normalizedPublishEvidence.version,
2231
+ channel: normalizedPublishEvidence.channel,
2232
+ sourceSha: normalizedPublishEvidence.sourceSha,
2233
+ releaseSha: normalizedPublishEvidence.releaseSha,
2234
+ targetRef: normalizedPublishEvidence.targetRef,
2235
+ releaseMaterialSha: normalizedPublishEvidence.releaseMaterialSha,
2236
+ publishToolingSha: normalizedPublishEvidence.publishToolingSha,
2237
+ });
2238
+ for (const error of validation.errors) {
2239
+ issues.push(issue("error", "publishEvidence.artifactProvenance", error));
2240
+ }
2241
+ } catch (error) {
2242
+ issues.push(issue(
2243
+ "error",
2244
+ "publishEvidence.artifactProvenance",
2245
+ `publish artifact provenance is invalid: ${error.message}`,
2246
+ ));
2093
2247
  }
2094
- if (passport?.release?.targetRef && normalizedPublishEvidence?.targetRef && passport.release.targetRef !== normalizedPublishEvidence.targetRef) {
2095
- issues.push(issue("error", "publishEvidence.targetRef.mismatch", "publishEvidence.targetRef must match passport.release.targetRef"));
2248
+ }
2249
+ for (const field of ["sourceSha", "releaseSha", "targetRef"]) {
2250
+ if (passport?.release?.[field] && normalizedPublishEvidence?.[field] && passport.release[field] !== normalizedPublishEvidence[field]) {
2251
+ issues.push(issue(
2252
+ "error",
2253
+ `publishEvidence.${field}.mismatch`,
2254
+ `publishEvidence.${field} must match passport.release.${field}`,
2255
+ ));
2096
2256
  }
2097
2257
  }
2258
+ }
2259
+
2260
+ function validateReleaseArtifacts({ artifacts, evidenceIndex, issues }) {
2098
2261
  if (artifacts.length === 0) {
2099
2262
  issues.push(issue("error", "artifacts.empty", "release passport must list at least one artifact"));
2100
2263
  }
2101
- const evidenceIndex = indexEvidenceArtifacts(evidenceArtifacts);
2102
2264
  for (const artifact of artifacts) {
2103
2265
  if (!artifact.name) {
2104
2266
  issues.push(issue("error", "artifact.name", "artifact name is required"));
@@ -2119,15 +2281,7 @@ export function createReleaseCheckReport({
2119
2281
  if (artifact.digest && evidenceDigest && artifact.digest !== evidenceDigest && artifact.digest !== `sha256:${evidenceDigest}`) {
2120
2282
  issues.push(issue("error", "artifact.digest.mismatch", `artifact ${artifact.name} digest differs between passport and evidence`));
2121
2283
  }
2122
- for (const field of [
2123
- "action",
2124
- "platform",
2125
- "contract_major",
2126
- "parent_digest",
2127
- "content",
2128
- "release",
2129
- "verification",
2130
- ]) {
2284
+ for (const field of ["action", "platform", "contract_major", "parent_digest", "content", "release", "verification"]) {
2131
2285
  if (
2132
2286
  Object.prototype.hasOwnProperty.call(evidence, field) &&
2133
2287
  stableJson(artifact[field]) !== stableJson(evidence[field])
@@ -2140,47 +2294,54 @@ export function createReleaseCheckReport({
2140
2294
  }
2141
2295
  }
2142
2296
  }
2143
- if (passport?.packageSet) {
2144
- const main = passport.packageSet.main || {};
2145
- const checkPackageEntry = (entry, label, role) => {
2146
- if (!entry.name || !entry.version) {
2147
- issues.push(issue("error", `${label}.identity`, `${label} must include name and version`));
2148
- }
2149
- if (!entry.distTag) {
2150
- issues.push(issue("error", `${label}.distTag`, `${label} must include distTag`));
2151
- }
2152
- if (!entry.digest) {
2153
- issues.push(issue("error", `${label}.digest`, `${label} must include digest`));
2154
- }
2155
- if (entry.name && entry.version) {
2156
- const artifact = findEvidenceForArtifact({
2157
- group: "node",
2158
- kind: "npm",
2159
- name: entry.name,
2160
- ref: entry.version,
2161
- digest: entry.digest,
2162
- }, evidenceIndex);
2163
- if (!artifact) {
2164
- issues.push(issue("error", `${label}.artifact`, `${label} must have matching npm artifact evidence`, {
2165
- role,
2166
- name: entry.name,
2167
- version: entry.version,
2168
- }));
2169
- }
2170
- }
2171
- };
2172
- checkPackageEntry(main, "packageSet.main", "main");
2173
- const platforms = Array.isArray(passport.packageSet.platforms) ? passport.packageSet.platforms : [];
2174
- if (platforms.length < 3) {
2175
- issues.push(issue("error", "packageSet.platforms", "packageSet must include at least three platform packages"));
2297
+ }
2298
+
2299
+ function validatePackageSet({ passport, evidenceIndex, issues }) {
2300
+ if (!passport?.packageSet) {
2301
+ return;
2302
+ }
2303
+ const main = passport.packageSet.main || {};
2304
+ const checkPackageEntry = (entry, label, role) => {
2305
+ if (!entry.name || !entry.version) {
2306
+ issues.push(issue("error", `${label}.identity`, `${label} must include name and version`));
2307
+ }
2308
+ if (!entry.distTag) {
2309
+ issues.push(issue("error", `${label}.distTag`, `${label} must include distTag`));
2176
2310
  }
2177
- for (const [index, entry] of platforms.entries()) {
2178
- checkPackageEntry(entry, `packageSet.platforms[${index}]`, "platform");
2311
+ if (!entry.digest) {
2312
+ issues.push(issue("error", `${label}.digest`, `${label} must include digest`));
2179
2313
  }
2180
- if (!Array.isArray(passport?.publish?.packages) || passport.publish.packages.length !== 1 + platforms.length) {
2181
- issues.push(issue("error", "publish.packages", "publish.packages must summarize main and platform packages"));
2314
+ if (entry.name && entry.version) {
2315
+ const artifact = findEvidenceForArtifact({
2316
+ group: "node",
2317
+ kind: "npm",
2318
+ name: entry.name,
2319
+ ref: entry.version,
2320
+ digest: entry.digest,
2321
+ }, evidenceIndex);
2322
+ if (!artifact) {
2323
+ issues.push(issue("error", `${label}.artifact`, `${label} must have matching npm artifact evidence`, {
2324
+ role,
2325
+ name: entry.name,
2326
+ version: entry.version,
2327
+ }));
2328
+ }
2182
2329
  }
2330
+ };
2331
+ checkPackageEntry(main, "packageSet.main", "main");
2332
+ const platforms = Array.isArray(passport.packageSet.platforms) ? passport.packageSet.platforms : [];
2333
+ if (platforms.length < 3) {
2334
+ issues.push(issue("error", "packageSet.platforms", "packageSet must include at least three platform packages"));
2335
+ }
2336
+ for (const [index, entry] of platforms.entries()) {
2337
+ checkPackageEntry(entry, `packageSet.platforms[${index}]`, "platform");
2183
2338
  }
2339
+ if (!Array.isArray(passport?.publish?.packages) || passport.publish.packages.length !== 1 + platforms.length) {
2340
+ issues.push(issue("error", "publish.packages", "publish.packages must summarize main and platform packages"));
2341
+ }
2342
+ }
2343
+
2344
+ function validateReleaseState({ passport, issues }) {
2184
2345
  if (passport?.trustedPublishing) {
2185
2346
  if (!passport.trustedPublishing.provider) {
2186
2347
  issues.push(issue("error", "trustedPublishing.provider", "trustedPublishing.provider is required"));
@@ -2198,17 +2359,10 @@ export function createReleaseCheckReport({
2198
2359
  } else if (passport.transaction.state !== "complete") {
2199
2360
  issues.push(issue("error", "transaction.state", "release passport transaction state must be complete"));
2200
2361
  }
2201
- if (!passport.transaction.exactTag) {
2202
- issues.push(issue("error", "transaction.exactTag", "transaction.exactTag is required"));
2203
- }
2204
- if (!passport.transaction.releaseSha) {
2205
- issues.push(issue("error", "transaction.releaseSha", "transaction.releaseSha is required"));
2206
- }
2207
- if (!passport.transaction.releaseMaterialSha) {
2208
- issues.push(issue("error", "transaction.releaseMaterialSha", "transaction.releaseMaterialSha is required"));
2209
- }
2210
- if (!passport.transaction.stateRef) {
2211
- issues.push(issue("error", "transaction.stateRef", "transaction.stateRef is required"));
2362
+ for (const field of ["exactTag", "releaseSha", "releaseMaterialSha", "stateRef"]) {
2363
+ if (!passport.transaction[field]) {
2364
+ issues.push(issue("error", `transaction.${field}`, `transaction.${field} is required`));
2365
+ }
2212
2366
  }
2213
2367
  }
2214
2368
  if (passport?.anchorManifest) {
@@ -2219,77 +2373,92 @@ export function createReleaseCheckReport({
2219
2373
  issues.push(issue("error", "anchorManifest.fields", "anchorManifest.fields must be an object"));
2220
2374
  }
2221
2375
  }
2222
- if (passport?.versionMaterial) {
2223
- if (passport.versionMaterial.contract !== "kungfu-buildchain-anchored-version-material/v1") {
2224
- issues.push(issue(
2225
- "error",
2226
- "versionMaterial.contract",
2227
- "versionMaterial contract must be kungfu-buildchain-anchored-version-material/v1",
2228
- ));
2229
- }
2230
- if (!passport.versionMaterial.alpha?.tree || !passport.versionMaterial.release?.tree) {
2376
+ if (!passport?.runnerPolicy?.productionDefault) {
2377
+ issues.push(issue("warning", "runnerPolicy.productionDefault", "runner policy should record the production default"));
2378
+ }
2379
+ }
2380
+
2381
+ function validateVersionMaterial({ passport, issues }) {
2382
+ if (!passport?.versionMaterial) {
2383
+ return;
2384
+ }
2385
+ if (passport.versionMaterial.contract !== "kungfu-buildchain-anchored-version-material/v1") {
2386
+ issues.push(issue(
2387
+ "error",
2388
+ "versionMaterial.contract",
2389
+ "versionMaterial contract must be kungfu-buildchain-anchored-version-material/v1",
2390
+ ));
2391
+ }
2392
+ if (!passport.versionMaterial.alpha?.tree || !passport.versionMaterial.release?.tree) {
2393
+ issues.push(issue(
2394
+ "error",
2395
+ "versionMaterial.tree",
2396
+ "versionMaterial must record alpha and release tree identities",
2397
+ ));
2398
+ }
2399
+ const allowedPaths = Array.isArray(passport.versionMaterial.allowedPaths)
2400
+ ? passport.versionMaterial.allowedPaths
2401
+ : [];
2402
+ const derivedFiles = Array.isArray(passport.versionMaterial.derivedFiles)
2403
+ ? passport.versionMaterial.derivedFiles
2404
+ : [];
2405
+ for (const [index, file] of derivedFiles.entries()) {
2406
+ if (!file?.path || !file?.sha256 || !allowedPaths.includes(file.path)) {
2231
2407
  issues.push(issue(
2232
2408
  "error",
2233
- "versionMaterial.tree",
2234
- "versionMaterial must record alpha and release tree identities",
2409
+ `versionMaterial.derivedFiles[${index}]`,
2410
+ "derived version material must have a path, digest, and matching allowed path",
2235
2411
  ));
2236
2412
  }
2237
- const allowedPaths = Array.isArray(passport.versionMaterial.allowedPaths)
2238
- ? passport.versionMaterial.allowedPaths
2239
- : [];
2240
- const derivedFiles = Array.isArray(passport.versionMaterial.derivedFiles)
2241
- ? passport.versionMaterial.derivedFiles
2413
+ }
2414
+ for (const side of ["alpha", "release"]) {
2415
+ const material = Array.isArray(passport.versionMaterial[side]?.material)
2416
+ ? passport.versionMaterial[side].material
2242
2417
  : [];
2243
- for (const [index, file] of derivedFiles.entries()) {
2244
- if (!file?.path || !file?.sha256 || !allowedPaths.includes(file.path)) {
2418
+ for (const [index, file] of material.entries()) {
2419
+ if (
2420
+ !file?.path ||
2421
+ !allowedPaths.includes(file.path) ||
2422
+ file.present !== true ||
2423
+ !/^sha256:[0-9a-f]{64}$/.test(file.sha256 || "")
2424
+ ) {
2245
2425
  issues.push(issue(
2246
2426
  "error",
2247
- `versionMaterial.derivedFiles[${index}]`,
2248
- "derived version material must have a path, digest, and matching allowed path",
2427
+ `versionMaterial.${side}.material[${index}]`,
2428
+ "version material must have an allowed path, present bytes, and sha256 digest",
2249
2429
  ));
2250
2430
  }
2251
2431
  }
2252
- for (const side of ["alpha", "release"]) {
2253
- const material = Array.isArray(passport.versionMaterial[side]?.material)
2254
- ? passport.versionMaterial[side].material
2255
- : [];
2256
- for (const [index, file] of material.entries()) {
2257
- if (
2258
- !file?.path ||
2259
- !allowedPaths.includes(file.path) ||
2260
- file.present !== true ||
2261
- !/^sha256:[0-9a-f]{64}$/.test(file.sha256 || "")
2262
- ) {
2263
- issues.push(issue(
2264
- "error",
2265
- `versionMaterial.${side}.material[${index}]`,
2266
- "version material must have an allowed path, present bytes, and sha256 digest",
2267
- ));
2268
- }
2269
- }
2270
- }
2271
- }
2272
- if (!passport?.runnerPolicy?.productionDefault) {
2273
- issues.push(issue("warning", "runnerPolicy.productionDefault", "runner policy should record the production default"));
2274
2432
  }
2433
+ }
2434
+
2435
+ function createReleaseImpactContext({ passport, impact }) {
2275
2436
  const surfaceImpacts = Array.isArray(impact?.surfaceImpacts) ? impact.surfaceImpacts : [];
2276
2437
  const passportSurfaceImpacts = Array.isArray(passport?.surfaceImpacts) ? passport.surfaceImpacts : [];
2277
2438
  const impactRelease = impact?.release && typeof impact.release === "object" && !Array.isArray(impact.release)
2278
2439
  ? impact.release
2279
2440
  : {};
2280
- const impactVersion = optionalString(impactRelease.version);
2281
- const impactLine = optionalString(impactRelease.line);
2282
2441
  const passportVersion = optionalString(passport?.release?.publishedVersion || passport?.release?.package?.version);
2283
2442
  const passportTargetRef = optionalString(passport?.release?.targetRef || passport?.release?.target_ref);
2284
2443
  const passportTargetLineMatch = passportTargetRef.match(/^(?:alpha|release)\/v(\d+)\/v\1\.(\d+)$/);
2285
- const passportLine = optionalString(
2286
- passport?.release?.line ||
2287
- (passportTargetLineMatch ? `v${passportTargetLineMatch[1]}.${passportTargetLineMatch[2]}` : ""),
2288
- );
2289
- const impactClassification = normalizeImpactLevel(impact?.classification);
2290
- const declaredFinalImpact = normalizeImpactLevel(impact?.versionImpact?.final || impact?.classification);
2291
- const computedFinalImpact = highestImpactLevel(surfaceImpacts.map((entry) => entry.impact));
2292
- const requiredSurfaceImpacts = surfaceImpactRequirement({ passport, impact });
2444
+ return {
2445
+ surfaceImpacts,
2446
+ passportSurfaceImpacts,
2447
+ impactVersion: optionalString(impactRelease.version),
2448
+ impactLine: optionalString(impactRelease.line),
2449
+ passportVersion,
2450
+ passportLine: optionalString(
2451
+ passport?.release?.line ||
2452
+ (passportTargetLineMatch ? `v${passportTargetLineMatch[1]}.${passportTargetLineMatch[2]}` : ""),
2453
+ ),
2454
+ impactClassification: normalizeImpactLevel(impact?.classification),
2455
+ declaredFinalImpact: normalizeImpactLevel(impact?.versionImpact?.final || impact?.classification),
2456
+ computedFinalImpact: highestImpactLevel(surfaceImpacts.map((entry) => entry.impact)),
2457
+ requiredSurfaceImpacts: surfaceImpactRequirement({ passport, impact }),
2458
+ };
2459
+ }
2460
+
2461
+ function validatePassportTrustSections({ passport, issues }) {
2293
2462
  const kfd1Metadata = resolveKfd1Metadata();
2294
2463
  issues.push(...validateKfd1ReleaseGateEvidence(passport?.[kfd1Metadata.key], { metadata: kfd1Metadata }));
2295
2464
  validateKfd2ReleaseTrustPassportAudit(passport?.["kfd-2"], issues);
@@ -2305,35 +2474,51 @@ export function createReleaseCheckReport({
2305
2474
  issues.push(issue("error", "kfd-3.metadata", error.message));
2306
2475
  }
2307
2476
  }
2308
- if (passport?.invariantPassports) {
2309
- const section = passport.invariantPassports;
2310
- if (section.contract !== INVARIANT_PASSPORT_GATE_CONTRACT) {
2311
- issues.push(issue("error", "invariantPassports.contract", `invariantPassports.contract must be ${INVARIANT_PASSPORT_GATE_CONTRACT}`));
2312
- }
2313
- if (section.result !== "passed") {
2314
- issues.push(issue("error", "invariantPassports.result", "invariantPassports.result must be passed"));
2315
- }
2316
- if (!Array.isArray(section.passports) || section.passports.length === 0) {
2317
- issues.push(issue("error", "invariantPassports.empty", "invariantPassports must contain at least one verified passport"));
2318
- }
2319
- const acceptedSourceShas = new Set([
2320
- passport?.release?.sourceSha,
2321
- passport?.release?.builtSourceSha,
2322
- passport?.release?.promotionChannelSha,
2323
- ].filter(Boolean));
2324
- for (const [index, entry] of (section.passports || []).entries()) {
2325
- const prefix = `invariantPassports.passports[${index}]`;
2326
- if (entry.verdict !== "verified") issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified`));
2327
- if (entry.coverage?.complete !== true) issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage.complete must be true`));
2328
- if (entry.source?.dirty !== false) issues.push(issue("error", `${prefix}.source.dirty`, `${prefix}.source.dirty must be false`));
2329
- if (acceptedSourceShas.size > 0 && !acceptedSourceShas.has(entry.source?.revision)) {
2330
- issues.push(issue("error", `${prefix}.source.revision`, `${prefix}.source.revision must match a release source identity`));
2331
- }
2332
- if (!/^sha256:[0-9a-f]{64}$/.test(optionalString(entry.passportRoot))) issues.push(issue("error", `${prefix}.passportRoot`, `${prefix}.passportRoot is invalid`));
2333
- if (!Array.isArray(entry.platforms) || entry.platforms.length === 0) issues.push(issue("error", `${prefix}.platforms`, `${prefix}.platforms must be non-empty`));
2334
- if (!Array.isArray(entry.residualRisk)) issues.push(issue("error", `${prefix}.residualRisk`, `${prefix}.residualRisk must be an array`));
2477
+ if (!passport?.invariantPassports) {
2478
+ return;
2479
+ }
2480
+ const section = passport.invariantPassports;
2481
+ if (section.contract !== INVARIANT_PASSPORT_GATE_CONTRACT) {
2482
+ issues.push(issue("error", "invariantPassports.contract", `invariantPassports.contract must be ${INVARIANT_PASSPORT_GATE_CONTRACT}`));
2483
+ }
2484
+ if (section.result !== "passed") {
2485
+ issues.push(issue("error", "invariantPassports.result", "invariantPassports.result must be passed"));
2486
+ }
2487
+ if (!Array.isArray(section.passports) || section.passports.length === 0) {
2488
+ issues.push(issue("error", "invariantPassports.empty", "invariantPassports must contain at least one verified passport"));
2489
+ }
2490
+ const acceptedSourceShas = new Set([
2491
+ passport?.release?.sourceSha,
2492
+ passport?.release?.builtSourceSha,
2493
+ passport?.release?.promotionChannelSha,
2494
+ ].filter(Boolean));
2495
+ for (const [index, entry] of (section.passports || []).entries()) {
2496
+ const prefix = `invariantPassports.passports[${index}]`;
2497
+ if (entry.verdict !== "verified") issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified`));
2498
+ if (entry.coverage?.complete !== true) issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage.complete must be true`));
2499
+ if (entry.source?.dirty !== false) issues.push(issue("error", `${prefix}.source.dirty`, `${prefix}.source.dirty must be false`));
2500
+ if (acceptedSourceShas.size > 0 && !acceptedSourceShas.has(entry.source?.revision)) {
2501
+ issues.push(issue("error", `${prefix}.source.revision`, `${prefix}.source.revision must match a release source identity`));
2335
2502
  }
2503
+ if (!/^sha256:[0-9a-f]{64}$/.test(optionalString(entry.passportRoot))) issues.push(issue("error", `${prefix}.passportRoot`, `${prefix}.passportRoot is invalid`));
2504
+ if (!Array.isArray(entry.platforms) || entry.platforms.length === 0) issues.push(issue("error", `${prefix}.platforms`, `${prefix}.platforms must be non-empty`));
2505
+ if (!Array.isArray(entry.residualRisk)) issues.push(issue("error", `${prefix}.residualRisk`, `${prefix}.residualRisk must be an array`));
2336
2506
  }
2507
+ }
2508
+
2509
+ function validateReleaseImpact({ passport, impact, context, issues }) {
2510
+ const {
2511
+ surfaceImpacts,
2512
+ passportSurfaceImpacts,
2513
+ impactVersion,
2514
+ impactLine,
2515
+ passportVersion,
2516
+ passportLine,
2517
+ impactClassification,
2518
+ declaredFinalImpact,
2519
+ computedFinalImpact,
2520
+ requiredSurfaceImpacts,
2521
+ } = context;
2337
2522
  if (impactVersion) {
2338
2523
  if (!impactLine) {
2339
2524
  issues.push(issue("error", "impact.release.line", "version-bound impact requires release.line"));
@@ -2397,6 +2582,21 @@ export function createReleaseCheckReport({
2397
2582
  if (passport?.versionImpact?.final && impact?.versionImpact?.final && passport.versionImpact.final !== impact.versionImpact.final) {
2398
2583
  issues.push(issue("error", "passport.versionImpact.final", "passport.versionImpact.final must match impact.versionImpact.final"));
2399
2584
  }
2585
+ }
2586
+
2587
+ function buildReleaseCheckReport({
2588
+ checkedAt,
2589
+ issues,
2590
+ requiredSurfaceImpacts,
2591
+ artifacts,
2592
+ evidenceArtifacts,
2593
+ passport,
2594
+ impact,
2595
+ agentIndex,
2596
+ productMechanism,
2597
+ surfaceImpacts,
2598
+ releaseEvidence,
2599
+ }) {
2400
2600
  const ok = issues.every((entry) => entry.level !== "error");
2401
2601
  return {
2402
2602
  schemaVersion: 1,
@@ -2423,46 +2623,122 @@ export function createReleaseCheckReport({
2423
2623
  surfaceImpactsRequired: requiredSurfaceImpacts.required,
2424
2624
  surfaceImpactCount: surfaceImpacts.length,
2425
2625
  versionImpact: impact?.versionImpact?.final || "",
2626
+ releaseEvidenceCount: releaseEvidence.length,
2426
2627
  },
2427
2628
  issues,
2428
2629
  };
2429
2630
  }
2430
2631
 
2431
- export async function readJsonFromLocation(location, redirectCount = 0) {
2632
+ export function createReleaseCheckReport({
2633
+ passport,
2634
+ artifactEvidence,
2635
+ publishEvidence,
2636
+ impact,
2637
+ agentIndex,
2638
+ productMechanism,
2639
+ kfdAgentHubEvidence,
2640
+ kfdSupportEvidence,
2641
+ releaseEvidenceDocuments = [],
2642
+ checkedAt = nowIso(),
2643
+ } = {}) {
2644
+ const issues = [];
2645
+ validateReleaseEvidenceContracts({
2646
+ passport,
2647
+ artifactEvidence,
2648
+ impact,
2649
+ agentIndex,
2650
+ productMechanism,
2651
+ kfdAgentHubEvidence,
2652
+ kfdSupportEvidence,
2653
+ checkedAt,
2654
+ issues,
2655
+ });
2656
+
2657
+ const tag = passport?.release?.tag || "";
2658
+ if (!tag) {
2659
+ issues.push(issue("error", "release.tag", "release.tag is required"));
2660
+ }
2661
+ if (!passport?.release?.sourceSha) {
2662
+ issues.push(issue("warning", "release.sourceSha", "release.sourceSha is recommended"));
2663
+ }
2664
+ const artifacts = Array.isArray(passport?.artifacts) ? passport.artifacts : [];
2665
+ const normalizedPublishEvidence = normalizePublishEvidence(publishEvidence);
2666
+ const { evidenceArtifacts, releaseEvidence } = validateReleaseEvidenceAttachments({
2667
+ passport,
2668
+ artifactEvidence,
2669
+ normalizedPublishEvidence,
2670
+ releaseEvidenceDocuments,
2671
+ issues,
2672
+ });
2673
+ validatePublishEvidenceSection({ passport, publishEvidence, normalizedPublishEvidence, issues });
2674
+ const evidenceIndex = indexEvidenceArtifacts(evidenceArtifacts);
2675
+ validateReleaseArtifacts({ artifacts, evidenceIndex, issues });
2676
+ validatePackageSet({ passport, evidenceIndex, issues });
2677
+ validateReleaseState({ passport, issues });
2678
+ validateVersionMaterial({ passport, issues });
2679
+ const impactContext = createReleaseImpactContext({ passport, impact });
2680
+ const { requiredSurfaceImpacts, surfaceImpacts } = impactContext;
2681
+ validatePassportTrustSections({ passport, issues });
2682
+ validateReleaseImpact({ passport, impact, context: impactContext, issues });
2683
+ return buildReleaseCheckReport({
2684
+ checkedAt,
2685
+ issues,
2686
+ requiredSurfaceImpacts,
2687
+ artifacts,
2688
+ evidenceArtifacts,
2689
+ passport,
2690
+ impact,
2691
+ agentIndex,
2692
+ productMechanism,
2693
+ surfaceImpacts,
2694
+ releaseEvidence,
2695
+ });
2696
+ }
2697
+
2698
+ export async function readJsonFromLocation(
2699
+ location,
2700
+ redirectCount = 0,
2701
+ { timeoutMs = 15_000 } = {},
2702
+ ) {
2432
2703
  const input = nonEmptyString(location, "location");
2704
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
2705
+ throw new Error("timeoutMs must be a positive integer");
2706
+ }
2433
2707
  if (redirectCount > 5) {
2434
2708
  throw new Error(`too many redirects while reading ${input}`);
2435
2709
  }
2436
2710
  if (/^https?:\/\//.test(input)) {
2437
2711
  const client = input.startsWith("https:") ? https : http;
2438
2712
  return new Promise((resolve, reject) => {
2439
- client
2440
- .get(input, (response) => {
2441
- if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
2442
- const nextLocation = new URL(response.headers.location, input).toString();
2443
- response.resume();
2444
- readJsonFromLocation(nextLocation, redirectCount + 1).then(resolve, reject);
2445
- return;
2446
- }
2447
- if (response.statusCode < 200 || response.statusCode >= 300) {
2448
- reject(new Error(`HTTP ${response.statusCode} while reading ${input}`));
2449
- response.resume();
2450
- return;
2713
+ const request = client.get(input, (response) => {
2714
+ if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
2715
+ const nextLocation = new URL(response.headers.location, input).toString();
2716
+ response.resume();
2717
+ readJsonFromLocation(nextLocation, redirectCount + 1, { timeoutMs }).then(resolve, reject);
2718
+ return;
2719
+ }
2720
+ if (response.statusCode < 200 || response.statusCode >= 300) {
2721
+ reject(new Error(`HTTP ${response.statusCode} while reading ${input}`));
2722
+ response.resume();
2723
+ return;
2724
+ }
2725
+ let body = "";
2726
+ response.setEncoding("utf8");
2727
+ response.on("data", (chunk) => {
2728
+ body += chunk;
2729
+ });
2730
+ response.on("end", () => {
2731
+ try {
2732
+ resolve(JSON.parse(body));
2733
+ } catch (error) {
2734
+ reject(error);
2451
2735
  }
2452
- let body = "";
2453
- response.setEncoding("utf8");
2454
- response.on("data", (chunk) => {
2455
- body += chunk;
2456
- });
2457
- response.on("end", () => {
2458
- try {
2459
- resolve(JSON.parse(body));
2460
- } catch (error) {
2461
- reject(error);
2462
- }
2463
- });
2464
- })
2465
- .on("error", reject);
2736
+ });
2737
+ });
2738
+ request.setTimeout(timeoutMs, () => {
2739
+ request.destroy(new Error(`timed out after ${timeoutMs}ms while reading ${input}`));
2740
+ });
2741
+ request.on("error", reject);
2466
2742
  });
2467
2743
  }
2468
2744
  return readJsonFile(input);
@@ -2509,6 +2785,20 @@ export async function verifyReleasePassport({
2509
2785
  kfdSupportEvidenceLocation
2510
2786
  ? await readJsonFromLocation(kfdSupportEvidenceLocation)
2511
2787
  : await resolveSiblingJson(basePath, passport.evidence?.kfdSupport) || undefined;
2788
+ const releaseEvidenceDocuments = [];
2789
+ for (const reference of Array.isArray(passport.releaseEvidence) ? passport.releaseEvidence : []) {
2790
+ const id = String(reference?.id || "");
2791
+ const expectedPath = `release-evidence-${id}.json`;
2792
+ const safePath =
2793
+ /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id) &&
2794
+ reference?.path === expectedPath;
2795
+ releaseEvidenceDocuments.push({
2796
+ reference,
2797
+ document: safePath
2798
+ ? await resolveSiblingJson(basePath, reference.path)
2799
+ : undefined,
2800
+ });
2801
+ }
2512
2802
  return createReleaseCheckReport({
2513
2803
  passport,
2514
2804
  artifactEvidence,
@@ -2518,6 +2808,7 @@ export async function verifyReleasePassport({
2518
2808
  productMechanism,
2519
2809
  kfdAgentHubEvidence,
2520
2810
  kfdSupportEvidence,
2811
+ releaseEvidenceDocuments,
2521
2812
  checkedAt,
2522
2813
  });
2523
2814
  }