@kungfu-tech/buildchain 4.0.1-alpha.8 → 4.0.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 (98) hide show
  1. package/README.md +9 -6
  2. package/actions/v4-release-candidate-promote/README.md +3 -0
  3. package/architecture/agent-change-map.md +1015 -0
  4. package/architecture/ci-lane-change-budget.json +70 -1
  5. package/architecture/decisions/0004-v4-declarative-release-tail.md +47 -0
  6. package/architecture/internal-capabilities.json +156 -11
  7. package/architecture/maintainability-debt.json +3229 -0
  8. package/architecture/maintainability-policy.json +480 -129
  9. package/architecture/v3-core-mechanism-inventory.json +4 -3
  10. package/architecture/v3-v4-capability-closure.md +59 -0
  11. package/architecture/v3-v4-live-capability-inventory.json +102404 -0
  12. package/architecture/v4-cross-platform-adopter-qualification.json +52 -0
  13. package/architecture/v4-delivery-warrant-shadow-fixtures.json +3 -3
  14. package/architecture/v4-runtime-semantic-closure.json +710 -0
  15. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  16. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  17. package/contracts/v4-publication-qualification-v1.schema.json +51 -0
  18. package/dist/site/agent-index.json +1 -0
  19. package/dist/site/artifact-schemas.json +2 -0
  20. package/dist/site/buildchain-contract.json +123 -32
  21. package/dist/site/buildchain-site.json +96 -28
  22. package/dist/site/capability-registry.json +6 -6
  23. package/dist/site/cli-registry.json +5 -2
  24. package/dist/site/controller-registry.json +41 -5
  25. package/dist/site/kfd-claims.json +170 -21
  26. package/dist/site/kfd-upstream-aggregate.json +1 -1
  27. package/dist/site/manual-registry.json +6 -6
  28. package/dist/site/node-api-registry.json +1474 -215
  29. package/dist/site/page-registry.json +85 -17
  30. package/dist/site/public-surface-audit.json +85 -20
  31. package/dist/site/publication-authority-registry.json +28 -1
  32. package/dist/site/publication-registry.json +4 -4
  33. package/dist/site/release-provenance.json +5 -0
  34. package/dist/site/schemas/v4-publication-qualification-v1.schema.json +51 -0
  35. package/dist/site/site-manifest.json +10 -10
  36. package/dist/site/workflow-registry.json +94 -18
  37. package/docs/MAP.md +1 -1
  38. package/docs/cli-reference.md +4 -4
  39. package/docs/dev-delivery-warrant.md +12 -9
  40. package/docs/node-api-reference.md +291 -206
  41. package/docs/publish-transaction.md +11 -4
  42. package/docs/release-candidate.md +7 -0
  43. package/docs/v4-adopter-delivery.md +23 -4
  44. package/docs/v4-declarative-release-tail-migration.md +43 -0
  45. package/package.json +8 -3
  46. package/packages/core/adopter-delivery-passport.js +36 -0
  47. package/packages/core/buildchain-contract.js +31 -1
  48. package/packages/core/buildchain-publication-authority.js +1 -0
  49. package/packages/core/dev-delivery-warrant-settlement.js +239 -3
  50. package/packages/core/dev-delivery-warrant-state.js +15 -2
  51. package/packages/core/dev-delivery-warrant.js +21 -4
  52. package/packages/core/github-governance-authority.js +4 -4
  53. package/packages/core/index.js +1 -0
  54. package/packages/core/paper-scaffold-content.js +401 -0
  55. package/packages/core/paper.js +12 -378
  56. package/packages/core/publication-control-plane-audit.js +135 -111
  57. package/packages/core/publication-rehearsal-projection.js +104 -0
  58. package/packages/core/publication-rehearsal-runtime.js +190 -0
  59. package/packages/core/release-candidate-recovery.js +6 -5
  60. package/packages/core/release-passport.js +82 -184
  61. package/packages/core/v4-cross-platform-adopter-qualification.js +343 -0
  62. package/packages/core/v4-floating-consumer-policy.js +3 -2
  63. package/packages/core/v4-floating-consumer-release-passport.js +134 -15
  64. package/packages/core/v4-publication-qualification.js +222 -0
  65. package/scripts/artifact-signing-controller-core.mjs +1 -1
  66. package/scripts/audit-publication-control-plane.mjs +17 -17
  67. package/scripts/buildchain-cli-help.mjs +3 -0
  68. package/scripts/check-internal-architecture.mjs +23 -3
  69. package/scripts/check-inventory.mjs +23 -5
  70. package/scripts/check-maintainability.mjs +145 -4
  71. package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
  72. package/scripts/check-v3-v4-capability-inventory.mjs +574 -0
  73. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -3
  74. package/scripts/check-v4-runtime-semantic-closure.mjs +266 -0
  75. package/scripts/dev-delivery-warrant-options.mjs +27 -11
  76. package/scripts/dev-delivery-warrant.mjs +45 -5
  77. package/scripts/dev-pr-auto-merge.mjs +25 -11
  78. package/scripts/dev-pr-delivery-warrant.mjs +44 -0
  79. package/scripts/dev-pr-prequeue-guard.mjs +0 -18
  80. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  81. package/scripts/generate-agent-change-map.mjs +118 -0
  82. package/scripts/generate-release-candidate-passport.mjs +26 -1
  83. package/scripts/generate-site-bundle.mjs +6 -0
  84. package/scripts/maintainability-domain-metrics.mjs +237 -0
  85. package/scripts/maintainability-governance.mjs +473 -0
  86. package/scripts/maintainability-metrics.mjs +28 -6
  87. package/scripts/project-cut-merge-queue-admission.mjs +394 -0
  88. package/scripts/promotion-routing-evidence.mjs +35 -6
  89. package/scripts/release-candidate-resolver.mjs +44 -0
  90. package/scripts/release-line-policy.mjs +25 -15
  91. package/scripts/release-tail.mjs +19 -2
  92. package/scripts/resume-from-candidate-run.mjs +106 -103
  93. package/scripts/run-lifecycle-core.mjs +341 -376
  94. package/scripts/site-capability-metadata.mjs +4 -0
  95. package/scripts/stable-candidate-qualification.mjs +9 -10
  96. package/scripts/v3-v4-capability-catalog.mjs +439 -0
  97. package/scripts/v4-cross-platform-adopter-qualification.mjs +228 -0
  98. package/scripts/v4-declarative-promotion-admission.mjs +42 -0
