@kungfu-tech/buildchain 4.1.2 → 4.1.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 (64) hide show
  1. package/CONTRIBUTING.md +7 -4
  2. package/architecture/action-taxonomy.json +4 -0
  3. package/architecture/agent-change-map.md +44 -0
  4. package/architecture/internal-capabilities.json +56 -0
  5. package/architecture/maintainability-debt.json +2 -1
  6. package/architecture/maintainability-policy.json +4 -4
  7. package/architecture/release-topology.json +14 -1
  8. package/architecture/universal-workflow-bootstrap.json +13 -2
  9. package/architecture/universal-workflow-capability-policy.json +14 -8
  10. package/contracts/promotion-invocation-v1.schema.json +6 -3
  11. package/contracts/promotion-request-v1.schema.json +6 -3
  12. package/contracts/release-discussion-v1.schema.json +143 -0
  13. package/dist/readers/release-discussion.cjs +207 -0
  14. package/dist/site/buildchain-contract.json +15 -11
  15. package/dist/site/buildchain-site.json +64 -8
  16. package/dist/site/capability-registry.json +3 -3
  17. package/dist/site/kfd-claims.json +95 -9
  18. package/dist/site/kfd-upstream-aggregate.json +1 -1
  19. package/dist/site/manual-registry.json +1 -1
  20. package/dist/site/node-api-registry.json +83 -1
  21. package/dist/site/page-registry.json +59 -3
  22. package/dist/site/public-surface-audit.json +53 -7
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/release-provenance.json +2 -1
  25. package/dist/site/site-manifest.json +5 -5
  26. package/dist/site/workflow-registry.json +79 -7
  27. package/docs/node-api-reference.md +9 -0
  28. package/docs/release-discussions.md +193 -0
  29. package/package.json +10 -6
  30. package/packages/core/providers/github/discussions/materials.js +157 -0
  31. package/packages/core/providers/github/discussions/transport.js +126 -0
  32. package/packages/core/publication/binary/action.js +21 -7
  33. package/packages/core/release/discussion/actions.js +65 -0
  34. package/packages/core/release/discussion/binary.js +103 -0
  35. package/packages/core/release/discussion/checkpoints.js +282 -0
  36. package/packages/core/release/discussion/envelope.js +142 -0
  37. package/packages/core/release/discussion/evidence.js +51 -0
  38. package/packages/core/release/discussion/presentation.js +92 -0
  39. package/packages/core/release/discussion/publication.js +123 -0
  40. package/packages/core/release/discussion/qualification.js +122 -0
  41. package/packages/core/release/discussion/reader-entry.js +11 -0
  42. package/packages/core/release/discussion/reader.js +177 -0
  43. package/packages/core/release/discussion/recovery.js +132 -0
  44. package/packages/core/release/discussion/session.js +115 -0
  45. package/packages/core/release/discussion/store.js +166 -0
  46. package/packages/core/release/discussion/threads.js +83 -0
  47. package/packages/core/release/github-release.js +3 -1
  48. package/packages/core/release/promote-candidate/action.js +86 -6
  49. package/packages/core/release/promote-candidate/preparation.js +106 -0
  50. package/packages/core/release/promote-candidate/product-provider.js +4 -1
  51. package/packages/core/release/promote-candidate/provider-settlement.js +36 -23
  52. package/packages/core/release/promote-candidate/transaction.js +55 -97
  53. package/packages/core/release/promotion/candidate.js +11 -2
  54. package/packages/core/release/promotion/qualification.js +5 -2
  55. package/packages/core/release/promotion-request.js +8 -4
  56. package/packages/core/workflow/engine/execution.js +5 -1
  57. package/packages/core/workflow/engine/provider-context.js +2 -0
  58. package/packages/core/workflow/engine/release-observation.js +1 -0
  59. package/packages/core/workflow/engine/release-promotion.js +38 -26
  60. package/packages/core/workflow/universal-workflow-bootstrap.js +1 -0
  61. package/scripts/build-release-discussion-reader.mjs +34 -0
  62. package/scripts/inventory/binary.mjs +1 -1
  63. package/scripts/maintainability-metrics.mjs +1 -0
  64. package/scripts/site-capability-metadata.mjs +1 -0
