@kungfu-tech/buildchain 3.0.6 → 3.0.7-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 (103) hide show
  1. package/README.md +4 -2
  2. package/actions/promote-buildchain-ref/README.md +10 -0
  3. package/actions/release-tail/README.md +17 -0
  4. package/bin/buildchain.mjs +11 -0
  5. package/bin/internal/command-registry.mjs +2 -0
  6. package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
  7. package/contracts/engineering-housekeeper-v1.schema.json +143 -0
  8. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
  9. package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
  10. package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
  11. package/contracts/release-tail-capabilities-v1.schema.json +199 -0
  12. package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
  13. package/dist/site/agent-index.json +3 -0
  14. package/dist/site/artifact-schemas.json +6 -0
  15. package/dist/site/buildchain-contract.json +42 -27
  16. package/dist/site/buildchain-site.json +384 -33
  17. package/dist/site/capability-registry.json +15 -12
  18. package/dist/site/cli-registry.json +102 -0
  19. package/dist/site/controller-registry.json +19 -3
  20. package/dist/site/kfd-claims.json +359 -17
  21. package/dist/site/kfd-upstream-aggregate.json +1 -1
  22. package/dist/site/manual-registry.json +53 -7
  23. package/dist/site/node-api-registry.json +5882 -3605
  24. package/dist/site/page-registry.json +348 -21
  25. package/dist/site/public-surface-audit.json +396 -16
  26. package/dist/site/publication-authority-registry.json +106 -1
  27. package/dist/site/publication-registry.json +4 -4
  28. package/dist/site/release-provenance.json +7 -0
  29. package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
  30. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
  31. package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
  32. package/dist/site/site-manifest.json +35 -11
  33. package/dist/site/workflow-registry.json +204 -10
  34. package/docs/MAP.md +6 -2
  35. package/docs/auditable-demo.md +2 -2
  36. package/docs/cli-reference.md +136 -0
  37. package/docs/dev-delivery-warrant.md +49 -4
  38. package/docs/engineering-housekeeper.md +138 -0
  39. package/docs/lifecycle-protocol.md +4 -2
  40. package/docs/node-api-reference.md +495 -281
  41. package/docs/publication-rehearsal.md +94 -0
  42. package/docs/release-governance.md +17 -2
  43. package/docs/release-tail-contract.md +160 -0
  44. package/docs/release-tail-provider-plane.md +120 -0
  45. package/docs/reusable-build-surface.md +11 -0
  46. package/package.json +11 -3
  47. package/packages/core/artifact-signing.js +61 -0
  48. package/packages/core/buildchain-agent-manuals.js +3 -0
  49. package/packages/core/buildchain-config.js +66 -6
  50. package/packages/core/buildchain-kfd-claims.js +1 -1
  51. package/packages/core/buildchain-publication-authority.js +5 -0
  52. package/packages/core/controller-evidence.js +2 -1
  53. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  54. package/packages/core/dev-delivery-warrant-shadow.js +502 -0
  55. package/packages/core/dev-delivery-warrant.js +15 -6
  56. package/packages/core/diagnostics.js +8 -3
  57. package/packages/core/engineering-housekeeper-github-client.js +222 -0
  58. package/packages/core/engineering-housekeeper-github.js +501 -0
  59. package/packages/core/engineering-housekeeper.js +259 -0
  60. package/packages/core/index.js +42 -0
  61. package/packages/core/kfd-gate.js +45 -15
  62. package/packages/core/paper-agent-entry.js +11 -5
  63. package/packages/core/paper-repository.js +1 -0
  64. package/packages/core/paper-scaffold-content.js +21 -0
  65. package/packages/core/paper.js +28 -2
  66. package/packages/core/publication-rehearsal-projection.js +173 -0
  67. package/packages/core/publication-rehearsal-runtime.js +921 -0
  68. package/packages/core/release-passport.js +130 -20
  69. package/packages/core/release-tail-compatibility.js +60 -0
  70. package/packages/core/release-tail-provider-adapters.js +461 -0
  71. package/packages/core/release-tail-provider-plane.js +1228 -0
  72. package/scripts/assemble-publication-artifact-admission.mjs +1 -1
  73. package/scripts/assemble-self-publication-admission.mjs +2 -2
  74. package/scripts/audit-publication-control-plane.mjs +1 -1
  75. package/scripts/auditable-demo-bundle-verification.mjs +2 -3
  76. package/scripts/auditable-demo-platform.mjs +2 -2
  77. package/scripts/auditable-demo-renditions.mjs +1 -1
  78. package/scripts/auditable-demo.mjs +2 -2
  79. package/scripts/build-contract-core.mjs +8 -3
  80. package/scripts/build-standalone-binary.mjs +14 -3
  81. package/scripts/buildchain-cli-help.mjs +13 -0
  82. package/scripts/check-core-mechanism-inventory.mjs +347 -0
  83. package/scripts/check-inventory.mjs +10 -8
  84. package/scripts/check-maintainability.mjs +9 -2
  85. package/scripts/check-release-tail-contract.mjs +435 -0
  86. package/scripts/dev-delivery-warrant.mjs +31 -4
  87. package/scripts/dev-pr-auto-merge.mjs +30 -4
  88. package/scripts/dev-pr-delivery-warrant.mjs +50 -0
  89. package/scripts/engineering-housekeeper-workflow.mjs +394 -0
  90. package/scripts/generate-channel-promotion-workflow.mjs +10 -8
  91. package/scripts/generate-site-bundle.mjs +47 -4
  92. package/scripts/init-repo.mjs +26 -2
  93. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  94. package/scripts/materialize-self-release-candidate-version.mjs +137 -0
  95. package/scripts/publication-commit-evidence.mjs +69 -23
  96. package/scripts/release-candidate-resolver.mjs +16 -10
  97. package/scripts/release-tail.mjs +159 -0
  98. package/scripts/resume-from-candidate-run.mjs +123 -9
  99. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  100. package/scripts/site-capability-metadata.mjs +13 -0
  101. package/scripts/v4-architecture.mjs +600 -0
  102. package/scripts/web-surface-core.mjs +8 -2
  103. package/scripts/workflow-call-contract.mjs +184 -5