@@ -72,77 +72,72 @@ function fact(id, pass, observed) {
72
72
  };
73
73
  }
74
74
 
75
- export function evaluatePublicationControlPlaneSnapshot({
75
+ function evaluatePublisherPolicy({
76
+ publisher,
77
+ publisherMode,
78
+ packageName,
76
79
  repository,
77
80
  workflowPath,
78
- publisherWorkflowPath = workflowPath,
81
+ publisherWorkflowPath,
82
+ workflowFilename,
79
83
  environment,
80
- branch,
81
- packageName,
82
- publisherMode = "npm-trusted-publisher",
83
- requiredStatusCheck = "check",
84
- snapshot,
85
- observedAt,
86
- expiresAt,
87
- } = {}) {
88
- const workflowFilename = String(publisherWorkflowPath || "").split("/").pop();
89
- const providerEnvironment = environment === "none" ? "" : environment;
90
- const actions = snapshot?.actions || {};
91
- const branchPolicy = snapshot?.branch || {};
92
- const environmentPolicy = snapshot?.environment || {};
93
- const oidc = snapshot?.oidc || {};
94
- const publisher = snapshot?.publisher || {};
95
- const runner = snapshot?.runner || {};
96
- const npmIdentityPass = publisher.packageName === packageName &&
97
- publisher.provider === "github" &&
98
- publisher.repository === repository &&
99
- publisher.workflowFilename === workflowFilename &&
100
- publisher.environment === providerEnvironment &&
101
- publisher.longLivedWorkflowCredentialPresent === false;
102
- const publisherPass = publisherMode === "npm-trusted-publisher"
103
- ? npmIdentityPass && (
84
+ providerEnvironment,
85
+ }) {
86
+ if (publisherMode === "npm-trusted-publisher") {
87
+ const identityPass = publisher.packageName === packageName &&
88
+ publisher.provider === "github" &&
89
+ publisher.repository === repository &&
90
+ publisher.workflowFilename === workflowFilename &&
91
+ publisher.environment === providerEnvironment &&
92
+ publisher.longLivedWorkflowCredentialPresent === false;
93
+ return identityPass && (
104
94
  (publisher.enforcement === "audited-control-plane" && publisher.allowPublish === true) ||
105
95
  (publisher.enforcement === "provider-at-transaction" &&
106
96
  publisher.authorizationDeferred === true &&
107
97
  publisher.configurationRead === false)
108
- )
109
- : publisherMode === "github-token"
110
- ? publisher.provider === "github-token" &&
111
- publisher.repository === repository &&
112
- publisher.workflowPath === workflowPath &&
113
- publisher.permissionScoped === true &&
114
- publisher.longLivedWorkflowCredentialPresent === false
115
- : publisherMode === "oidc-role"
116
- ? publisher.provider !== "" &&
117
- publisher.repository === repository &&
118
- publisher.workflowPath === publisherWorkflowPath &&
119
- publisher.environment === environment &&
120
- publisher.trustQualifying === true &&
121
- /^[0-9a-f]{64}$/i.test(String(publisher.roleDigest || "").replace(/^sha256:/, "")) &&
122
- publisher.longLivedWorkflowCredentialPresent === false
123
- : false;
124
- const credentialIsolationPass = publisherMode === "github-token"
98
+ );
99
+ }
100
+ if (publisherMode === "github-token") {
101
+ return publisher.provider === "github-token" &&
102
+ publisher.repository === repository &&
103
+ publisher.workflowPath === workflowPath &&
104
+ publisher.permissionScoped === true &&
105
+ publisher.longLivedWorkflowCredentialPresent === false;
106
+ }
107
+ if (publisherMode !== "oidc-role") return false;
108
+ return publisher.provider !== "" &&
109
+ publisher.repository === repository &&
110
+ publisher.workflowPath === publisherWorkflowPath &&
111
+ publisher.environment === environment &&
112
+ publisher.trustQualifying === true &&
113
+ /^[0-9a-f]{64}$/i.test(String(publisher.roleDigest || "").replace(/^sha256:/, "")) &&
114
+ publisher.longLivedWorkflowCredentialPresent === false;
115
+ }
116
+
117
+ function evaluateCredentialIsolation({ oidc, publisherMode, publisherWorkflowPath, providerEnvironment }) {
118
+ return publisherMode === "github-token"
125
119
  ? oidc.githubTokenJobScoped === true && oidc.longLivedCredentialPresent === false
126
120
  : oidc.workflowPath === publisherWorkflowPath &&
127
121
  oidc.environment === providerEnvironment &&
128
122
  oidc.idTokenJobScoped === true &&
129
123
  oidc.longLivedCredentialPresent === false;
130
- const configuredBranchPolicyPass = branchPolicy.ref === branch &&
131
- branchPolicy.strict === true &&
132
- Number(branchPolicy.requiredApprovals || 0) >= 1 &&
133
- branchPolicy.requireConversationResolution === true &&
134
- branchPolicy.enforceAdmins === true;
135
- const declaredRequiredStatusCheck = branchPolicy.declaredRequiredStatusCheck || branchPolicy.requiredStatusCheck;
136
- const resolvedRequiredStatusCheck = branchPolicy.requiredStatusCheck;
137
- const requiredStatusCheckBindingPass = declaredRequiredStatusCheck === requiredStatusCheck && (
138
- resolvedRequiredStatusCheck === declaredRequiredStatusCheck ||
139
- (
140
- resolvedRequiredStatusCheck.startsWith(`${declaredRequiredStatusCheck} / `) &&
141
- branchPolicy.requiredStatusCheckMatchCount === 1
142
- )
143
- );
124
+ }
125
+
126
+ function requiredStatusCheckBinding(branchPolicy, requiredStatusCheck) {
127
+ const declared = branchPolicy.declaredRequiredStatusCheck || branchPolicy.requiredStatusCheck;
128
+ const resolved = branchPolicy.requiredStatusCheck;
129
+ return {
130
+ resolved,
131
+ pass: declared === requiredStatusCheck && (
132
+ resolved === declared ||
133
+ (resolved.startsWith(`${declared} / `) && branchPolicy.requiredStatusCheckMatchCount === 1)
134
+ ),
135
+ };
136
+ }
137
+
138
+ function sourceAuthorizationPass(branchPolicy) {
144
139
  const authorizationSha = branchPolicy.authorizationSha || branchPolicy.sourceSha;
145
- const sourceAuthorizationPass = branchPolicy.sourceSha === authorizationSha || (
140
+ return branchPolicy.sourceSha === authorizationSha || (
146
141
  branchPolicy.releaseReconciliation?.qualifying === true &&
147
142
  branchPolicy.releaseReconciliation.parentSha === authorizationSha &&
148
143
  /^\d+\.\d+\.\d+$/.test(String(branchPolicy.releaseReconciliation.version || "")) &&
@@ -150,74 +145,103 @@ export function evaluatePublicationControlPlaneSnapshot({
150
145
  Array.isArray(branchPolicy.releaseReconciliation.changedPaths) &&
151
146
  branchPolicy.releaseReconciliation.changedPaths.length > 0
152
147
  );
153
- const sourceBranchBindingPass = branchPolicy.sourceSha === branchPolicy.headSha ||
154
- branchPolicy.sourceContainedInBranch === true;
155
- const providerTransactionBranchPass = branchPolicy.ref === branch &&
148
+ }
149
+
150
+ function evaluateConfiguredBranchPolicy(branchPolicy, branch) {
151
+ return branchPolicy.ref === branch &&
152
+ branchPolicy.strict === true &&
153
+ Number(branchPolicy.requiredApprovals || 0) >= 1 &&
154
+ branchPolicy.requireConversationResolution === true &&
155
+ branchPolicy.enforceAdmins === true;
156
+ }
157
+
158
+ function evaluateProviderBranchPolicy(branchPolicy, branch, requiredCheck) {
159
+ return branchPolicy.ref === branch &&
156
160
  branchPolicy.policyMode === "provider-enforced-transaction" &&
157
161
  branchPolicy.protected === true &&
158
162
  branchPolicy.enforcementLevel === "everyone" &&
159
163
  Array.isArray(branchPolicy.requiredStatusChecks) &&
160
- requiredStatusCheckBindingPass &&
161
- branchPolicy.requiredStatusChecks.includes(resolvedRequiredStatusCheck) &&
164
+ requiredCheck.pass &&
165
+ branchPolicy.requiredStatusChecks.includes(requiredCheck.resolved) &&
162
166
  branchPolicy.requiredCheckPassed === true &&
163
167
  /^[0-9a-f]{40}$/i.test(String(branchPolicy.requiredCheckSha || "")) &&
164
- branchPolicy.requiredCheckSha === branchPolicy.pullRequestHeadSha &&
165
- sourceBranchBindingPass &&
166
- sourceAuthorizationPass &&
168
+ branchPolicy.requiredCheckSha === branchPolicy.pullRequestHeadSha;
169
+ }
170
+
171
+ function evaluateProviderPullRequest(branchPolicy, branch, repository) {
172
+ return (branchPolicy.sourceSha === branchPolicy.headSha || branchPolicy.sourceContainedInBranch === true) &&
173
+ sourceAuthorizationPass(branchPolicy) &&
167
174
  branchPolicy.mergedPullRequest === true &&
168
175
  branchPolicy.baseRef === branch &&
169
176
  branchPolicy.headRepository === repository &&
170
177
  Number(branchPolicy.approvalCount || 0) >= 1 &&
171
178
  branchPolicy.independentApproval === true;
172
- const facts = [
173
- fact(
174
- "actions-policy",
175
- actions.defaultWorkflowPermissions === "read" && actions.canApprovePullRequestReviews === false,
176
- actions,
177
- ),
178
- fact(
179
- "branch-policy",
180
- configuredBranchPolicyPass || providerTransactionBranchPass,
181
- branchPolicy,
182
- ),
183
- fact(
184
- "environment-policy",
185
- environment === "none"
186
- ? environmentPolicy.declared === false && environmentPolicy.exists === false
187
- : environmentPolicy.name === environment &&
188
- environmentPolicy.declared === true &&
189
- environmentPolicy.exists === true &&
190
- environmentPolicy.protected === true &&
191
- environmentPolicy.branchAuthorized === true &&
192
- (environmentPolicy.reviewRequired !== true || environmentPolicy.preventSelfReview === true),
193
- environmentPolicy,
194
- ),
195
- fact(
196
- "oidc-policy",
197
- credentialIsolationPass,
198
- oidc,
199
- ),
200
- fact(
201
- "publisher-policy",
202
- publisherPass,
203
- publisher,
204
- ),
205
- fact(
206
- "runner-policy",
207
- runner.class === "ephemeral" &&
208
- runner.label === "ubuntu-24.04" &&
209
- runner.githubHosted === true &&
210
- runner.selfHostedAuthorized === false,
211
- runner,
212
- ),
213
- ];
214
- return createPublicationControlPlaneAudit({
179
+ }
180
+
181
+ function evaluateBranchPolicy({ branchPolicy, branch, repository, requiredStatusCheck }) {
182
+ const requiredCheck = requiredStatusCheckBinding(branchPolicy, requiredStatusCheck);
183
+ return evaluateConfiguredBranchPolicy(branchPolicy, branch) || (
184
+ evaluateProviderBranchPolicy(branchPolicy, branch, requiredCheck) &&
185
+ evaluateProviderPullRequest(branchPolicy, branch, repository)
186
+ );
187
+ }
188
+
189
+ function evaluateEnvironmentPolicy(environmentPolicy, environment) {
190
+ return environment === "none"
191
+ ? environmentPolicy.declared === false && environmentPolicy.exists === false
192
+ : environmentPolicy.name === environment &&
193
+ environmentPolicy.declared === true &&
194
+ environmentPolicy.exists === true &&
195
+ environmentPolicy.protected === true &&
196
+ environmentPolicy.branchAuthorized === true &&
197
+ (environmentPolicy.reviewRequired !== true || environmentPolicy.preventSelfReview === true);
198
+ }
199
+
200
+ function evaluateRunnerPolicy(runner) {
201
+ return runner.class === "ephemeral" &&
202
+ runner.label === "ubuntu-24.04" &&
203
+ runner.githubHosted === true &&
204
+ runner.selfHostedAuthorized === false;
205
+ }
206
+
207
+ function evaluatePublicationControlPlaneSnapshotOptions(options) {
208
+ const {
215
209
  repository,
216
210
  workflowPath,
217
- publisherWorkflowPath,
211
+ publisherWorkflowPath = workflowPath,
218
212
  environment,
219
- facts,
213
+ branch,
214
+ packageName,
215
+ publisherMode = "npm-trusted-publisher",
216
+ requiredStatusCheck = "check",
217
+ snapshot,
220
218
  observedAt,
221
219
  expiresAt,
220
+ } = options;
221
+ const workflowFilename = String(publisherWorkflowPath || "").split("/").pop();
222
+ const providerEnvironment = environment === "none" ? "" : environment;
223
+ const actions = snapshot?.actions || {};
224
+ const branchPolicy = snapshot?.branch || {};
225
+ const environmentPolicy = snapshot?.environment || {};
226
+ const oidc = snapshot?.oidc || {};
227
+ const publisher = snapshot?.publisher || {};
228
+ const runner = snapshot?.runner || {};
229
+ const facts = [
230
+ fact("actions-policy", actions.defaultWorkflowPermissions === "read" && actions.canApprovePullRequestReviews === false, actions),
231
+ fact("branch-policy", evaluateBranchPolicy({ branchPolicy, branch, repository, requiredStatusCheck }), branchPolicy),
232
+ fact("environment-policy", evaluateEnvironmentPolicy(environmentPolicy, environment), environmentPolicy),
233
+ fact("oidc-policy", evaluateCredentialIsolation({ oidc, publisherMode, publisherWorkflowPath, providerEnvironment }), oidc),
234
+ fact("publisher-policy", evaluatePublisherPolicy({
235
+ publisher, publisherMode, packageName, repository, workflowPath,
236
+ publisherWorkflowPath, workflowFilename, environment, providerEnvironment,
237
+ }), publisher),
238
+ fact("runner-policy", evaluateRunnerPolicy(runner), runner),
239
+ ];
240
+ return createPublicationControlPlaneAudit({
241
+ repository, workflowPath, publisherWorkflowPath, environment, facts, observedAt, expiresAt,
222
242
  });
223
243
  }
244
+
245
+ export function evaluatePublicationControlPlaneSnapshot({ repository, workflowPath, publisherWorkflowPath = workflowPath, environment, branch, packageName, publisherMode = "npm-trusted-publisher", requiredStatusCheck = "check", snapshot, observedAt, expiresAt, } = {}) {
246
+ return evaluatePublicationControlPlaneSnapshotOptions({ repository, workflowPath, publisherWorkflowPath, environment, branch, packageName, publisherMode, requiredStatusCheck, snapshot, observedAt, expiresAt });
247
+ }
@@ -0,0 +1,104 @@
1
+ import {
2
+ PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
3
+ PUBLICATION_REHEARSAL_COMMAND,
4
+ RELEASE_LOCAL_CONSTRUCTIBILITY_ADR,
5
+ RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT,
6
+ } from "./publication-rehearsal-runtime.js";
7
+
8
+ export const PUBLICATION_REHEARSAL_AGENT_SECTION_START =
9
+ "<!-- buildchain:publication-rehearsal:v4:start -->";
10
+ export const PUBLICATION_REHEARSAL_AGENT_SECTION_END =
11
+ "<!-- buildchain:publication-rehearsal:v4:end -->";
12
+ export const PUBLICATION_REHEARSAL_WORKFLOW_PATH =
13
+ ".github/workflows/publication-rehearsal.yml";
14
+
15
+ export function publicationRehearsalToml() {
16
+ return `[publication_rehearsal]\ncontract = ${JSON.stringify(PUBLICATION_REHEARSAL_CAPSULE_CONTRACT)}\nadr = ${JSON.stringify(RELEASE_LOCAL_CONSTRUCTIBILITY_ADR)}\ninvariant = ${JSON.stringify(RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT)}\ncommand = ${JSON.stringify(PUBLICATION_REHEARSAL_COMMAND)}\ncapsule = ".buildchain/publication-rehearsal/capsule.json"\ncandidate_root = ".buildchain/publication-rehearsal/candidate"\nstate = ".buildchain/publication-rehearsal/state.json"\nevidence = ".buildchain/publication-rehearsal/evidence.json"\n`;
17
+ }
18
+
19
+ export function assertPublicationRehearsalConfig(config) {
20
+ const section = config?.publication_rehearsal;
21
+ if (!section || typeof section !== "object" || Array.isArray(section))
22
+ throw new Error("publication_rehearsal table is missing");
23
+ const expected = {
24
+ contract: PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
25
+ adr: RELEASE_LOCAL_CONSTRUCTIBILITY_ADR,
26
+ invariant: RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT,
27
+ command: PUBLICATION_REHEARSAL_COMMAND,
28
+ capsule: ".buildchain/publication-rehearsal/capsule.json",
29
+ candidate_root: ".buildchain/publication-rehearsal/candidate",
30
+ state: ".buildchain/publication-rehearsal/state.json",
31
+ evidence: ".buildchain/publication-rehearsal/evidence.json",
32
+ };
33
+ if (
34
+ JSON.stringify(Object.keys(section).sort()) !==
35
+ JSON.stringify(Object.keys(expected).sort())
36
+ )
37
+ throw new Error(
38
+ `publication_rehearsal fields must be exactly: ${Object.keys(expected).sort().join(", ")}`,
39
+ );
40
+ for (const [key, value] of Object.entries(expected))
41
+ if (section[key] !== value)
42
+ throw new Error(`publication_rehearsal.${key} is stale or unsupported`);
43
+ return structuredClone(section);
44
+ }
45
+
46
+ export function appendPublicationRehearsalToml(source) {
47
+ const current = String(source || "").trimEnd();
48
+ if (/^\[publication_rehearsal\]$/mu.test(current))
49
+ throw new Error(
50
+ "buildchain config already contains a publication_rehearsal table",
51
+ );
52
+ return `${current}\n\n${publicationRehearsalToml()}`;
53
+ }
54
+
55
+ export function projectPublicationRehearsalToml(source) {
56
+ const current = String(source || "");
57
+ const start = current.search(/^\[publication_rehearsal\]\s*$/mu);
58
+ if (start === -1) return appendPublicationRehearsalToml(current);
59
+ const remainder = current.slice(start);
60
+ const newline = remainder.indexOf("\n");
61
+ const nextTable = remainder.slice(newline + 1).search(/^\[[^\n]+\]\s*$/mu);
62
+ const end =
63
+ nextTable === -1 ? current.length : start + newline + 1 + nextTable;
64
+ const prefix = current.slice(0, start).trimEnd();
65
+ const suffix = current.slice(end).trimStart();
66
+ return `${prefix}\n\n${publicationRehearsalToml()}${suffix ? `\n${suffix}` : ""}`;
67
+ }
68
+
69
+ export function publicationRehearsalAgentInstructions() {
70
+ return `${PUBLICATION_REHEARSAL_AGENT_SECTION_START}\n## Publication rehearsal (required)\n\nBuildchain v4 rehearsal follows \`${RELEASE_LOCAL_CONSTRUCTIBILITY_ADR}\`.\n${RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT}\n\nRun the source-bound capsule with:\n\n\`\`\`sh\n${PUBLICATION_REHEARSAL_COMMAND}\n\`\`\`\n\nSimulation and replay evidence never claim production publication authority.\n${PUBLICATION_REHEARSAL_AGENT_SECTION_END}`;
71
+ }
72
+
73
+ export function mergePublicationRehearsalAgentInstructions(current = "") {
74
+ const source = String(current || "");
75
+ const section = publicationRehearsalAgentInstructions();
76
+ const start = source.indexOf(PUBLICATION_REHEARSAL_AGENT_SECTION_START);
77
+ const end = source.indexOf(PUBLICATION_REHEARSAL_AGENT_SECTION_END);
78
+ if ((start === -1) !== (end === -1) || end < start)
79
+ throw new Error(
80
+ "AGENTS.md has an incomplete Buildchain publication rehearsal section",
81
+ );
82
+ if (start === -1)
83
+ return source.trim()
84
+ ? `${source.trimEnd()}\n\n${section}\n`
85
+ : `# AGENTS.md\n\n${section}\n`;
86
+ if (
87
+ source.indexOf(PUBLICATION_REHEARSAL_AGENT_SECTION_START, start + 1) !==
88
+ -1 ||
89
+ source.indexOf(PUBLICATION_REHEARSAL_AGENT_SECTION_END, end + 1) !== -1
90
+ )
91
+ throw new Error(
92
+ "AGENTS.md has ambiguous Buildchain publication rehearsal sections",
93
+ );
94
+ return `${source.slice(0, start)}${section}${source.slice(
95
+ end + PUBLICATION_REHEARSAL_AGENT_SECTION_END.length,
96
+ )}`;
97
+ }
98
+
99
+ export function publicationRehearsalWorkflow(buildchainRef) {
100
+ const ref = String(buildchainRef || "").trim();
101
+ if (!ref)
102
+ throw new Error("publication rehearsal workflow requires a Buildchain ref");
103
+ return `name: Publication Rehearsal\n\non:\n workflow_dispatch:\n inputs:\n capsule-path:\n description: "Source-bound v4 publication rehearsal capsule"\n required: true\n default: ".buildchain/publication-rehearsal/capsule.json"\n candidate-root:\n description: "Repository-relative candidate root"\n required: true\n default: ".buildchain/publication-rehearsal/candidate"\n\npermissions:\n contents: read\n\njobs:\n rehearsal:\n uses: kungfu-systems/buildchain/.github/workflows/release-tail.yml@${ref}\n with:\n buildchain-ref: ${ref}\n rehearsal-capsule-path: \${{ inputs.capsule-path }}\n candidate-root: \${{ inputs.candidate-root }}\n rehearsal-mode: simulate\n state-path: ".buildchain/publication-rehearsal/state.json"\n rehearsal-evidence-path: ".buildchain/publication-rehearsal/evidence.json"\n`;
104
+ }
@@ -0,0 +1,190 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { releaseTailRoot } from "./release-tail-provider-plane.js";
5
+ import {
6
+ V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
7
+ V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
8
+ createV4PublicationRehearsalCapsule,
9
+ executeV4PublicationRehearsal,
10
+ validateV4PublicationRehearsalCapsule,
11
+ verifyV4PublicationRehearsalCapsule,
12
+ } from "./v4-publication-rehearsal.js";
13
+
14
+ export const PUBLICATION_REHEARSAL_CAPSULE_CONTRACT =
15
+ V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT;
16
+ export const PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT =
17
+ V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT;
18
+ export const PUBLICATION_REHEARSAL_DIAGNOSTIC_CONTRACT =
19
+ "buildchain-v4-publication-rehearsal-compatibility-diagnostic/v1";
20
+ export const RELEASE_LOCAL_CONSTRUCTIBILITY_ADR =
21
+ "docs/v4-publication-rehearsal.md";
22
+ export const RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT =
23
+ "Publication rehearsal is source-, candidate-, manifest-, config-, policy-, observation-, and shared-core-bound; rehearsal authority is effect-disabled by default and never grants production authority.";
24
+ export const PUBLICATION_REHEARSAL_COMMAND =
25
+ 'buildchain release-tail rehearse --capsule "$PWD/.buildchain/publication-rehearsal/capsule.json" --candidate-root "$PWD/.buildchain/publication-rehearsal/candidate" --mode simulate --state "$PWD/.buildchain/publication-rehearsal/state.json" --evidence "$PWD/.buildchain/publication-rehearsal/evidence.json"';
26
+
27
+ const REQUIRED_V4_INPUTS = Object.freeze([
28
+ "source.repository",
29
+ "source.revision",
30
+ "manifest.path",
31
+ "manifest.root",
32
+ "config.path",
33
+ "config.root",
34
+ ]);
35
+
36
+ function migrationReceipt(surface, reasonCode) {
37
+ const body = {
38
+ schema: "buildchain-v3-v4-public-surface-migration/v1",
39
+ surface,
40
+ status: "migration-required",
41
+ reasonCode,
42
+ replacement: {
43
+ module: "@kungfu-tech/buildchain/v4-publication-rehearsal",
44
+ capsuleContract: V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
45
+ requiredInputs: REQUIRED_V4_INPUTS,
46
+ },
47
+ productionAuthority: false,
48
+ };
49
+ return { ...body, migrationRoot: releaseTailRoot(body) };
50
+ }
51
+
52
+ export class PublicationRehearsalError extends Error {
53
+ constructor(
54
+ message,
55
+ {
56
+ code = "publication-rehearsal-v4-migration-required",
57
+ classification = "migration",
58
+ migration = null,
59
+ cause,
60
+ } = {},
61
+ ) {
62
+ super(message, cause ? { cause } : undefined);
63
+ this.name = "PublicationRehearsalError";
64
+ this.rehearsalCode = code;
65
+ this.rehearsalClass = classification;
66
+ this.migration = migration;
67
+ this.bindingRoot = "";
68
+ }
69
+ }
70
+
71
+ function hasV4CreationBindings(input) {
72
+ return Boolean(
73
+ input?.source?.repository &&
74
+ input?.source?.revision &&
75
+ input?.manifest?.path &&
76
+ input?.manifest?.root &&
77
+ input?.config?.path &&
78
+ input?.config?.root,
79
+ );
80
+ }
81
+
82
+ function requireV4CreationBindings(input, surface) {
83
+ if (hasV4CreationBindings(input)) return;
84
+ const migration = migrationReceipt(surface, "v4-source-binding-required");
85
+ throw new PublicationRehearsalError(
86
+ "v3 rehearsal capsules cannot be upgraded implicitly; provide the v4 source, manifest, and config bindings",
87
+ { migration },
88
+ );
89
+ }
90
+
91
+ export function createPublicationRehearsalCapsule(input = {}) {
92
+ requireV4CreationBindings(
93
+ input,
94
+ "@kungfu-tech/buildchain/publication-rehearsal-runtime#createPublicationRehearsalCapsule",
95
+ );
96
+ return createV4PublicationRehearsalCapsule(input);
97
+ }
98
+
99
+ export function normalizePublicationRehearsalCapsule(input) {
100
+ return validateV4PublicationRehearsalCapsule(input);
101
+ }
102
+
103
+ export function verifyPublicationRehearsalCapsule({
104
+ capsule,
105
+ capsuleRoot,
106
+ candidateRoot = capsuleRoot,
107
+ } = {}) {
108
+ return verifyV4PublicationRehearsalCapsule({ capsule, candidateRoot });
109
+ }
110
+
111
+ export function publicationRehearsalBindingRoot(capsule) {
112
+ return validateV4PublicationRehearsalCapsule(capsule).capsuleRoot;
113
+ }
114
+
115
+ export function publicationRehearsalDiagnostic(error, { capsule } = {}) {
116
+ const migration = error?.migration || null;
117
+ const body = {
118
+ contract: PUBLICATION_REHEARSAL_DIAGNOSTIC_CONTRACT,
119
+ status: "rejected",
120
+ errorClass: error?.rehearsalClass || (migration ? "migration" : "input"),
121
+ code:
122
+ error?.rehearsalCode ||
123
+ error?.code ||
124
+ "publication-rehearsal-runtime-error",
125
+ message: String(error?.message || "publication rehearsal failed"),
126
+ bindingRoot: capsule?.capsuleRoot || "",
127
+ migration,
128
+ productionAuthority: false,
129
+ };
130
+ return { ...body, diagnosticRoot: releaseTailRoot(body) };
131
+ }
132
+
133
+ export async function executePublicationRehearsal({
134
+ capsule,
135
+ capsuleRoot,
136
+ candidateRoot = capsuleRoot,
137
+ mode = "simulate",
138
+ environment = {},
139
+ adapters = {},
140
+ authority = null,
141
+ transaction = null,
142
+ checkpoint,
143
+ } = {}) {
144
+ if (
145
+ !environment ||
146
+ typeof environment !== "object" ||
147
+ Array.isArray(environment) ||
148
+ Object.keys(environment).length > 0
149
+ ) {
150
+ const migration = migrationReceipt(
151
+ "@kungfu-tech/buildchain/publication-rehearsal-runtime#executePublicationRehearsal",
152
+ "ambient-environment-forbidden",
153
+ );
154
+ throw new PublicationRehearsalError(
155
+ "v4 rehearsal accepts no ambient environment; bind every semantic input into the capsule",
156
+ { code: "undeclared-environment", classification: "input", migration },
157
+ );
158
+ }
159
+ return executeV4PublicationRehearsal({
160
+ capsule,
161
+ candidateRoot,
162
+ mode,
163
+ adapters,
164
+ authority,
165
+ transaction,
166
+ checkpoint,
167
+ });
168
+ }
169
+
170
+ export function resolvePublicationRehearsalFile(capsuleRoot, relativePath) {
171
+ if (!path.isAbsolute(capsuleRoot))
172
+ throw new PublicationRehearsalError(
173
+ "capsuleRoot must be an explicit absolute path",
174
+ { code: "implicit-workspace-forbidden", classification: "input" },
175
+ );
176
+ const base = fs.realpathSync(capsuleRoot);
177
+ const resolved = path.resolve(base, String(relativePath || ""));
178
+ if (
179
+ !resolved.startsWith(`${base}${path.sep}`) ||
180
+ !fs.existsSync(resolved) ||
181
+ !fs.statSync(resolved).isFile() ||
182
+ fs.lstatSync(resolved).isSymbolicLink() ||
183
+ !fs.realpathSync(resolved).startsWith(`${base}${path.sep}`)
184
+ )
185
+ throw new PublicationRehearsalError(
186
+ "publication rehearsal file must be a regular file inside the explicit candidate root",
187
+ { code: "capsule-path-ambiguous", classification: "input" },
188
+ );
189
+ return resolved;
190
+ }
@@ -47,6 +47,7 @@ const TRUSTED_ASSOCIATIONS = new Set(["COLLABORATOR", "MEMBER", "OWNER"]);
47
47
  const RESUMABLE_TRANSACTION_STATES = new Set([
48
48
  "sealed",
49
49
  "publishing",
50
+ "publish_failed",
50
51
  "published",
51
52
  "package-published",
52
53
  "finalizing",
@@ -354,7 +355,7 @@ function validateCandidateProvenance({
354
355
  run,
355
356
  workflow,
356
357
  pullRequest,
357
- ancestry,
358
+ ancestry, targetTree,
358
359
  passport,
359
360
  anchorProvenance,
360
361
  expectedTransactionId,
@@ -383,7 +384,7 @@ function validateCandidateProvenance({
383
384
  const boundByNumber = Array.isArray(provenanceRun?.pullRequestNumbers) && provenanceRun.pullRequestNumbers.includes(Number(pullRequest?.number));
384
385
  const boundByHead = provenanceRun?.headSha === pullRequest?.headSha && normalizedRef(provenanceRun?.headBranch) === normalizedRef(pullRequest?.headRef);
385
386
  if (!boundByNumber && !boundByHead) fail("pr-identity-invalid", "candidate run is not bound to the merged candidate PR", "Select the exact PR-stage Build run.");
386
- if (!ancestry?.mergeIsAncestor && ancestry?.status !== "identical") fail("ancestry-invalid", "candidate merge is not an ancestor of the promotion SHA", "Promote a descendant of the verified merged candidate identity.");
387
+ if (!ancestry?.mergeIsAncestor && ancestry?.status !== "identical" && exactSha(targetTree, "targetTree") !== exactSha(passport?.source?.treeHash, "passport.source.treeHash")) fail("ancestry-invalid", "candidate merge is neither an ancestor nor tree-equivalent to the promotion SHA", "Promote a descendant of the verified merged candidate identity or its exact tree-equivalent protected merge result.");
387
388
  return { repository, workflowFile };
388
389
  }
389
390
 
@@ -428,11 +429,11 @@ function validateRecoveryTransaction({ existingTransaction, expectedTransactionI
428
429
  if (expectedTransactionId && !actualTransactionId) fail("transaction-identity-conflict", `expected transaction ${expectedTransactionId} does not exist`, "Remove the stale transaction identity only if no durable transaction was ever sealed; otherwise preserve evidence and enter repair_required.");
429
430
  if (expectedTransactionId && expectedTransactionId !== actualTransactionId) fail("transaction-identity-conflict", `existing transaction ${actualTransactionId} conflicts with expected ${expectedTransactionId}`, "Enter repair_required and inspect the durable transaction before any retry.");
430
431
  if (existingTransaction) {
431
- const expectedIdentity = releaseTransactionId({ repository, version, sourceSha: sha, targetRef: normalizedRef(targetRef) });
432
+ const expectedIdentity = releaseTransactionId({ repository, version, sourceSha: exactSha(existingTransaction.source_sha, "existingTransaction.source_sha"), targetRef: normalizedRef(targetRef) });
432
433
  assertEqual(existingTransaction.id, expectedIdentity, "transaction-identity-conflict", "durable transaction identity", "Enter repair_required; the durable transaction does not belong to this exact publication target.");
433
434
  assertEqual(existingTransaction.repository, repository, "transaction-identity-conflict", "durable transaction repository", "Enter repair_required; never cross repository transaction boundaries.");
434
435
  assertEqual(normalizedRef(existingTransaction.target_ref), normalizedRef(targetRef), "transaction-identity-conflict", "durable transaction target ref", "Enter repair_required; never retarget a sealed transaction.");
435
- assertEqual(existingTransaction.source_sha, sha, "transaction-identity-conflict", "durable transaction source SHA", "Resume with the transaction's exact promotion SHA or enter repair_required.");
436
+ if (![existingTransaction.source_sha, existingTransaction.release_sha, existingTransaction.release_material_sha].filter(Boolean).includes(sha)) fail("transaction-identity-conflict", "recovery promotion SHA is not bound to the durable transaction", "Resume with the transaction's exact source, release, or release material SHA, or enter repair_required.");
436
437
  assertEqual(existingTransaction.version, version, "transaction-identity-conflict", "durable transaction version", "Enter repair_required; never change a sealed publication version.");
437
438
  assertEqual(existingTransaction.channel, channel || passport.target.channel, "transaction-identity-conflict", "durable transaction channel", "Enter repair_required; never move a transaction between channels.");
438
439
  }
@@ -476,7 +477,7 @@ export function verifyReleaseCandidateRecovery({
476
477
  const { repository, workflowFile } = validateCandidateProvenance({
477
478
  candidateRepository, targetRepository, expectedRunId, expectedWorkflowFile,
478
479
  expectedWorkflowName, targetRef, run, workflow, pullRequest, ancestry,
479
- passport, anchorProvenance, expectedTransactionId,
480
+ targetTree, passport, anchorProvenance, expectedTransactionId,
480
481
  });
481
482
  const { sha, tree, runtimeSha, toolingSha } = validateCandidateIdentity({
482
483
  repository, channel, targetSha, targetTree, expectedSourceTree,