@@ -1,103 +1,47 @@
1
- import {
2
- resolveCandidateBuildSummaryPath,
3
- resolveCandidateProviderInputs,
4
- resolvePublicationTarget,
5
- } from "./evidence-inputs.js";
6
- import {
7
- assertCandidateEvidenceBinding,
8
- canonicalChannel,
9
- observeProtectedPublicationSource,
10
- } from "./evidence-binding.js";
11
- import { productProviderRequest } from "./provider-request.js";
12
- import { createReleaseDocuments } from "./release-documents.js";
1
+ import { prepareCandidatePublication } from "./preparation.js";
13
2
  import { applyAndSettle } from "./provider-settlement.js";
14
- import { read } from "./files.js";
15
3
  import { candidatePublicationOutputs } from "./outputs.js";
16
4
  import { completePublicationDevelopment } from "./publication-completion.js";
17
5
  import {
18
- activateExactPnpm,
19
- planProductPublication,
20
- } from "./product-provider.js";
21
- import path from "node:path";
6
+ readApplyDocuments,
7
+ verifyPublicationSettlement,
8
+ } from "../../publication/settlement/transaction.js";
9
+ import { canonicalChannel } from "./evidence-binding.js";
10
+ import { activateExactPnpm } from "./product-provider.js";
11
+
22
12
  export async function promoteReleaseCandidate(
23
13
  request,
24
- { octokit, mutationOctokit, actor, runId, observe = () => {} },
25
- ) {
26
- const repository = request["repository"];
27
- const declaredSourceSha = request["source-sha"];
28
- const fallbackVersion = request["version"];
29
- const fallbackTag = request["tag"];
30
- const channel = request["channel"];
31
- const expectedTransactionId = request["resume-transaction-id"];
32
- const candidatePassportPath = request["candidate-passport-path"];
33
- const buildSummaryPath = resolveCandidateBuildSummaryPath({
34
- declaredPath: request["candidate-build-summary-path"],
35
- });
36
- const candidate = read(candidatePassportPath);
37
- const stageCapsules = read(request["stage-capsules-path"]);
38
- const qualification = read(request["publication-qualification-path"]);
39
- const token = request["token"];
40
- const publicationTarget = resolvePublicationTarget({
41
- recoveryReceiptPath: request["recovery-receipt-path"],
42
- candidate,
43
- repository,
44
- channel,
45
- sourceSha: declaredSourceSha,
46
- targetRef: request["target-ref"],
47
- targetSha: request["target-sha"],
48
- expectedTransactionId,
49
- });
50
- const sourceSha = publicationTarget.sourceSha;
51
- assertCandidateEvidenceBinding({ candidate, stageCapsules, repository });
52
- const sourceBinding = await observeProtectedPublicationSource({
53
- octokit,
54
- repository,
55
- protectedSourceSha: sourceSha,
56
- candidate,
57
- });
58
- const providerInputs = resolveCandidateProviderInputs({
59
- recoveryReceiptPath: request["recovery-receipt-path"],
60
- artifactKind: request["publish-artifact-kind"] || "npm",
61
- sealedBundleRoot: request["sealed-bundle-root"],
62
- sealedBundleManifest: request["sealed-bundle-manifest"],
63
- requiredArtifactsPath: request["required-artifacts-path"],
64
- publishPackageMain: request["publish-package-main"],
65
- });
66
- const providerRequest = productProviderRequest({
67
- request,
68
- actor,
69
- runId,
14
+ {
70
15
  octokit,
71
16
  mutationOctokit,
72
- repository,
73
- targetRef: publicationTarget.targetRef,
74
- targetSha: publicationTarget.targetSha,
75
- candidate,
76
- candidatePassportPath,
77
- buildSummaryPath,
78
- qualification,
79
- providerInputs,
80
- });
81
- const publicationPlan = await planProductPublication(providerRequest, {
82
- fallbackVersion,
83
- fallbackTag,
84
- });
85
- const documents = await createReleaseDocuments({
86
- request,
87
17
  actor,
88
18
  runId,
19
+ observe = () => {},
20
+ observeNode = (_node, effect) => effect(),
21
+ },
22
+ ) {
23
+ const context = await observeNode("qualification", async () => {
24
+ const prepared = await prepareCandidatePublication(request, {
25
+ octokit,
26
+ mutationOctokit,
27
+ actor,
28
+ runId,
29
+ });
30
+ if (request.retainRecoveryMaterials)
31
+ await request.retainRecoveryMaterials();
32
+ return prepared;
33
+ });
34
+ const {
89
35
  repository,
90
36
  sourceSha,
91
- fallbackVersion,
37
+ token,
92
38
  channel,
93
- candidate,
94
- stageCapsules,
95
39
  qualification,
96
- sourceBinding,
40
+ providerRequest,
97
41
  publicationPlan,
98
- publicationIntent: providerRequest.publicationIntent,
99
- octokit,
100
- });
42
+ documents,
43
+ sourceBinding,
44
+ } = context;
101
45
  activateExactPnpm();
102
46
  const settlement = await applyAndSettle({
103
47
  request,
@@ -111,20 +55,34 @@ export async function promoteReleaseCandidate(
111
55
  providerRequest,
112
56
  publicationPlan,
113
57
  documents,
58
+ observeNode,
114
59
  });
115
- await completePublicationDevelopment({
116
- repository,
117
- sourceSha,
118
- token,
119
- channel: canonicalChannel(channel),
120
- settlement,
121
- documents,
122
- sourceBinding,
123
- providerRequest,
124
- octokit,
125
- mutationOctokit,
126
- });
60
+ await observeNode("next-development", () =>
61
+ completePublicationDevelopment({
62
+ repository,
63
+ sourceSha,
64
+ token,
65
+ channel: canonicalChannel(channel),
66
+ settlement,
67
+ documents,
68
+ sourceBinding,
69
+ providerRequest,
70
+ octokit,
71
+ mutationOctokit,
72
+ }),
73
+ );
127
74
  const outputs = candidatePublicationOutputs(documents, settlement);
75
+ await observeNode(
76
+ "settlement",
77
+ async () =>
78
+ verifyPublicationSettlement(readApplyDocuments(".buildchain"), {
79
+ repository,
80
+ tag: documents.tag,
81
+ sourceSha: settlement.productProviderResult.publication.releaseSha,
82
+ candidateSha: sourceSha,
83
+ }),
84
+ (status) => status,
85
+ );
128
86
  observe(outputs);
129
87
  return { documents, settlement, outputs };
130
88
  }
@@ -1,3 +1,4 @@
1
+ import { recoverDiscussionCandidate } from "../discussion/recovery.js";
1
2
  import { resolveReleaseCandidateArtifacts } from "../candidate/resolve.js";
2
3
  import { resumeFromCandidateRun } from "../recovery/candidate.js";
3
4
  export async function qualifyPromotionCandidate(
@@ -16,6 +17,7 @@ export async function qualifyPromotionCandidate(
16
17
  {
17
18
  fresh = resolveReleaseCandidateArtifacts,
18
19
  recover = resumeFromCandidateRun,
20
+ recoverDiscussion = recoverDiscussionCandidate,
19
21
  } = {},
20
22
  ) {
21
23
  const shared = {
@@ -29,6 +31,15 @@ export async function qualifyPromotionCandidate(
29
31
  runtimeSha,
30
32
  outputDir,
31
33
  };
34
+ if (request["resume-discussion-id"])
35
+ return recoverDiscussion({
36
+ discussionId: request["resume-discussion-id"],
37
+ repository,
38
+ token,
39
+ outputDir,
40
+ targetSha: intent["requested-sha"],
41
+ targetRef: intent["target-ref"],
42
+ });
32
43
  if (request["resume-candidate-run-id"])
33
44
  return recover({
34
45
  ...shared,
@@ -52,8 +63,6 @@ export async function qualifyPromotionCandidate(
52
63
  runtimeRoot,
53
64
  recoveryRunId,
54
65
  recoveryRunAttempt,
55
-
56
-
57
66
  });
58
67
  return fresh({
59
68
  ...shared,
@@ -24,6 +24,7 @@ export async function qualifyPromotion(
24
24
  requestedChannel: request.channel,
25
25
  dryRun: request["dry-run"],
26
26
  resume: Boolean(
27
+ request["resume-discussion-id"] ||
27
28
  request["resume-candidate-run-id"] ||
28
29
  request["publish-transaction-override"],
29
30
  ),
@@ -46,8 +47,10 @@ export async function qualifyPromotion(
46
47
  );
47
48
  const candidateVersion = qualified.candidateVersion || qualified.version,
48
49
  publicationVersion = qualified.publicationVersion || qualified.version;
49
- const recoveredVersion =
50
- request["publish-transaction-override"] || request["resume-transaction-id"]
50
+ const recoveredVersion = request["resume-discussion-id"]
51
+ ? publicationVersion
52
+ : request["publish-transaction-override"] ||
53
+ request["resume-transaction-id"]
51
54
  ? await recoverVersion(
52
55
  {
53
56
  requestedSha: intent["requested-sha"],
@@ -49,13 +49,17 @@ export function normalizePromotionRequest(value, kind = "request") {
49
49
  throw new Error(`Unsupported promotion ${kind} schema`);
50
50
  result[key] = input;
51
51
  }
52
+ if (
53
+ result["resume-discussion-id"] &&
54
+ (result["resume-candidate-run-id"] || result["resume-transaction-id"])
55
+ )
56
+ throw new Error(
57
+ "Discussion recovery cannot be combined with candidate-run or transaction-id recovery",
58
+ );
52
59
  return result;
53
60
  }
54
61
 
55
- export function bindPromotionInvocation(
56
- requestValue,
57
- selection,
58
- ) {
62
+ export function bindPromotionInvocation(requestValue, selection) {
59
63
  const request = normalizePromotionRequest(requestValue);
60
64
  const projected = Object.fromEntries(
61
65
  Object.entries(request).filter(([key]) =>
@@ -6,7 +6,10 @@ import {
6
6
  import { createReleaseInvocation } from "../../release/release-invocation.js";
7
7
  import { executeBootstrapConformance } from "./conformance.js";
8
8
  import { executeReleasePromotion } from "./release-promotion.js";
9
+ import { executeReleaseDiscussion } from "../../release/discussion/qualification.js";
9
10
  async function capabilityResult(request, admission, context) {
11
+ if (request.capability.id === "release-discussion")
12
+ return executeReleaseDiscussion(request, admission, context);
10
13
  if (request.capability.id === "bootstrap-conformance")
11
14
  return executeBootstrapConformance(request, admission, context);
12
15
  if (request.capability.id === "release-invocation") {
@@ -61,7 +64,8 @@ export async function executeAdmittedWorkflow(request, admission, context) {
61
64
 
62
65
  export function assertResultLineage(admission, result) {
63
66
  const { resultRoot, ...body } = result || {};
64
- if (resultRoot !== contentRoot("universal-workflow-result", body)) fail("candidate result root mismatch");
67
+ if (resultRoot !== contentRoot("universal-workflow-result", body))
68
+ fail("candidate result root mismatch");
65
69
  if (
66
70
  result?.schema !== "kungfu-buildchain-v4-universal-workflow-result/v1" ||
67
71
  result.requestRoot !== admission.requestRoot ||
@@ -7,6 +7,8 @@ export function providerExecutionContext({ token, mutationToken, env }) {
7
7
  apiUrl,
8
8
  actor: env.GITHUB_ACTOR,
9
9
  runId: env.GITHUB_RUN_ID,
10
+ runAttempt: env.GITHUB_RUN_ATTEMPT,
11
+ runtimeSelection: env.BUILDCHAIN_RUNTIME_SELECTION,
10
12
  octokit: token ? getOctokit(token, { baseUrl: apiUrl }) : undefined,
11
13
  mutationOctokit:
12
14
  mutationToken || token
@@ -29,6 +29,7 @@ export async function observeReleaseRoute(
29
29
  targetRef,
30
30
  dryRun: inputs["dry-run"] === true,
31
31
  resume:
32
+ Boolean(inputs["resume-discussion-id"]) ||
32
33
  String(inputs["resume-candidate-run-id"] || "") !== "" ||
33
34
  inputs["publish-transaction-override"] === true,
34
35
  });
@@ -6,9 +6,10 @@ import { planReleaseRoute } from "../../release/release-invocation.js";
6
6
  import { selectRecoveredProductPublicationVersion } from "../universal-workflow-bootstrap.js";
7
7
  import { assertDeclarativePromotionInputs } from "../../publication/publication-qualification.js";
8
8
  import { normalizePromotionRequest } from "../../release/promotion-request.js";
9
- import { resolveReleaseCandidateArtifacts } from "../../release/candidate/resolve.js";
9
+ import { qualifyPromotionCandidate } from "../../release/promotion/candidate.js";
10
10
  import { materializePublicationIntent } from "../../release/candidate/publication-intent.js";
11
- import { promoteReleaseCandidate } from "../../release/promote-candidate/transaction.js";
11
+ import { publishWithDiscussion } from "../../release/discussion/publication.js";
12
+ import { selectedRecordRuntime } from "../../release/discussion/session.js";
12
13
  import {
13
14
  observeReleaseRoute,
14
15
  observedSourceTimestamp,
@@ -34,7 +35,9 @@ async function materializeProductPublicationIntent(
34
35
  const version = String(
35
36
  candidate.publicationVersion || candidate.version || "",
36
37
  ).trim();
37
- const explicitResume = String(inputs["resume-transaction-id"] || "") !== "";
38
+ const explicitResume = Boolean(
39
+ inputs["resume-transaction-id"] || inputs["resume-discussion-id"],
40
+ );
38
41
  const recovery =
39
42
  route.decision !== "Resume" || explicitResume
40
43
  ? {}
@@ -45,14 +48,16 @@ async function materializeProductPublicationIntent(
45
48
  route.channel,
46
49
  context,
47
50
  );
48
- const recoveredVersion = selectRecoveredProductPublicationVersion({
49
- routeDecision: route.decision,
50
- candidateVersion: version,
51
- channel: route.channel,
52
- requestedSha: route.requestedSha,
53
- explicitResume,
54
- ...recovery,
55
- });
51
+ const recoveredVersion = inputs["resume-discussion-id"]
52
+ ? version
53
+ : selectRecoveredProductPublicationVersion({
54
+ routeDecision: route.decision,
55
+ candidateVersion: version,
56
+ channel: route.channel,
57
+ requestedSha: route.requestedSha,
58
+ explicitResume,
59
+ ...recovery,
60
+ });
56
61
  materializePublicationIntent({
57
62
  repository,
58
63
  channel: route.channel,
@@ -128,24 +133,21 @@ export async function executeReleasePromotion(request, admission, context) {
128
133
  prepareReleaseConsumerDependencies();
129
134
  if (payload.inputs["trusted-publishing"] === true)
130
135
  prepareTrustedPublishingNpm();
131
- const candidate = await resolveReleaseCandidateArtifacts({
136
+ const candidate = await qualifyPromotionCandidate({
137
+ request: payload.inputs,
138
+ intent: {
139
+ "target-ref": route.targetRef,
140
+ "requested-sha": route.requestedSha,
141
+ channel: route.channel,
142
+ },
132
143
  repository,
133
- targetRef: route.targetRef,
134
- targetSha: route.requestedSha,
135
144
  token: context.token,
136
145
  apiUrl: context.apiUrl,
137
- workflowFile: payload.inputs["release-candidate-workflow-file"],
138
- workflowName: payload.inputs["release-candidate-workflow-name"],
139
- artifactName: payload.inputs["artifact-name"],
140
- artifactPatterns: payload.inputs["artifact-patterns"],
141
- githubReleasePayloadPatterns:
142
- payload.inputs["github-release-payload-patterns"],
143
- requiredArtifactCount: payload.inputs["required-artifact-count"],
144
- publishArtifactKind: payload.inputs["publish-artifact-kind"],
145
- publishPackageMain: payload.inputs["publish-package-main"],
146
146
  runtimeSha: admission.runtime.sha,
147
- outputDir: ".buildchain/release-candidate",
148
- waitSeconds: payload.inputs["release-candidate-wait-seconds"],
147
+ runtimeRoot: context.runtimeRoot,
148
+ recoveryRunId: context.runId,
149
+ recoveryRunAttempt: context.runAttempt,
150
+ outputDir: path.resolve(".buildchain/release-candidate"),
149
151
  });
150
152
  if (!candidate.enabled)
151
153
  fail(candidate.reason || "release candidate is unavailable");
@@ -203,12 +205,22 @@ export async function executeReleasePromotion(request, admission, context) {
203
205
  "publish-transaction-override":
204
206
  payload.inputs["publish-transaction-override"],
205
207
  "resume-transaction-id": payload.inputs["resume-transaction-id"],
208
+ "resume-discussion-id": payload.inputs["resume-discussion-id"],
209
+ "standalone-binary-distribution":
210
+ payload.inputs["standalone-binary-distribution"],
206
211
  "artifact-paths": candidate.paths.releaseAssets,
207
212
  "state-path": ".buildchain/release-tail/state.json",
208
213
  "failure-after-capability":
209
214
  payload.inputs["provider-failure-after-capability"],
210
215
  };
211
- const provider = await promoteReleaseCandidate(publicationRequest, {
216
+ const provider = await publishWithDiscussion(publicationRequest, {
217
+ runtime: selectedRecordRuntime({
218
+ BUILDCHAIN_RUNTIME_SELECTION: context.runtimeSelection,
219
+ BUILDCHAIN_RUNTIME_ROOT: context.runtimeRoot,
220
+ }),
221
+ workspace: process.cwd(),
222
+ runtimeRoot: context.runtimeRoot,
223
+ attempt: `${context.runId}:${context.runAttempt || "1"}:bootstrap`,
212
224
  octokit: context.octokit,
213
225
  mutationOctokit: context.mutationOctokit,
214
226
  actor: context.actor,
@@ -22,6 +22,7 @@ const PERMISSIONS = new Set([
22
22
  "checks",
23
23
  "contents",
24
24
  "deployments",
25
+ "discussions",
25
26
  "id-token",
26
27
  "issues",
27
28
  "packages",
@@ -0,0 +1,34 @@
1
+ import { build } from "tsup";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ const check = process.argv.includes("--check");
6
+ const output = check
7
+ ? fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-reader-check-"))
8
+ : "dist/readers";
9
+ try {
10
+ await build({
11
+ entry: {
12
+ "release-discussion": "packages/core/release/discussion/reader-entry.js",
13
+ },
14
+ outDir: output,
15
+ format: ["cjs"],
16
+ target: "node24",
17
+ platform: "node",
18
+ splitting: false,
19
+ sourcemap: false,
20
+ minify: false,
21
+ clean: false,
22
+ outExtension: () => ({ js: ".cjs" }),
23
+ silent: true,
24
+ });
25
+ if (
26
+ check &&
27
+ !fs
28
+ .readFileSync(path.join(output, "release-discussion.cjs"))
29
+ .equals(fs.readFileSync("dist/readers/release-discussion.cjs"))
30
+ )
31
+ throw new Error("Historical release reader bundle is stale");
32
+ } finally {
33
+ if (check) fs.rmSync(output, { recursive: true, force: true });
34
+ }
@@ -32,5 +32,5 @@ export function assertBinaryInventory(root) {
32
32
  assert.equal(effect["continue-on-error"], undefined);
33
33
  const transaction = publish.modules.get("packages/core/publication/binary/transaction.js");
34
34
  assert.match(transaction, /validateBinaryCapability\([\s\S]*await client.publish\(/, "Binary effects must follow capability admission");
35
- assert.match(publish.modules.get("packages/core/publication/binary/action.js"), /verifyCheckoutIdentity\([\s\S]*await publishBinaryAssets\(/);
35
+ assert.match(publish.modules.get("packages/core/publication/binary/action.js"), /verifyCheckoutIdentity\([\s\S]*await observeBinaryDistribution\([\s\S]*publishBinaryAssets\(/);
36
36
  }
@@ -52,6 +52,7 @@ function isTestFile(file) {
52
52
  function isGeneratedFile(file) {
53
53
  return (
54
54
  GENERATED_PATTERN.test(file) ||
55
+ /^dist\/readers\/[^/]+\.cjs$/u.test(file) ||
55
56
  file.startsWith("templates/bootstrap-recovery/")
56
57
  );
57
58
  }
@@ -231,6 +231,7 @@ export function nodeApiMeta(exportName) {
231
231
  "./v4-tail-reseal": { group: "release-passport-trust", summary: "Effect-pure retained Stage Capsule validation and explicit Warrant-, credential-, and release-tail-fenced macOS reseal planning APIs." },
232
232
  "./tail-reseal": { group: "release-passport-trust", summary: "Effect-pure retained Stage Capsule validation and explicit Warrant-, credential-, and release-tail-fenced macOS reseal planning APIs." },
233
233
  "./v4-tail-reseal-receipt": { group: "release-passport-trust", summary: "Four-platform byte readback, standard candidate Passport binding, and terminal v4 tail-reseal receipt APIs." },
234
+ "./release-discussion-reader": { group: "release-passport-trust", summary: "Standalone historical decoder for append-only release Discussion intent, attempt lineage and recovery handoff." },
234
235
  "./tail-reseal-receipt": { group: "release-passport-trust", summary: "Four-platform byte readback, standard candidate Passport binding, and terminal v4 tail-reseal receipt APIs." },
235
236
  "./v4-publication-rehearsal": { group: "release-passport-trust", summary: "Content-addressed v4 publication rehearsal capsules, shared production release-tail execution, replay, provider observation, and fail-closed verification APIs." },
236
237
  "./publication-rehearsal": { group: "release-passport-trust", summary: "Content-addressed v4 publication rehearsal capsules, shared production release-tail execution, replay, provider observation, and fail-closed verification APIs." },