@kungfu-tech/buildchain 2.10.3 → 2.10.4

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 (37) hide show
  1. package/README.md +1 -0
  2. package/actions/promote-buildchain-ref/README.md +10 -7
  3. package/dist/site/badge-endpoint-registry.json +160 -0
  4. package/dist/site/badges/v1/kfd-4/aligned.json +14 -0
  5. package/dist/site/badges/v1/kfd-4/declared.json +14 -0
  6. package/dist/site/badges/v1/kfd-4/downgraded.json +14 -0
  7. package/dist/site/badges/v1/kfd-4/draft.json +14 -0
  8. package/dist/site/badges/v1/kfd-4/failed.json +14 -0
  9. package/dist/site/badges/v1/kfd-4/missing.json +14 -0
  10. package/dist/site/badges/v1/kfd-4/passed.json +14 -0
  11. package/dist/site/badges/v1/kfd-4/planned.json +14 -0
  12. package/dist/site/buildchain-contract.json +13 -13
  13. package/dist/site/buildchain-site.json +24 -24
  14. package/dist/site/capability-registry.json +1 -1
  15. package/dist/site/kfd-claims.json +4 -4
  16. package/dist/site/manual-registry.json +5 -5
  17. package/dist/site/node-api-registry.json +9 -9
  18. package/dist/site/page-registry.json +14 -14
  19. package/dist/site/public-surface-audit.json +1 -1
  20. package/dist/site/site-manifest.json +9 -9
  21. package/docs/readme-badges.md +14 -10
  22. package/docs/release-candidate.md +4 -1
  23. package/docs/release-governance.md +5 -2
  24. package/docs/release-passport.md +5 -2
  25. package/docs/reusable-build-surface.md +5 -2
  26. package/package.json +2 -2
  27. package/packages/core/artifact-passport.js +37 -11
  28. package/packages/core/badges.js +1 -0
  29. package/packages/core/buildchain-contract.js +1 -1
  30. package/packages/core/buildchain-kfd-claims.js +1 -1
  31. package/packages/core/index.js +1 -0
  32. package/packages/core/readme-badges.js +98 -39
  33. package/packages/core/release-passport.js +1 -0
  34. package/scripts/check-inventory.mjs +2 -0
  35. package/scripts/generate-site-bundle.mjs +11 -6
  36. package/scripts/web-surface-production-release-pr.mjs +88 -3
  37. package/scripts/web-surface.mjs +39 -1
@@ -2063,7 +2063,7 @@
2063
2063
  "pageRegistryPath": "dist/site/page-registry.json",
2064
2064
  "cliRegistryDigest": "281c186e9dbdf45e55e8afcd59cfee66518f42c146d1b79dfd8837048c00bec8",
2065
2065
  "workflowRegistryDigest": "b24b2a68ae83941cbb6a719a62b4eafbd0f5e859e5810ae766ce9a5db69ddfae",
2066
- "pageRegistryDigest": "c3be81db06f3a183ce53cd0e9199b88be9c8b1d615c395f7fdbfac139796c455"
2066
+ "pageRegistryDigest": "3f1337aa8d9a1ef1d9c607612db76705479d5a1b270360d001dfcbb1a68a69cb"
2067
2067
  },
