@kungfu-tech/buildchain 2.5.5-alpha.2 → 2.5.5-alpha.3

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.
@@ -67,6 +67,7 @@ function usage() {
67
67
  [--trusted-publishing-json <json-or-path>]
68
68
  [--transaction-json <json-or-path>]
69
69
  [--anchor-manifest-json <json-or-path>]
70
+ [--impact-json <json-or-path>]
70
71
  [--build-summary-json <json-or-path>]
71
72
  [--platform-manifest-json <json-or-path>]...
72
73
  [--dist-tag-evidence-json <json-or-path>]
@@ -716,6 +717,7 @@ async function main(argv = process.argv.slice(2)) {
716
717
  trustedPublishingJson: readFlag(collectArgs, "trusted-publishing-json", ""),
717
718
  transactionJson: readFlag(collectArgs, "transaction-json", ""),
718
719
  anchorManifestJson: readFlag(collectArgs, "anchor-manifest-json", ""),
720
+ impactJson: readFlag(collectArgs, "impact-json", ""),
719
721
  buildSummaryJson: readFlag(collectArgs, "build-summary-json", ""),
720
722
  platformManifestJsons: readRepeatedFlag(collectArgs, "platform-manifest-json"),
721
723
  distTagEvidenceJson: readFlag(collectArgs, "dist-tag-evidence-json", ""),
package/docs/cli.md CHANGED
@@ -252,6 +252,7 @@ buildchain collect github-release \
252
252
  --publish-evidence-json .buildchain/release-evidence/v2.3.2/evidence.json \
253
253
  --transaction-json .buildchain/release-state/v2.3.2/state.json \
254
254
  --package-set-json package-set.json \
255
+ --impact-json impact.json \
255
256
  --trusted-publishing-json trusted-publishing.json \
256
257
  --anchor-manifest-json libnode.release.json \
257
258
  --build-summary-json .buildchain/artifacts/build-summary.json \
@@ -275,6 +276,13 @@ platform packages with version, dist-tag, and digest evidence. Verification
275
276
  fails closed if supplied sections are internally incomplete or point to
276
277
  artifacts without matching evidence.
277
278
 
279
+ `--impact-json` supplies the surface-aware impact ledger. When it includes
280
+ `surfaceImpacts[]`, the verifier requires `versionImpact.final` to match the
281
+ highest declared surface impact and copies `versionImpact` plus
282
+ `surfaceImpacts` into `buildchain.release.json`. This lets
283
+ `buildchain explain release --for agent --json` state why a release is patch,
284
+ minor, or major instead of relying on file-path memory.
285
+
278
286
  Buildchain dogfoods its observability toolkit in this lane. The standalone
279
287
  builder writes API-generated events, while the workflow uses `buildchain mark`,
280
288
  `buildchain span`, `buildchain verify observability-log`, and `buildchain log
@@ -36,6 +36,10 @@ Additive passport sections:
36
36
 
37
37
  - `release`: exact tag, line, channel, source SHA, target ref, release SHA,
38
38
  release material SHA, publish tooling SHA, and durable release-state ref.
39
+ - `versionImpact`: final patch/minor/major classification, source, and
40
+ rationale.
41
+ - `surfaceImpacts`: per registered surface classification. The final impact is
42
+ the highest entry in this list.
39
43
  - `packageSet`: main package, platform packages, package-set order, registry,
40
44
  versions, dist-tags, and package digests.
41
45
  - `anchorManifest`: anchored/manual version manifest path, digest, and fields.
@@ -102,6 +106,11 @@ dist-tag, registry, role, platform, and digest, so agents do not need to stitch
102
106
  npm facts back together from the lower-level evidence files.
103
107
  `buildSummary`, `platformArtifactManifests`, and `distTagPromotion` preserve the
104
108
  build and npm dist-tag evidence chain in the same passport.
109
+ `impact.json` can be supplied with `--impact-json`; when it contains
110
+ `surfaceImpacts[]`, verification fails closed unless `versionImpact.final`
111
+ matches the highest surface impact. For example, KFD-2 content can remain patch
112
+ while an additive `registry.kind` field on the machine-consumed KFD registry
113
+ schema records a minor `kfd-registry-schema` surface impact.
105
114
 
106
115
  Verify a release passport:
107
116
 
@@ -20,7 +20,8 @@ latest stable production patch for that minor line.
20
20
 
21
21
  ## Welded Surfaces
22
22
 
23
- These surfaces require at least a minor bump when first introduced:
23
+ These surfaces are classified independently; the final release impact is the
24
+ highest impact across the affected registered surfaces:
24
25
 
25
26
  - reusable workflow inputs, outputs, and artifact contracts;
26
27
  - public CLI command families and their machine-readable JSON shapes;
@@ -31,20 +32,35 @@ These surfaces require at least a minor bump when first introduced:
31
32
  ledger, and agent index files;
32
33
  - binary distribution shapes that users can install or automate against.
33
34
 
34
- Additive fields inside an existing welded surface can be patch releases when
35
- old consumers continue to work and validation remains stricter, not looser.
36
- Removing fields, changing meanings, weakening trust gates, or changing the
37
- expected ref flow requires a major bump.
35
+ For each surface:
36
+
37
+ - content, documentation, or implementation-only work that does not touch a
38
+ registered surface is patch;
39
+ - additive fields, new commands, new exports, new evidence sections, or new
40
+ registered surfaces are minor;
41
+ - removals, incompatible renames, changed meanings, newly required fields,
42
+ weakened trust gates, or changed ref flow are major.
43
+
44
+ The release passport records this as `surfaceImpacts[]` plus
45
+ `versionImpact.final`. The final impact must equal the highest surface impact,
46
+ so an agent cannot silently label a release patch when one machine surface needs
47
+ minor review.
48
+
49
+ Example: a KFD document such as KFD-2 is content and remains patch, but adding a
50
+ `kind` field to the machine-consumed KFD `registry.json` is an additive change
51
+ to the `kfd-registry-schema` surface and therefore requires minor-impact
52
+ review. This avoids both false shortcuts: "new KFD means minor" and "all KFD
53
+ repository changes are patch".
38
54
 
39
55
  ## Decision Log
40
56
 
41
- | Date | Decision | Line | Reason |
42
- | --- | --- | --- | --- |
43
- | 2026-07-02 | Buildchain toolkit observability is a minor surface. | `v2.1` | It adds the public logging SDK, CLI observability commands, and package subpaths that consumers can import. |
44
- | 2026-07-02 | Release passport and binary distribution are a minor surface. | `v2.2` | They add agent-readable release passport files, artifact evidence, impact ledger, agent index, GitHub Release collection and verification commands, and standalone binary assets. |
45
- | 2026-07-02 | Web surface host mapping is a minor surface. | `v2.3` | It adds first-class multi-host surface bindings, reusable workflow URL outputs, per-surface deployment overrides, and an agent-readable fixture contract. |
46
- | 2026-07-03 | Infra contract lifecycle is a minor surface. | `v2.4` | It adds the provider-neutral `infra-contract` CLI command family, project type, adapter capability contract, lifecycle evidence bundle, propagation evidence, CI evidence mode, and consumer-facing contract artifacts. |
47
- | 2026-07-04 | Scheduled integration governance is a minor surface. | `v2.5` | It adds scheduled feature-branch discovery, conflict-free integration, reporting, and agent-visible governance automation for dev-line maintenance. |
57
+ | Date | Action | Line | Faces | Class | Rationale | PR |
58
+ | --- | --- | --- | --- | --- | --- | --- |
59
+ | 2026-07-04 | open-minor | `v2.5` | scheduled-integration-governance | additive | Scheduled integration governance adds scheduled feature-branch discovery, conflict-free integration, reporting, and agent-visible governance automation for dev-line maintenance. | |
60
+ | 2026-07-03 | open-minor | `v2.4` | infra-contract-lifecycle | additive | Infra contract lifecycle adds the provider-neutral `infra-contract` CLI command family, project type, adapter capability contract, lifecycle evidence bundle, propagation evidence, CI evidence mode, and consumer-facing contract artifacts. | |
61
+ | 2026-07-02 | open-minor | `v2.3` | web-surface-host-mapping | additive | Web surface host mapping adds first-class multi-host surface bindings, reusable workflow URL outputs, per-surface deployment overrides, and an agent-readable fixture contract. | |
62
+ | 2026-07-02 | open-minor | `v2.2` | release-passport, binary-distribution | additive | Release passport and binary distribution add agent-readable release passport files, artifact evidence, impact ledger, agent index, GitHub Release collection and verification commands, and standalone binary assets. | |
63
+ | 2026-07-02 | open-minor | `v2.1` | logging-sdk, cli-observability, package-subpaths | additive | Buildchain toolkit observability adds the public logging SDK, CLI observability commands, and package subpaths that consumers can import. | |
48
64
 
49
65
  ## Runner Policy
50
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.5.5-alpha.2",
3
+ "version": "2.5.5-alpha.3",
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",
@@ -36,6 +36,29 @@ function optionalString(value) {
36
36
  return value === undefined || value === null ? "" : String(value);
37
37
  }
38
38
 
39
+ const VERSION_IMPACT_ORDER = new Map([
40
+ ["unknown", 0],
41
+ ["patch", 1],
42
+ ["minor", 2],
43
+ ["major", 3],
44
+ ]);
45
+
46
+ function normalizeImpactLevel(value, fallback = "unknown") {
47
+ const normalized = optionalString(value || fallback).toLowerCase();
48
+ return VERSION_IMPACT_ORDER.has(normalized) ? normalized : optionalString(value || fallback);
49
+ }
50
+
51
+ function highestImpactLevel(levels = []) {
52
+ let highest = "unknown";
53
+ for (const level of levels) {
54
+ const normalized = normalizeImpactLevel(level);
55
+ if ((VERSION_IMPACT_ORDER.get(normalized) ?? -1) > (VERSION_IMPACT_ORDER.get(highest) ?? -1)) {
56
+ highest = normalized;
57
+ }
58
+ }
59
+ return highest;
60
+ }
61
+
39
62
  function stableJson(value) {
40
63
  if (Array.isArray(value)) {
41
64
  return `[${value.map(stableJson).join(",")}]`;
@@ -123,14 +146,21 @@ function defaultProductMechanism({ repository = "", productName = "Buildchain" }
123
146
  }
124
147
 
125
148
  function defaultImpact({ tag = "", line = "", decision = "unknown" } = {}) {
149
+ const finalImpact = normalizeImpactLevel(decision);
126
150
  return {
127
151
  schemaVersion: 1,
128
152
  contract: IMPACT_LEDGER_CONTRACT,
129
153
  release: { tag, line },
130
- classification: decision,
131
- breaking: false,
154
+ versionImpact: {
155
+ final: finalImpact,
156
+ source: "default",
157
+ rationale: "No surface impact classification was supplied.",
158
+ },
159
+ surfaceImpacts: [],
160
+ classification: finalImpact,
161
+ breaking: finalImpact === "major",
132
162
  security: false,
133
- migrationRequired: false,
163
+ migrationRequired: finalImpact === "major",
134
164
  summary: "No release impact summary was supplied.",
135
165
  recovery: {
136
166
  rollback: "Use the previous exact release tag or previous floating channel ref.",
@@ -339,6 +369,55 @@ function normalizePublishEvidence(value = undefined) {
339
369
  };
340
370
  }
341
371
 
372
+ function normalizeSurfaceImpact(entry = {}, index = 0) {
373
+ const id = nonEmptyString(entry.id || entry.surface || entry.surfaceId, `surfaceImpacts[${index}].id`);
374
+ const impact = normalizeImpactLevel(entry.impact || entry.classification || entry.versionImpact);
375
+ return {
376
+ id,
377
+ impact,
378
+ class: optionalString(entry.class || entry.changeClass || entry.change_class),
379
+ rationale: optionalString(entry.rationale || entry.reason),
380
+ source: optionalString(entry.source),
381
+ };
382
+ }
383
+
384
+ function normalizeImpactLedger(value = undefined, { tag = "", line = "", decision = "unknown" } = {}) {
385
+ if (!value) {
386
+ return defaultImpact({ tag, line, decision });
387
+ }
388
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
389
+ throw new Error("impact must be a JSON object");
390
+ }
391
+ const surfaceImpacts = Array.isArray(value.surfaceImpacts)
392
+ ? value.surfaceImpacts.map((entry, index) => normalizeSurfaceImpact(entry, index))
393
+ : [];
394
+ const finalImpact = normalizeImpactLevel(
395
+ value.versionImpact?.final ||
396
+ value.versionImpact ||
397
+ value.classification ||
398
+ decision,
399
+ );
400
+ return {
401
+ ...value,
402
+ schemaVersion: Number(value.schemaVersion || 1),
403
+ contract: value.contract || IMPACT_LEDGER_CONTRACT,
404
+ release: {
405
+ tag: optionalString(value.release?.tag || tag),
406
+ line: optionalString(value.release?.line || line),
407
+ ...(value.release && typeof value.release === "object" && !Array.isArray(value.release) ? value.release : {}),
408
+ },
409
+ versionImpact: {
410
+ final: finalImpact,
411
+ source: optionalString(value.versionImpact?.source || value.impactSource || "declared"),
412
+ rationale: optionalString(value.versionImpact?.rationale || value.rationale || value.summary),
413
+ },
414
+ surfaceImpacts,
415
+ classification: normalizeImpactLevel(value.classification || finalImpact),
416
+ breaking: value.breaking === undefined ? finalImpact === "major" : Boolean(value.breaking),
417
+ migrationRequired: value.migrationRequired === undefined ? finalImpact === "major" : Boolean(value.migrationRequired),
418
+ };
419
+ }
420
+
342
421
  function packageSetEntries(packageSet = undefined) {
343
422
  if (!packageSet) {
344
423
  return [];
@@ -503,6 +582,7 @@ export function createReleasePassport({
503
582
  distTagPromotionEvidence = undefined,
504
583
  release = {},
505
584
  publish = {},
585
+ impact = undefined,
506
586
  workflow = {},
507
587
  } = {}) {
508
588
  const normalizedTag = nonEmptyString(tag, "tag");
@@ -518,6 +598,7 @@ export function createReleasePassport({
518
598
  const normalizedDistTagPromotionEvidence = distTagPromotionEvidence
519
599
  ? normalizeEvidenceDocument(distTagPromotionEvidence, "distTagPromotionEvidence")
520
600
  : undefined;
601
+ const normalizedImpact = normalizeImpactLedger(impact, { tag: normalizedTag, line });
521
602
  const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
522
603
  const normalizedPublishSummary = normalizePublishSummary({
523
604
  packageSet: normalizedPackageSet,
@@ -618,6 +699,8 @@ export function createReleasePassport({
618
699
  ...(normalizedBuildSummary ? { buildSummary: normalizedBuildSummary } : {}),
619
700
  ...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
620
701
  ...(normalizedDistTagPromotionEvidence ? { distTagPromotion: normalizedDistTagPromotionEvidence } : {}),
702
+ versionImpact: normalizedImpact.versionImpact,
703
+ surfaceImpacts: normalizedImpact.surfaceImpacts,
621
704
  artifacts: [
622
705
  ...artifactEvidence.artifacts.map((asset) => ({
623
706
  group: "release",
@@ -680,6 +763,7 @@ export function collectGitHubReleasePassport({
680
763
  trustedPublishingJson = "",
681
764
  transactionJson = "",
682
765
  anchorManifestJson = "",
766
+ impactJson = "",
683
767
  buildSummaryJson = "",
684
768
  platformManifestJsons = [],
685
769
  distTagEvidenceJson = "",
@@ -695,6 +779,7 @@ export function collectGitHubReleasePassport({
695
779
  const trustedPublishing = parseJsonInput(trustedPublishingJson, undefined);
696
780
  const transactionMeta = parseJsonInputWithMeta(transactionJson, undefined);
697
781
  const anchorManifest = normalizeAnchorManifest(parseJsonInputWithMeta(anchorManifestJson, undefined));
782
+ const impactMeta = parseJsonInputWithMeta(impactJson, undefined);
698
783
  const buildSummaryMeta = parseJsonInputWithMeta(buildSummaryJson, undefined);
699
784
  const platformManifestMetas = (platformManifestJsons || [])
700
785
  .filter(Boolean)
@@ -710,7 +795,7 @@ export function collectGitHubReleasePassport({
710
795
  const resolvedOutputDir = path.resolve(cwd, outputDir);
711
796
  const productMechanism = defaultProductMechanism({ repository, productName });
712
797
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: resolvedTag, sourceSha, workflow });
713
- const impact = defaultImpact({ tag: resolvedTag, line, decision: "unknown" });
798
+ const impact = normalizeImpactLedger(impactMeta.value, { tag: resolvedTag, line, decision: "unknown" });
714
799
  const agentIndex = defaultAgentIndex({ tag: resolvedTag });
715
800
  const passport = createReleasePassport({
716
801
  cwd,
@@ -747,6 +832,7 @@ export function collectGitHubReleasePassport({
747
832
  : undefined,
748
833
  release: releaseExtra,
749
834
  publish,
835
+ impact,
750
836
  publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
751
837
  transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
752
838
  workflow,
@@ -1042,6 +1128,38 @@ export function createReleaseCheckReport({
1042
1128
  if (!passport?.runnerPolicy?.productionDefault) {
1043
1129
  issues.push(issue("warning", "runnerPolicy.productionDefault", "runner policy should record the production default"));
1044
1130
  }
1131
+ const surfaceImpacts = Array.isArray(impact?.surfaceImpacts) ? impact.surfaceImpacts : [];
1132
+ const passportSurfaceImpacts = Array.isArray(passport?.surfaceImpacts) ? passport.surfaceImpacts : [];
1133
+ const declaredFinalImpact = normalizeImpactLevel(impact?.versionImpact?.final || impact?.classification);
1134
+ const computedFinalImpact = highestImpactLevel(surfaceImpacts.map((entry) => entry.impact));
1135
+ if (surfaceImpacts.length > 0) {
1136
+ for (const [index, entry] of surfaceImpacts.entries()) {
1137
+ if (!entry.id) {
1138
+ issues.push(issue("error", `impact.surfaceImpacts[${index}].id`, "surface impact id is required"));
1139
+ }
1140
+ if (!VERSION_IMPACT_ORDER.has(entry.impact)) {
1141
+ issues.push(issue("error", `impact.surfaceImpacts[${index}].impact`, "surface impact must be patch, minor, or major"));
1142
+ }
1143
+ if (!entry.rationale) {
1144
+ issues.push(issue("error", `impact.surfaceImpacts[${index}].rationale`, "surface impact rationale is required"));
1145
+ }
1146
+ }
1147
+ if (!impact?.versionImpact?.rationale) {
1148
+ issues.push(issue("error", "impact.versionImpact.rationale", "version impact rationale is required when surface impacts are supplied"));
1149
+ }
1150
+ if (declaredFinalImpact !== computedFinalImpact) {
1151
+ issues.push(issue("error", "impact.versionImpact.final", "versionImpact.final must equal the highest surface impact", {
1152
+ declared: declaredFinalImpact,
1153
+ computed: computedFinalImpact,
1154
+ }));
1155
+ }
1156
+ if (stableJson(passportSurfaceImpacts) !== stableJson(surfaceImpacts)) {
1157
+ issues.push(issue("error", "passport.surfaceImpacts", "passport.surfaceImpacts must mirror impact.surfaceImpacts"));
1158
+ }
1159
+ }
1160
+ if (passport?.versionImpact?.final && impact?.versionImpact?.final && passport.versionImpact.final !== impact.versionImpact.final) {
1161
+ issues.push(issue("error", "passport.versionImpact.final", "passport.versionImpact.final must match impact.versionImpact.final"));
1162
+ }
1045
1163
  const ok = issues.every((entry) => entry.level !== "error");
1046
1164
  return {
1047
1165
  schemaVersion: 1,
@@ -1064,6 +1182,8 @@ export function createReleaseCheckReport({
1064
1182
  impactPresent: Boolean(impact),
1065
1183
  agentIndexPresent: Boolean(agentIndex),
1066
1184
  productMechanismPresent: Boolean(productMechanism),
1185
+ surfaceImpactCount: surfaceImpacts.length,
1186
+ versionImpact: impact?.versionImpact?.final || "",
1067
1187
  },
1068
1188
  issues,
1069
1189
  };
@@ -1156,8 +1276,11 @@ export async function explainReleasePassport({ passportLocation, forAudience = "
1156
1276
  artifactCount: report.completeness.artifactCount,
1157
1277
  runnerPolicy: passport.runnerPolicy,
1158
1278
  impact: {
1159
- breaking: false,
1160
- migrationRequired: false,
1279
+ versionImpact: passport.versionImpact || {},
1280
+ surfaceImpacts: Array.isArray(passport.surfaceImpacts) ? passport.surfaceImpacts : [],
1281
+ breaking: Boolean(passport.versionImpact?.final === "major"),
1282
+ migrationRequired: Boolean(passport.versionImpact?.final === "major"),
1283
+ summary: passport.versionImpact?.rationale || "",
1161
1284
  },
1162
1285
  recovery: passport.recovery,
1163
1286
  nextAction,
@@ -130,7 +130,9 @@ if (!coreIndexSource.includes("reportBuildchainIssue")) {
130
130
  throw new Error("packages/core/index.js must export reportBuildchainIssue");
131
131
  }
132
132
  for (const requiredSnippet of [
133
- "Release passport and binary distribution are a minor surface.",
133
+ "surfaceImpacts[]",
134
+ "versionImpact.final",
135
+ "kfd-registry-schema",
134
136
  "`v2.2`",
135
137
  "GitHub-hosted runners for production",
136
138
  "Self-hosted runners remain compatibility fixtures",