@@ -146,6 +146,15 @@ function invariantSemanticPreimage(passport) {
146
146
  return value;
147
147
  }
148
148
 
149
+ function hasCompleteInvariantCoverage(value) {
150
+ const coverage = value?.coverage;
151
+ return Number.isInteger(coverage?.required)
152
+ && coverage.required > 0
153
+ && coverage.verified === coverage.required
154
+ && [coverage.missing, coverage.falsified, coverage.unqualified]
155
+ .every((items) => Array.isArray(items) && items.length === 0);
156
+ }
157
+
149
158
  function normalizeInvariantPassport(meta, index = 0) {
150
159
  const value = meta?.value;
151
160
  const label = `invariantPassportJsons[${index}]`;
@@ -165,11 +174,18 @@ function normalizeInvariantPassport(meta, index = 0) {
165
174
  if (value.passportRoot !== expectedRoot) {
166
175
  throw new Error(`${label}.passportRoot mismatch: expected ${expectedRoot}, got ${value.passportRoot}`);
167
176
  }
168
- if (value.verdict !== "verified") {
169
- throw new Error(`${label}.verdict must be verified, got ${value.verdict}`);
170
- }
171
- if (value.coverage?.complete !== true) {
172
- throw new Error(`${label}.coverage.complete must be true`);
177
+ const verifiedPassport = value.verdict === "verified" && value.coverage?.complete === true;
178
+ const scopedUnqualifiedClaim = value.verdict === "unqualified"
179
+ && value.coverage?.complete === false
180
+ && hasCompleteInvariantCoverage(value)
181
+ && value.releaseClaims?.verdict === "unqualified"
182
+ && /^sha256:[0-9a-f]{64}$/.test(optionalString(value.releaseClaims?.claimRoot))
183
+ && Array.isArray(value.releaseClaims?.diagnostics)
184
+ && value.releaseClaims.diagnostics.length > 0
185
+ && Array.isArray(value.diagnostics)
186
+ && value.diagnostics.length === 0;
187
+ if (!verifiedPassport && !scopedUnqualifiedClaim) {
188
+ throw new Error(`${label}.verdict must be verified, or unqualified only for complete invariant coverage with one explicit unqualified releaseClaims section`);
173
189
  }
174
190
  if (value.source?.dirty !== false) {
175
191
  throw new Error(`${label}.source.dirty must be false`);
@@ -182,6 +198,9 @@ function normalizeInvariantPassport(meta, index = 0) {
182
198
  : [];
183
199
  if (platforms.length === 0) throw new Error(`${label}.coverage.platforms must be non-empty`);
184
200
  if (!Array.isArray(value.residualRisk)) throw new Error(`${label}.residualRisk must be an array`);
201
+ const releaseClaimRisks = scopedUnqualifiedClaim
202
+ ? value.releaseClaims.diagnostics.map((entry) => `Exit/provider claim ${optionalString(entry?.code) || "unqualified"}: ${optionalString(entry?.message) || "consumer claim remains unqualified"}`)
203
+ : [];
185
204
  return {
186
205
  path: optionalString(meta.path),
187
206
  sha256: optionalString(meta.sha256),
@@ -194,7 +213,15 @@ function normalizeInvariantPassport(meta, index = 0) {
194
213
  source: structuredClone(value.source),
195
214
  coverage: structuredClone(value.coverage),
196
215
  platforms,
197
- residualRisk: structuredClone(value.residualRisk),
216
+ admission: {
217
+ scope: "consumer-invariant-coverage",
218
+ result: "passed",
219
+ consumerVerdict: value.verdict,
220
+ releaseClaimsVerdict: optionalString(value.releaseClaims?.verdict),
221
+ },
222
+ releaseClaims: value.releaseClaims ? structuredClone(value.releaseClaims) : undefined,
223
+ diagnostics: Array.isArray(value.diagnostics) ? structuredClone(value.diagnostics) : [],
224
+ residualRisk: [...new Set([...value.residualRisk, ...releaseClaimRisks])].sort(),
198
225
  };
199
226
  }
200
227
 
@@ -533,7 +560,12 @@ function mergeAuthoritativeImpactBase(impact, basePassport = undefined) {
533
560
  return merged;
534
561
  }
535
562
 
536
- function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "command" } = {}) {
563
+ function parseJsonCommandOutput({
564
+ command = "",
565
+ cwd = process.cwd(),
566
+ label = "command",
567
+ acceptNonzeroJson = undefined,
568
+ } = {}) {
537
569
  const normalized = String(command || "").trim();
538
570
  if (!normalized) {
539
571
  return { value: undefined, path: "", sha256: "" };
@@ -547,7 +579,7 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
547
579
  if (result.error) {
548
580
  throw new Error(`${label} failed to start: ${result.error.message}`);
549
581
  }
550
- if (result.status !== 0) {
582
+ if (result.status !== 0 && typeof acceptNonzeroJson !== "function") {
551
583
  const stderr = String(result.stderr || "").trim();
552
584
  throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
553
585
  }
@@ -561,10 +593,15 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
561
593
  } catch (error) {
562
594
  throw new Error(`${label} output must be valid JSON: ${error.message}`, { cause: error });
563
595
  }
596
+ if (result.status !== 0 && !acceptNonzeroJson({ status: result.status, value: parsed })) {
597
+ const stderr = String(result.stderr || "").trim();
598
+ throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
599
+ }
564
600
  return {
565
601
  value: parsed,
566
602
  path: "",
567
603
  sha256: sha256Text(stableJson(parsed)),
604
+ status: result.status,
568
605
  };
569
606
  }
570
607
 
@@ -879,6 +916,41 @@ function arrayOrSingleton(value) {
879
916
  return value && typeof value === "object" ? [value] : [];
880
917
  }
881
918
 
919
+ const KFD2_RESIDUAL_RISK_SCHEMA = "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk";
920
+
921
+ function normalizeKfd2ResidualRisk(entry, { claim = {}, claimIndex = 0, riskIndex = 0 } = {}) {
922
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
923
+ return entry;
924
+ }
925
+ const reason = optionalString(entry).trim();
926
+ if (!reason) {
927
+ return entry;
928
+ }
929
+ const claimId = optionalString(claim.id || `claim-${claimIndex + 1}`)
930
+ .toLowerCase()
931
+ .replace(/[^a-z0-9]+/g, "-")
932
+ .replace(/^-+|-+$/g, "") || `claim-${claimIndex + 1}`;
933
+ const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility)
934
+ ? claim.responsibility
935
+ : {};
936
+ return {
937
+ id: `${claimId}-residual-risk-${riskIndex + 1}`,
938
+ definedBy: KFD2_RESIDUAL_RISK_SCHEMA,
939
+ riskType: "manual-review-risk",
940
+ trustImpact: "downgrade-warning",
941
+ machineProvability: "not-machine-verifiable",
942
+ agentAction: "request-maintainer-review",
943
+ reason,
944
+ owner: optionalString(
945
+ responsibility.releaseDecisionOwner
946
+ || responsibility.owner
947
+ || responsibility.sourceOwner
948
+ || responsibility.sourceContractOwner
949
+ || "release maintainer",
950
+ ),
951
+ };
952
+ }
953
+
882
954
  function normalizeKfd2Claim(raw = {}, index = 0) {
883
955
  const claim = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
884
956
  const sourceBindings = arrayOrEmpty(claim.sourceBindings || claim.source_bindings || claim.sources || claim.declaredSources);
@@ -892,10 +964,13 @@ function normalizeKfd2Claim(raw = {}, index = 0) {
892
964
  ? claim.audit_boundary
893
965
  : {};
894
966
  const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility) ? claim.responsibility : {};
895
- const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(entry, {
896
- kind: "residualRisk",
897
- label: `kfd-2.claims[${index}].residualRisk[${riskIndex}]`,
898
- }));
967
+ const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(
968
+ normalizeKfd2ResidualRisk(entry, { claim, claimIndex: index, riskIndex }),
969
+ {
970
+ kind: "residualRisk",
971
+ label: `kfd-2.claims[${index}].residualRisk[${riskIndex}]`,
972
+ },
973
+ ));
899
974
  const downgradeReasons = arrayOrSingleton(claim.downgradeReasons || claim.downgrade_reasons || claim.downgradeReason || claim.downgrade_reason)
900
975
  .map((entry, reasonIndex) => validateKfd2TrustTaxonomyEntry(entry, {
901
976
  kind: "downgradeReason",
@@ -1494,6 +1569,7 @@ export function collectGitHubReleasePassport({
1494
1569
  outputDir = ".buildchain/release-passport",
1495
1570
  assetsJson = "",
1496
1571
  assetsDir = "",
1572
+ kfdArtifactSearchRoots = [],
1497
1573
  releaseJson = "",
1498
1574
  productName = "Buildchain",
1499
1575
  packageName = "@kungfu-tech/buildchain",
@@ -1568,6 +1644,11 @@ export function collectGitHubReleasePassport({
1568
1644
  .filter(Boolean)
1569
1645
  .map((witnessJson) => parseJsonInputWithMeta(witnessJson, undefined, { cwd, label: "kfd3ArtifactWitnessJsons entry" }))
1570
1646
  .filter((meta) => meta.value);
1647
+ const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
1648
+ command: kfd3ArtifactVerifyCommand,
1649
+ cwd,
1650
+ label: "KFD-3 artifact verify command",
1651
+ });
1571
1652
  const kfdSupportMatrixMeta = parseJsonInputWithMeta(
1572
1653
  kfdSupportMatrixJson,
1573
1654
  undefined,
@@ -1578,11 +1659,6 @@ export function collectGitHubReleasePassport({
1578
1659
  .map((gateJson) => parseJsonInputWithMeta(gateJson, undefined, { cwd, label: "kfdProductGateJsons entry" }))
1579
1660
  .filter((meta) => meta.value);
1580
1661
  const basePassportMeta = parseJsonInputWithMeta(basePassportJson, undefined, { cwd, label: "basePassportJson" });
1581
- const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
1582
- command: kfd3ArtifactVerifyCommand,
1583
- cwd,
1584
- label: "KFD-3 artifact verify command",
1585
- });
1586
1662
  const invariantPassportMetas = (invariantPassportJsons || [])
1587
1663
  .filter(Boolean)
1588
1664
  .map((passportJson) => parseJsonInputWithMeta(passportJson, undefined, { cwd, label: "invariantPassportJsons entry" }))
@@ -1591,6 +1667,22 @@ export function collectGitHubReleasePassport({
1591
1667
  command: invariantPassportCommand,
1592
1668
  cwd,
1593
1669
  label: "invariant passport command",
1670
+ acceptNonzeroJson: ({ status, value }) => {
1671
+ if (status !== 2) return false;
1672
+ try {
1673
+ const normalized = normalizeInvariantPassport(
1674
+ { value, path: "", sha256: sha256Text(stableJson(value)) },
1675
+ invariantPassportMetas.length,
1676
+ );
1677
+ return normalized.verdict === "unqualified"
1678
+ && normalized.coverage?.complete === false
1679
+ && normalized.releaseClaims?.verdict === "unqualified"
1680
+ && normalized.admission?.scope === "consumer-invariant-coverage"
1681
+ && normalized.admission?.result === "passed";
1682
+ } catch {
1683
+ return false;
1684
+ }
1685
+ },
1594
1686
  });
1595
1687
  if (invariantPassportCommandMeta.value) invariantPassportMetas.push(invariantPassportCommandMeta);
1596
1688
  const invariantPassports = createInvariantPassportGate(invariantPassportMetas);
@@ -1664,6 +1756,7 @@ export function collectGitHubReleasePassport({
1664
1756
  const kfd1 = createKfd1ReleaseGateEvidence({
1665
1757
  cwd,
1666
1758
  artifactRoot: assetsDir ? path.resolve(cwd, assetsDir) : "",
1759
+ artifactSearchRoots: kfdArtifactSearchRoots.map((root) => path.resolve(cwd, root)),
1667
1760
  artifacts: assets,
1668
1761
  witnesses: kfd1WitnessMetas.map((meta) => meta.value),
1669
1762
  });
@@ -2092,7 +2185,17 @@ function validateReleaseEvidenceContracts({
2092
2185
  for (const [index, value] of (passport?.githubArtifactAttestations || []).entries()) {
2093
2186
  try {
2094
2187
  const policy = normalizeGitHubArtifactAttestationPolicy(value);
2095
- if (policy.caller.sourceSha !== String(passport?.release?.sourceSha || "").toLowerCase()) {
2188
+ const release = passport?.release || {};
2189
+ const acceptedSourceShas = new Set([String(release.sourceSha || "").toLowerCase()]);
2190
+ if (
2191
+ release.treeEquivalent === true
2192
+ && release.builtSourceTreeSha
2193
+ && release.builtSourceTreeSha === release.promotionChannelTreeSha
2194
+ && release.builtSourceSha
2195
+ ) {
2196
+ acceptedSourceShas.add(String(release.builtSourceSha).toLowerCase());
2197
+ }
2198
+ if (!acceptedSourceShas.has(policy.caller.sourceSha)) {
2096
2199
  issues.push(issue(
2097
2200
  "error",
2098
2201
  `githubArtifactAttestations[${index}].caller.sourceSha`,
@@ -2490,8 +2593,15 @@ function validatePassportTrustSections({ passport, issues }) {
2490
2593
  ].filter(Boolean));
2491
2594
  for (const [index, entry] of (section.passports || []).entries()) {
2492
2595
  const prefix = `invariantPassports.passports[${index}]`;
2493
- if (entry.verdict !== "verified") issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified`));
2494
- if (entry.coverage?.complete !== true) issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage.complete must be true`));
2596
+ if (entry.admission?.scope !== "consumer-invariant-coverage" || entry.admission?.result !== "passed") {
2597
+ issues.push(issue("error", `${prefix}.admission`, `${prefix}.admission must record passed consumer invariant coverage`));
2598
+ }
2599
+ if (entry.verdict !== "verified" && !(entry.verdict === "unqualified" && entry.releaseClaims?.verdict === "unqualified")) {
2600
+ issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified or preserve one explicit unqualified releaseClaims verdict`));
2601
+ }
2602
+ if (entry.coverage?.complete !== true && !hasCompleteInvariantCoverage(entry)) {
2603
+ issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage must be complete or prove every required invariant coordinate verified`));
2604
+ }
2495
2605
  if (entry.source?.dirty !== false) issues.push(issue("error", `${prefix}.source.dirty`, `${prefix}.source.dirty must be false`));
2496
2606
  if (acceptedSourceShas.size > 0 && !acceptedSourceShas.has(entry.source?.revision)) {
2497
2607
  issues.push(issue("error", `${prefix}.source.revision`, `${prefix}.source.revision must match a release source identity`));
@@ -0,0 +1,60 @@
1
+ export const RELEASE_TAIL_COMPATIBILITY_CONTRACT =
2
+ "kungfu.buildchain.release-tail.compatibility-diagnostics/v1";
3
+
4
+ export const RELEASE_TAIL_LEGACY_HOOKS = Object.freeze({
5
+ "publication-gate-command": "declarative admission predicates",
6
+ "publication-consumer-qualification-command":
7
+ "declarative readback predicates",
8
+ "publish-command": "artifact.publish",
9
+ "lifecycle.publish": "artifact.publish",
10
+ "publication-commit-command": "signed-channel.commit",
11
+ "release-activation-command": "release.activate",
12
+ "release-passport-evidence-command": "released-evidence.synthesize",
13
+ "release-passport-attachment-command": "typed evidence requirements",
14
+ "verification-command": "the separate version-state verification contract",
15
+ });
16
+
17
+ function present(value) {
18
+ if (value === undefined || value === null || value === false) return false;
19
+ if (typeof value === "string") return value.trim() !== "";
20
+ return true;
21
+ }
22
+
23
+ export function diagnoseLegacyReleaseTailHooks(hooks = {}) {
24
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
25
+ throw new Error("legacy release-tail hook diagnostics require an object");
26
+ }
27
+ const diagnostics = [];
28
+ for (const name of Object.keys(hooks).sort()) {
29
+ if (!present(hooks[name])) continue;
30
+ const replacement = RELEASE_TAIL_LEGACY_HOOKS[name];
31
+ if (!replacement) {
32
+ diagnostics.push({
33
+ code: "release-tail-command-forbidden",
34
+ hook: name,
35
+ level: "error",
36
+ message: `unregistered executable release-tail hook '${name}' is forbidden`,
37
+ replacement: "none",
38
+ });
39
+ continue;
40
+ }
41
+ diagnostics.push({
42
+ code: "release-tail-hook-deprecated",
43
+ hook: name,
44
+ level: "warning",
45
+ message: `legacy release-tail hook '${name}' is deprecated; use ${replacement}`,
46
+ replacement,
47
+ });
48
+ }
49
+ return {
50
+ schema: RELEASE_TAIL_COMPATIBILITY_CONTRACT,
51
+ compatible: diagnostics.every((entry) => entry.level !== "error"),
52
+ migrationWindow: {
53
+ startsAt: "train/v3/v3.0/release-tail-contract",
54
+ closesAt: "earlier-of-90-days-or-first-v3.2-stable",
55
+ maximumMinorLines: 2,
56
+ permanentEscapeHatch: false,
57
+ },
58
+ diagnostics,
59
+ };
60
+ }