2068
2068
  "comparison": {
2069
2069
  "missingCliRegistry": [],
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "contract": "kungfu-buildchain-site-manifest",
4
- "generatedAt": "2026-07-08T10:30:54.924Z",
5
- "publishedAt": "2026-07-08T10:30:54.924Z",
4
+ "generatedAt": "2026-07-08T12:25:38.748Z",
5
+ "publishedAt": "2026-07-08T12:25:38.748Z",
6
6
  "reproducible": true,
7
7
  "timestampPolicy": "ci-injected",
8
8
  "deterministicInputs": [
@@ -19,7 +19,7 @@
19
19
  "declared Buildchain surface manifest contract"
20
20
  ],
21
21
  "sourceDateEpoch": "0",
22
- "sourceRevision": "c01d56a7410ded5f631a83950227873cef32c37b",
22
+ "sourceRevision": "2c2936458d8b1d9af67f4421af00a647de6284bb",
23
23
  "timestampPolicyDetails": {
24
24
  "contract": "kungfu-buildchain-surface-timestamp-policy",
25
25
  "timestampFields": [
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "package": {
39
39
  "name": "@kungfu-tech/buildchain",
40
- "version": "2.10.3",
40
+ "version": "2.10.4",
41
41
  "versionSource": "package.json#version"
42
42
  },
43
43
  "entrypoint": "buildchain-site.json",
@@ -69,7 +69,7 @@
69
69
  "path": "docs/release-passport.md",
70
70
  "plane": "verify",
71
71
  "exists": true,
72
- "digest": "sha256:75d8bade4b0b96b26941b11835c2debcdada8f1723412935fb8ae6884f08f9f3"
72
+ "digest": "sha256:ca764740841b364ff7cfea79b87221d29c09df09fee6d19e78ed4b0b173bfad3"
73
73
  },
74
74
  {
75
75
  "id": "release-candidate",
@@ -77,7 +77,7 @@
77
77
  "path": "docs/release-candidate.md",
78
78
  "plane": "verify",
79
79
  "exists": true,
80
- "digest": "sha256:6b5107cded122925e8560a9b117ff1c96f5cf05d934caf83d7e4b2a4d777175e"
80
+ "digest": "sha256:391f14569c7d2a43dd9986c6a29462d674a77338dac6a4ac89c79a4c3d6c4947"
81
81
  },
82
82
  {
83
83
  "id": "release-propagation",
@@ -93,7 +93,7 @@
93
93
  "path": "docs/readme-badges.md",
94
94
  "plane": "use",
95
95
  "exists": true,
96
- "digest": "sha256:06eaef737dcc73a6526d856d79275ee87af9670f0e2be39a5596758bfc3527c9"
96
+ "digest": "sha256:ddaf49a7d6e6c11aea8d21d1ba736159897e16207cd74af0044014425faf8a2c"
97
97
  },
98
98
  {
99
99
  "id": "homebrew",
@@ -205,7 +205,7 @@
205
205
  "path": "docs/reusable-build-surface.md",
206
206
  "plane": "use",
207
207
  "exists": true,
208
- "digest": "sha256:e76aefe23b83f1b99b8c13cee1ca99760fd9902a41fbec25e193426329826340"
208
+ "digest": "sha256:a3f979fcede4450dabe5b8d6182395993c9f28559f7176b071a6ff9db565a355"
209
209
  },
210
210
  {
211
211
  "id": "publish-transaction",
@@ -221,7 +221,7 @@
221
221
  "path": "docs/release-governance.md",
222
222
  "plane": "why",
223
223
  "exists": true,
224
- "digest": "sha256:15fa0295ae2d97a2fd4ca17024d3da62df1057c9118b77a7e6b7550a404cd27b"
224
+ "digest": "sha256:69218a4749170e1f84b82f211d57187a7ff33f1a510aced587e240d1d901b9e7"
225
225
  },
226
226
  {
227
227
  "id": "release-flow",
@@ -24,6 +24,7 @@ Use the public package export:
24
24
  import {
25
25
  collectBadgeBundleFacts,
26
26
  collectReadmeBadgeFacts,
27
+ createKfdBadgeSpecsFromStandards,
27
28
  renderBadgeBundleBlock,
28
29
  renderReadmeBadgeBlock,
29
30
  checkBadgeBundleBlock,
@@ -60,9 +61,11 @@ The Node API is the implementation source. The CLI delegates to it.
60
61
  `collectBadgeBundleFacts({ cwd, claims })` is the trust-badge bundle API. It
61
62
  uses the same repository facts, but returns contract
62
63
  `kungfu-buildchain-badge-bundle-facts` and only renders the Buildchain trust
63
- claims: `kfd-1`, `kfd-2`, `kfd-3`, and `release-passport`. Those four claims are
64
- enabled by default. Callers can pass `claims: "kfd-1,release-passport"` or an
65
- array to narrow the bundle without hand-writing badge Markdown.
64
+ claims: every active `kfd-*` standard discovered from KFD standards metadata,
65
+ plus `release-passport`. KFD-1, KFD-2, KFD-3, KFD-4, and release passport are
66
+ enabled by default with current KFD metadata. Callers can pass
67
+ `claims: "kfd-1,release-passport"` or an array to narrow the bundle without
68
+ hand-writing badge Markdown.
66
69
 
67
70
  ## CLI
68
71
 
@@ -114,11 +117,12 @@ kfd_standards = "node_modules/@kungfu-tech/kfd/standards.json"
114
117
  kfd_1 = "declared"
115
118
  kfd_2 = "planned"
116
119
  kfd_3 = "aligned"
120
+ kfd_4 = "declared"
117
121
  platforms = ["macOS", "Linux", "Windows"]
118
122
  workflows = ["verify.yml", "build.yml"]
119
123
 
120
124
  [badges.bundle]
121
- claims = ["kfd-1", "kfd-2", "kfd-3", "release-passport"]
125
+ claims = ["kfd-1", "kfd-2", "kfd-3", "kfd-4", "release-passport"]
122
126
  ```
123
127
 
124
128
  `release_passport` may be a local path or URL. If omitted, Buildchain tries
@@ -161,12 +165,12 @@ may display a KFD state as `passed`.
161
165
 
162
166
  ## KFD Badge Rules
163
167
 
164
- KFD passed is evidence-backed. A repository may display `KFD-1 passed`,
165
- `KFD-2 passed`, or `KFD-3 passed` only when its own release passport verifies
166
- successfully and the corresponding passport section has `status: "passed"`.
167
- The KFD badge vocabulary comes from KFD standards metadata, not Buildchain
168
- private strings: for example KFD-2 uses the `releaseTrustPassport` concept from
169
- `@kungfu-tech/kfd/standards.json`.
168
+ KFD passed is evidence-backed. A repository may display `KFD-N passed` only when
169
+ its own release passport verifies successfully and the corresponding passport
170
+ section has `status: "passed"`. The KFD badge vocabulary comes from KFD
171
+ standards metadata, not Buildchain private strings: for example KFD-2 uses the
172
+ `releaseTrustPassport` concept and KFD-4 uses the `observerPerspective` concept
173
+ from `@kungfu-tech/kfd/standards.json`.
170
174
 
171
175
  When no release passport exists yet, or when the passport cannot be verified,
172
176
  Buildchain downgrades each KFD badge to the explicit local declaration such as
@@ -84,12 +84,15 @@ generation scripts.
84
84
 
85
85
  By default, the wrapper forwards GitHub Release publication to the underlying
86
86
  `promote-buildchain-ref` semver model. Once the release transaction is complete,
87
- the action creates or updates the exact-tag GitHub Release, applies
87
+ the action creates or updates the public GitHub Release, applies
88
88
  prerelease/latest metadata from the semver tag, and uploads the publish evidence
89
89
  file together with the generated release passport assets. This keeps
90
90
  npm/registry publication, Buildchain release passport persistence, and
91
91
  `release.published` propagation in one declarative reusable workflow. Consumers
92
92
  that do not publish GitHub Releases can opt out with `github-release: false`.
93
+ For anchored/manual package releases, the public GitHub Release tag defaults to
94
+ `v<publishedVersion>` while the internal transaction exact tag remains recorded
95
+ in the release passport.
93
96
 
94
97
  Products that publish KFD release trust evidence can keep that path declarative
95
98
  too. Pass KFD-1 self contract witnesses, KFD-2 public claim files, and KFD-3
@@ -479,10 +479,13 @@ consumer shell glue. Consumers normally use the `release-candidate-promote.yml`
479
479
  wrapper, where GitHub Release publication is enabled by default and can be
480
480
  disabled with `github-release: false`; the wrapper passes that declaration to
481
481
  the action. After the publish transaction reaches `complete`, Buildchain creates
482
- or updates the exact-tag GitHub Release and uploads the generated
482
+ or updates the public GitHub Release and uploads the generated
483
483
  `buildchain.release.json`, release-passport assets, and publish evidence. Semver
484
484
  prerelease tags are marked `prerelease=true` and `make_latest=false`; stable
485
- semver tags are marked latest. This is the supported path for downstream
485
+ semver tags are marked latest. For anchored/manual package releases, the public
486
+ release tag defaults to `v<publishedVersion>` while the internal exact
487
+ transaction tag remains in the release passport and release-state ref. This is
488
+ the supported path for downstream
486
489
  `release.published` propagation across semver, major, and promote-only release
487
490
  candidate publication models.
488
491
 
@@ -34,8 +34,11 @@ digests, trusted publishing evidence, and transaction result.
34
34
 
35
35
  Additive passport sections:
36
36
 
37
- - `release`: exact tag, line, channel, source SHA, target ref, release SHA,
38
- release material SHA, publish tooling SHA, and durable release-state ref.
37
+ - `release`: public release tag, internal transaction tag, line, channel,
38
+ source SHA, target ref, release SHA, release material SHA, publish tooling
39
+ SHA, and durable release-state ref. Anchored/manual package releases use the
40
+ published package version for the public tag while preserving the internal
41
+ exact transaction tag for Buildchain recovery.
39
42
  - `versionImpact`: final patch/minor/major classification, source, and
40
43
  rationale.
41
44
  - `surfaceImpacts`: per registered surface classification. The final impact is
@@ -628,13 +628,16 @@ resolution and before publish. A compatible `v2` drift leaves an issue in the
628
628
  consumer repository but does not trigger a second heavy build; an incompatible
629
629
  drift fails before publish side effects.
630
630
 
631
- The wrapper publishes the promoted exact tag as a GitHub Release by default.
631
+ The wrapper publishes the public release tag as a GitHub Release by default.
632
632
  After `promote-buildchain-ref` reports a complete release transaction, the
633
- wrapper creates or updates the exact-tag release, marks semver prerelease tags
633
+ wrapper creates or updates the public release, marks semver prerelease tags
634
634
  such as `v1.2.3-alpha.0`, `v1.2.3-rc.1`, or `v22.22.3-kf.3-alpha.7` as
635
635
  `prerelease=true` and `make_latest=false`, marks stable semver tags as latest,
636
636
  and uploads the publish evidence file plus every file in the generated release
637
637
  passport directory, including `buildchain.release.json` and `check-report.json`.
638
+ For anchored/manual package releases, the public release tag is derived from the
639
+ published package version and the internal exact transaction tag remains
640
+ available in the release passport.
638
641
  Consumers do not need to hand-write `gh release` logic to trigger
639
642
  `release.published` propagation. Set `github-release: false` only for
640
643
  repositories that intentionally do not maintain GitHub Releases.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.10.3",
3
+ "version": "2.10.4",
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",
@@ -89,7 +89,7 @@
89
89
  "vitest": "^4.1.9"
90
90
  },
91
91
  "dependencies": {
92
- "@kungfu-tech/kfd": "1.0.0-alpha.16",
92
+ "@kungfu-tech/kfd": "1.0.0-alpha.19",
93
93
  "smol-toml": "1.7.0"
94
94
  }
95
95
  }
@@ -297,14 +297,31 @@ function embeddedPointerCandidates(subject) {
297
297
  : [];
298
298
  }
299
299
 
300
- function githubReleasePassportLocation(subject, options) {
301
- const repository = options.repository || subject.githubRelease?.repository || "";
302
- const tag = options.tag || subject.githubRelease?.tag || "";
303
- if (!repository || !tag) {
300
+ function releaseTagForArtifactVersion(version = "") {
301
+ const value = optionalString(version).trim();
302
+ if (!value) {
304
303
  return "";
305
304
  }
305
+ return value.startsWith("v") ? value : `v${value}`;
306
+ }
307
+
308
+ function githubReleasePassportLocations(subject, options) {
309
+ const repository = options.repository || subject.githubRelease?.repository || "";
310
+ if (!repository) {
311
+ return [];
312
+ }
313
+ const tags = [
314
+ subject.githubRelease?.tag,
315
+ releaseTagForArtifactVersion(subject.version),
316
+ options.tag,
317
+ ].map(optionalString).filter(Boolean);
318
+ const uniqueTags = [...new Set(tags)];
306
319
  const baseUrl = (options.githubReleaseBaseUrl || "https://github.com").replace(/\/$/, "");
307
- return `${baseUrl}/${repository}/releases/download/${encodeURIComponent(tag)}/buildchain.release.json`;
320
+ return uniqueTags.map((tag) => ({
321
+ tag,
322
+ repository,
323
+ location: `${baseUrl}/${repository}/releases/download/${encodeURIComponent(tag)}/buildchain.release.json`,
324
+ }));
308
325
  }
309
326
 
310
327
  export async function resolveArtifactSubject(subject, {
@@ -455,18 +472,27 @@ export async function discoverArtifactPassport({
455
472
  return found("local-config-index", matched.passportLocation, { locator: matched.locator });
456
473
  }
457
474
  }
458
- const githubLocation = githubReleasePassportLocation(subject, { repository, tag, githubReleaseBaseUrl });
459
- if (githubLocation) {
475
+ const githubLocations = githubReleasePassportLocations(subject, { repository, tag, githubReleaseBaseUrl });
476
+ for (const githubLocation of githubLocations) {
460
477
  try {
461
- await readJsonFromLocation(githubLocation);
462
- attempts.push({ method: "github-release-default", location: githubLocation, status: "found" });
463
- return found("github-release-default", githubLocation, { repository: repository || subject.githubRelease?.repository || "", tag: tag || subject.githubRelease?.tag || "" });
478
+ await readJsonFromLocation(githubLocation.location);
479
+ attempts.push({
480
+ method: "github-release-default",
481
+ location: githubLocation.location,
482
+ status: "found",
483
+ details: { repository: githubLocation.repository, tag: githubLocation.tag },
484
+ });
485
+ return found("github-release-default", githubLocation.location, {
486
+ repository: githubLocation.repository,
487
+ tag: githubLocation.tag,
488
+ });
464
489
  } catch (error) {
465
490
  attempts.push({
466
491
  method: "github-release-default",
467
- location: githubLocation,
492
+ location: githubLocation.location,
468
493
  status: "miss",
469
494
  error: error.message,
495
+ details: { repository: githubLocation.repository, tag: githubLocation.tag },
470
496
  });
471
497
  }
472
498
  }
@@ -9,6 +9,7 @@ export {
9
9
  checkReadmeBadgeBlock,
10
10
  collectBadgeBundleFacts,
11
11
  collectReadmeBadgeFacts,
12
+ createKfdBadgeSpecsFromStandards,
12
13
  createReadmeBadgeEndpointRegistry,
13
14
  readReadme,
14
15
  renderBadgeBundleBlock,
@@ -207,7 +207,7 @@ export function createBuildchainContractWorld({ root = process.cwd(), packageJso
207
207
  "protected release refs and durable release-state are finalized by Buildchain",
208
208
  "release passport finalization is idempotent after publish side effects",
209
209
  "publish transactions can require a resolved publish-gate source lock to prevent floating-ref drift",
210
- "semver GitHub Releases are created or updated only after transaction completion, with prerelease/latest metadata derived from the exact tag",
210
+ "semver GitHub Releases are created or updated only after transaction completion, with prerelease/latest metadata derived from the public release tag",
211
211
  ],
212
212
  }),
213
213
  surface(root, {
@@ -353,7 +353,7 @@ export function createBuildchainPublicClaimDefinitions() {
353
353
  },
354
354
  {
355
355
  id: "claim:buildchain-semver-github-release",
356
- claim: "Semver promotion can create or update an exact-tag GitHub Release only after the publish transaction completes, and uploads Buildchain release passport/evidence assets.",
356
+ claim: "Semver promotion can create or update the public GitHub Release only after the publish transaction completes, and uploads Buildchain release passport/evidence assets while preserving the internal transaction exact tag.",
357
357
  sourcePaths: [
358
358
  "actions/promote-buildchain-ref/index.js",
359
359
  "actions/promote-buildchain-ref/action.yml",
@@ -238,6 +238,7 @@ export {
238
238
  checkReadmeBadgeBlock,
239
239
  collectBadgeBundleFacts,
240
240
  collectReadmeBadgeFacts,
241
+ createKfdBadgeSpecsFromStandards,
241
242
  createReadmeBadgeEndpointRegistry,
242
243
  readReadme,
243
244
  renderBadgeBundleBlock,
@@ -18,19 +18,22 @@ export const BADGE_BUNDLE_DEFAULT_CLAIMS = [
18
18
  "kfd-1",
19
19
  "kfd-2",
20
20
  "kfd-3",
21
+ "kfd-4",
21
22
  "release-passport",
22
23
  ];
23
24
 
24
- const KFD_KEYS = [
25
+ const KFD_FALLBACK_KEYS = [
25
26
  { key: "kfd-1", id: "kfd1", label: "KFD-1", text: "contract world" },
26
27
  { key: "kfd-2", id: "kfd2", label: "KFD-2", text: "trust passport" },
27
28
  { key: "kfd-3", id: "kfd3", label: "KFD-3", text: "collaboration interface" },
29
+ { key: "kfd-4", id: "kfd4", label: "KFD-4", text: "timeline observer" },
28
30
  ];
29
31
 
30
32
  const KFD_BADGE_CONCEPTS = {
31
33
  "kfd-1": "contractWorld",
32
34
  "kfd-2": "releaseTrustPassport",
33
35
  "kfd-3": "collaborationInterface",
36
+ "kfd-4": "observerPerspective",
34
37
  };
35
38
 
36
39
  const requireFromHere = createRequire(import.meta.url);
@@ -47,23 +50,7 @@ const STATE_COLORS = {
47
50
  unknown: "6e7781",
48
51
  };
49
52
 
50
- const BUILDCHAIN_BADGE_IDS = [
51
- "kfd-1",
52
- "kfd-2",
53
- "kfd-3",
54
- "buildchain-release-passport",
55
- ];
56
-
57
- const BADGE_BUNDLE_CLAIM_ALIASES = {
58
- kfd1: "kfd-1",
59
- "kfd_1": "kfd-1",
60
- "kfd-1": "kfd-1",
61
- kfd2: "kfd-2",
62
- "kfd_2": "kfd-2",
63
- "kfd-2": "kfd-2",
64
- kfd3: "kfd-3",
65
- "kfd_3": "kfd-3",
66
- "kfd-3": "kfd-3",
53
+ const STATIC_BADGE_BUNDLE_CLAIM_ALIASES = {
67
54
  passport: "release-passport",
68
55
  "release_passport": "release-passport",
69
56
  "release-passport": "release-passport",
@@ -136,10 +123,20 @@ function hostedBadgeUrl({ badgeConfig = {}, id, state }) {
136
123
  return `${badgeEndpointBaseUrl(badgeConfig)}/${encodeURIComponent(id)}/${encodeURIComponent(state || "unknown")}.svg`;
137
124
  }
138
125
 
139
- export function createReadmeBadgeEndpointRegistry({ kfdSpecs = KFD_KEYS } = {}) {
126
+ export function createKfdBadgeSpecsFromStandards(standardsMetadata) {
127
+ return kfdBadgeSpecs(summarizeKfdStandards({
128
+ value: standardsMetadata && typeof standardsMetadata === "object" ? standardsMetadata : undefined,
129
+ source: "provided",
130
+ }));
131
+ }
132
+
133
+ export function createReadmeBadgeEndpointRegistry({ kfdSpecs = undefined, kfdStandards = undefined } = {}) {
134
+ const effectiveKfdSpecs = Array.isArray(kfdSpecs) && kfdSpecs.length > 0
135
+ ? kfdSpecs
136
+ : createKfdBadgeSpecsFromStandards(kfdStandards);
140
137
  const states = Object.keys(STATE_COLORS).filter((state) => state !== "unknown");
141
138
  const specs = [
142
- ...kfdSpecs.map((entry) => ({
139
+ ...effectiveKfdSpecs.map((entry) => ({
143
140
  id: entry.key,
144
141
  label: entry.label,
145
142
  messageTemplate: `${entry.text} {state}`,
@@ -211,15 +208,55 @@ function splitClaimList(value) {
211
208
  return [];
212
209
  }
213
210
 
214
- function normalizeBadgeBundleClaims(value, fallback = BADGE_BUNDLE_DEFAULT_CLAIMS) {
211
+ function fallbackKfdEntryForKey(key) {
212
+ return KFD_FALLBACK_KEYS.find((entry) => entry.key === key) || {};
213
+ }
214
+
215
+ function normalizeKfdKey(value) {
216
+ const normalized = String(value || "").trim().toLowerCase().replace(/_/g, "-");
217
+ const match = normalized.match(/^kfd-?(\d+)$/);
218
+ return match ? `kfd-${match[1]}` : normalized;
219
+ }
220
+
221
+ function kfdSortValue(key) {
222
+ const match = String(key || "").match(/^kfd-(\d+)$/);
223
+ return match ? Number(match[1]) : Number.MAX_SAFE_INTEGER;
224
+ }
225
+
226
+ function sortedKfdKeys(keys = []) {
227
+ return [...new Set(keys.map(normalizeKfdKey).filter((key) => /^kfd-\d+$/.test(key)))]
228
+ .sort((left, right) => {
229
+ const numeric = kfdSortValue(left) - kfdSortValue(right);
230
+ return numeric || left.localeCompare(right);
231
+ });
232
+ }
233
+
234
+ function defaultBadgeBundleClaimsForKfd(kfdSpecs = KFD_FALLBACK_KEYS) {
235
+ const claims = sortedKfdKeys(kfdSpecs.map((entry) => entry.key));
236
+ return [...claims, "release-passport"];
237
+ }
238
+
239
+ function badgeBundleClaimAliases(kfdSpecs = KFD_FALLBACK_KEYS) {
240
+ const aliases = { ...STATIC_BADGE_BUNDLE_CLAIM_ALIASES };
241
+ for (const key of sortedKfdKeys(kfdSpecs.map((entry) => entry.key))) {
242
+ const number = key.replace(/^kfd-/, "");
243
+ aliases[key] = key;
244
+ aliases[`kfd${number}`] = key;
245
+ aliases[`kfd_${number}`] = key;
246
+ }
247
+ return aliases;
248
+ }
249
+
250
+ function normalizeBadgeBundleClaims(value, fallback = BADGE_BUNDLE_DEFAULT_CLAIMS, kfdSpecs = KFD_FALLBACK_KEYS) {
215
251
  const selected = [];
216
252
  const unknown = [];
253
+ const aliases = badgeBundleClaimAliases(kfdSpecs);
217
254
  for (const claim of splitClaimList(value)) {
218
255
  const key = String(claim || "").trim().toLowerCase();
219
256
  if (!key) {
220
257
  continue;
221
258
  }
222
- const normalized = BADGE_BUNDLE_CLAIM_ALIASES[key];
259
+ const normalized = aliases[key.replace(/_/g, "-")] || aliases[key];
223
260
  if (!normalized) {
224
261
  unknown.push(key);
225
262
  continue;
@@ -234,13 +271,18 @@ function normalizeBadgeBundleClaims(value, fallback = BADGE_BUNDLE_DEFAULT_CLAIM
234
271
  return selected.length > 0 ? selected : [...fallback];
235
272
  }
236
273
 
237
- function badgeBundleConfig(badgeConfig = {}) {
274
+ function badgeBundleConfig(badgeConfig = {}, kfdSpecs = KFD_FALLBACK_KEYS) {
238
275
  const configured = badgeConfig.bundle && typeof badgeConfig.bundle === "object"
239
276
  ? badgeConfig.bundle
240
277
  : {};
278
+ const defaultClaims = defaultBadgeBundleClaimsForKfd(kfdSpecs);
241
279
  return {
242
280
  enabled: configured.enabled === undefined ? true : Boolean(configured.enabled),
243
- claims: normalizeBadgeBundleClaims(configured.claims || badgeConfig.bundle_claims || badgeConfig.bundleClaims),
281
+ claims: normalizeBadgeBundleClaims(
282
+ configured.claims || badgeConfig.bundle_claims || badgeConfig.bundleClaims,
283
+ defaultClaims,
284
+ kfdSpecs,
285
+ ),
244
286
  mode: String(configured.mode || "readme"),
245
287
  hosted: configured.hosted === undefined ? true : Boolean(configured.hosted),
246
288
  };
@@ -320,6 +362,13 @@ function summarizeKfdStandards(loaded) {
320
362
  const standards = loaded?.value?.standards && typeof loaded.value.standards === "object"
321
363
  ? loaded.value.standards
322
364
  : {};
365
+ const discoveredKeys = sortedKfdKeys(Object.keys(standards).filter((key) => {
366
+ const standard = standards[key];
367
+ return standard && typeof standard === "object" && standard.status !== "retired";
368
+ }));
369
+ const keys = discoveredKeys.length > 0
370
+ ? discoveredKeys
371
+ : sortedKfdKeys(KFD_FALLBACK_KEYS.map((entry) => entry.key));
323
372
  const summary = {
324
373
  contract: loaded?.value?.contract || "",
325
374
  schemaVersion: loaded?.value?.schemaVersion || undefined,
@@ -331,20 +380,22 @@ function summarizeKfdStandards(loaded) {
331
380
  error: loaded?.error || "",
332
381
  standards: {},
333
382
  };
334
- for (const fallback of KFD_KEYS) {
335
- const standard = standards[fallback.key] || {};
336
- const conceptKey = KFD_BADGE_CONCEPTS[fallback.key];
337
- summary.standards[fallback.key] = {
338
- key: standard.key || fallback.key,
339
- id: standard.id || fallback.label,
340
- label: standard.label || fallback.label,
383
+ for (const key of keys) {
384
+ const fallback = fallbackKfdEntryForKey(key);
385
+ const standard = standards[key] || {};
386
+ const conceptKey = KFD_BADGE_CONCEPTS[key] || Object.keys(standard.interfaces || {})[0] || Object.keys(standard.concepts || {})[0] || "";
387
+ const label = standard.label || fallback.label || String(key).toUpperCase();
388
+ summary.standards[key] = {
389
+ key: standard.key || key,
390
+ id: standard.id || fallback.id || label,
391
+ label,
341
392
  title: standard.title || "",
342
393
  status: standard.status || "",
343
394
  revision: standard.revision || undefined,
344
395
  documentUrl: standard.document?.url || "",
345
396
  documentPath: standard.document?.path || "",
346
397
  documentSha256: standard.document?.sha256 || "",
347
- badgeText: standard.concepts?.[conceptKey] || fallback.text,
398
+ badgeText: standard.concepts?.[conceptKey] || fallback.text || label,
348
399
  interfaceContract: conceptKey ? standard.interfaces?.[conceptKey]?.contract || "" : "",
349
400
  schemaId: conceptKey ? standard.interfaces?.[conceptKey]?.schemaId || standard.schemaIds?.[conceptKey] || "" : "",
350
401
  };
@@ -353,12 +404,15 @@ function summarizeKfdStandards(loaded) {
353
404
  }
354
405
 
355
406
  function kfdBadgeSpecs(kfdStandards) {
356
- return KFD_KEYS.map((fallback) => {
357
- const standard = kfdStandards?.standards?.[fallback.key] || {};
407
+ const keys = sortedKfdKeys(Object.keys(kfdStandards?.standards || {}));
408
+ return keys.map((key) => {
409
+ const fallback = fallbackKfdEntryForKey(key);
410
+ const standard = kfdStandards?.standards?.[key] || {};
358
411
  return {
359
412
  ...fallback,
413
+ key,
360
414
  label: standard.label || fallback.label,
361
- text: standard.badgeText || fallback.text,
415
+ text: standard.badgeText || fallback.text || standard.label || key.toUpperCase(),
362
416
  title: standard.title || "",
363
417
  standardDocumentUrl: standard.documentUrl || "",
364
418
  standardDocumentSha256: standard.documentSha256 || "",
@@ -664,7 +718,8 @@ export async function collectReadmeBadgeFacts({ cwd = process.cwd() } = {}) {
664
718
  : releasePassportLocation;
665
719
  const releaseState = releasePassportState({ report, error, badgeConfig });
666
720
  const kfdStandards = summarizeKfdStandards(await loadKfdStandards({ cwd: resolvedCwd, badgeConfig }));
667
- const kfd = kfdBadgeSpecs(kfdStandards).map((entry) => {
721
+ const kfdSpecs = kfdBadgeSpecs(kfdStandards);
722
+ const kfd = kfdSpecs.map((entry) => {
668
723
  const passed = kfdSectionPassed({ passport, report, key: entry.key });
669
724
  const state = passed ? "passed" : declaredKfdState({ badgeConfig, key: entry.key, id: entry.id });
670
725
  return {
@@ -719,10 +774,13 @@ export async function collectReadmeBadgeFacts({ cwd = process.cwd() } = {}) {
719
774
  badgeRuntime: {
720
775
  provider: "buildchain-hosted",
721
776
  endpointBaseUrl: badgeEndpointBaseUrl(badgeConfig),
722
- hostedBadgeIds: BUILDCHAIN_BADGE_IDS,
777
+ hostedBadgeIds: [
778
+ ...defaultBadgeBundleClaimsForKfd(kfdSpecs).filter((claim) => claim !== "release-passport"),
779
+ "buildchain-release-passport",
780
+ ],
723
781
  logoPolicy: BUILDCHAIN_BADGE_LOGO_PLACEHOLDER,
724
782
  },
725
- badgeBundle: badgeBundleConfig(badgeConfig),
783
+ badgeBundle: badgeBundleConfig(badgeConfig, kfdSpecs),
726
784
  badges: [],
727
785
  };
728
786
  facts.badges = buildBadgeEntries(facts, { badgeConfig });
@@ -733,7 +791,7 @@ export async function collectBadgeBundleFacts({ cwd = process.cwd(), claims = un
733
791
  const readmeFacts = await collectReadmeBadgeFacts({ cwd });
734
792
  const config = {
735
793
  ...readmeFacts.badgeBundle,
736
- claims: normalizeBadgeBundleClaims(claims, readmeFacts.badgeBundle.claims),
794
+ claims: normalizeBadgeBundleClaims(claims, readmeFacts.badgeBundle.claims, readmeFacts.kfd),
737
795
  };
738
796
  const badges = buildBadgeBundleEntries(readmeFacts, config);
739
797
  return {
@@ -746,6 +804,7 @@ export async function collectBadgeBundleFacts({ cwd = process.cwd(), claims = un
746
804
  sourceFactsSha256: hashText(JSON.stringify(readmeFacts)),
747
805
  policy: {
748
806
  defaultClaims: [...BADGE_BUNDLE_DEFAULT_CLAIMS],
807
+ discoveredDefaultClaims: defaultBadgeBundleClaimsForKfd(readmeFacts.kfd),
749
808
  claims: config.claims,
750
809
  hosted: config.hosted,
751
810
  mode: config.mode,
@@ -1032,6 +1032,7 @@ export function createReleasePassport({
1032
1032
  },
1033
1033
  release: {
1034
1034
  tag: normalizedTag,
1035
+ publicTag: optionalString(release.publicTag || release.public_tag || normalizedTag),
1035
1036
  internalTag: optionalString(release.internalTag || release.internal_tag || normalizedTag),
1036
1037
  internalVersion,
1037
1038
  publishedVersion,
@@ -271,6 +271,7 @@ for (const requiredSnippet of [
271
271
  "renderReadmeBadgeBlock",
272
272
  "checkReadmeBadgeBlock",
273
273
  "updateReadmeBadgeBlock",
274
+ "createKfdBadgeSpecsFromStandards",
274
275
  "createReadmeBadgeEndpointRegistry",
275
276
  ]) {
276
277
  if (!coreIndexSource.includes(requiredSnippet)) {
@@ -761,6 +762,7 @@ for (const requiredSnippet of [
761
762
  "github-release:",
762
763
  "github-release-title:",
763
764
  "github-release-notes:",
765
+ "public-release-tag:",
764
766
  "github-release-url:",
765
767
  "github-release-action:",
766
768
  ]) {