@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
@@ -0,0 +1,259 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export const ENGINEERING_HOUSEKEEPER_CONTRACT =
4
+ "kungfu-buildchain-engineering-housekeeper/v1";
5
+ export const ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION = 1;
6
+
7
+ export const HOUSEKEEPER_REASON_CODES = Object.freeze({
8
+ ELIGIBLE_MERGED_BRANCH: "eligible.merged-branch",
9
+ PROTECTED_BRANCH: "branch.protected",
10
+ RETAINED_BRANCH: "branch.retained",
11
+ DEFAULT_BRANCH: "branch.default",
12
+ TARGET_BRANCH: "branch.target",
13
+ OPEN_PR_HEAD: "branch.open-pr-head",
14
+ CROSS_REPOSITORY: "branch.cross-repository",
15
+ NOT_MERGED: "branch.not-merged",
16
+ HEAD_ADVANCED: "branch.head-advanced",
17
+ RENAMED: "branch.renamed",
18
+ TARGET_ADVANCED: "branch.target-advanced",
19
+ PERMISSION_DENIED: "branch.permission-denied",
20
+ PR_ACTIVE: "pull-request.active",
21
+ PR_STALE_REPORT: "pull-request.stale-report",
22
+ PR_LABEL_ELIGIBLE: "pull-request.label-eligible",
23
+ PR_AUTO_CLOSE_DISABLED: "pull-request.auto-close-disabled",
24
+ PR_FORKED: "pull-request.forked",
25
+ REPEATED_NO_OP: "replay.already-applied",
26
+ });
27
+
28
+ export const DEFAULT_HOUSEKEEPER_POLICY = Object.freeze({
29
+ protectedPatterns: ["dev/**", "alpha/**", "release/**", "publish-gate/**"],
30
+ retainedPatterns: ["train/**", "authority/**"],
31
+ pullRequests: Object.freeze({
32
+ reportStale: true,
33
+ label: "",
34
+ autoClose: false,
35
+ }),
36
+ });
37
+
38
+ function stableJson(value) {
39
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
40
+ if (value && typeof value === "object") {
41
+ return `{${Object.keys(value)
42
+ .sort()
43
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
44
+ .join(",")}}`;
45
+ }
46
+ return JSON.stringify(value);
47
+ }
48
+
49
+ export function engineeringHousekeeperRoot(value) {
50
+ return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
51
+ }
52
+
53
+ function globMatches(pattern, value) {
54
+ const expression = String(pattern)
55
+ .split("**")
56
+ .map((chunk) =>
57
+ chunk
58
+ .split("*")
59
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
60
+ .join("[^/]*"),
61
+ )
62
+ .join(".*");
63
+ return new RegExp(`^${expression}$`).test(value);
64
+ }
65
+
66
+ function matchesAny(patterns, value) {
67
+ return [...(patterns || [])].some((pattern) => globMatches(pattern, value));
68
+ }
69
+
70
+ function normalizePolicy(policy = {}) {
71
+ return {
72
+ protectedPatterns: [
73
+ ...(policy.protectedPatterns ||
74
+ DEFAULT_HOUSEKEEPER_POLICY.protectedPatterns),
75
+ ].sort(),
76
+ retainedPatterns: [
77
+ ...(policy.retainedPatterns ||
78
+ DEFAULT_HOUSEKEEPER_POLICY.retainedPatterns),
79
+ ].sort(),
80
+ pullRequests: {
81
+ reportStale: policy.pullRequests?.reportStale !== false,
82
+ label: String(policy.pullRequests?.label || ""),
83
+ autoClose: false,
84
+ },
85
+ };
86
+ }
87
+
88
+ function sortedUnique(values) {
89
+ return [...new Set(values)].sort();
90
+ }
91
+
92
+ export function classifyHousekeeperBranch(branch, policyInput = {}) {
93
+ const policy = normalizePolicy(policyInput);
94
+ const reasons = [];
95
+ if (branch.isDefault) reasons.push(HOUSEKEEPER_REASON_CODES.DEFAULT_BRANCH);
96
+ if (branch.name === branch.target?.name)
97
+ reasons.push(HOUSEKEEPER_REASON_CODES.TARGET_BRANCH);
98
+ if (
99
+ branch.isProtected === true ||
100
+ matchesAny(policy.protectedPatterns, branch.name)
101
+ )
102
+ reasons.push(HOUSEKEEPER_REASON_CODES.PROTECTED_BRANCH);
103
+ if (matchesAny(policy.retainedPatterns, branch.name))
104
+ reasons.push(HOUSEKEEPER_REASON_CODES.RETAINED_BRANCH);
105
+ if (branch.sourceRepository && branch.sourceRepository !== branch.repository)
106
+ reasons.push(HOUSEKEEPER_REASON_CODES.CROSS_REPOSITORY);
107
+ if ((branch.openPullRequestNumbers || []).length > 0)
108
+ reasons.push(HOUSEKEEPER_REASON_CODES.OPEN_PR_HEAD);
109
+ if (branch.ancestry !== "ancestor")
110
+ reasons.push(HOUSEKEEPER_REASON_CODES.NOT_MERGED);
111
+ const eligible =
112
+ reasons.length === 0 &&
113
+ Boolean(branch.headOid) &&
114
+ Boolean(branch.target?.headOid);
115
+ return {
116
+ kind: "branch",
117
+ repository: branch.repository,
118
+ name: branch.name,
119
+ headOid: branch.headOid,
120
+ target: { name: branch.target?.name, headOid: branch.target?.headOid },
121
+ eligible,
122
+ decision: eligible ? "delete" : "retain",
123
+ reasonCodes: eligible
124
+ ? [HOUSEKEEPER_REASON_CODES.ELIGIBLE_MERGED_BRANCH]
125
+ : sortedUnique(reasons),
126
+ };
127
+ }
128
+
129
+ export function classifyHousekeeperPullRequest(pullRequest, policyInput = {}) {
130
+ const policy = normalizePolicy(policyInput);
131
+ const active = ["open", "draft"].includes(pullRequest.state);
132
+ const forked = pullRequest.headRepository !== pullRequest.repository;
133
+ const stale = active && pullRequest.stale === true;
134
+ const actions = stale && policy.pullRequests.reportStale ? ["report"] : [];
135
+ if (stale && policy.pullRequests.label) actions.push("label");
136
+ const reasons = [HOUSEKEEPER_REASON_CODES.PR_AUTO_CLOSE_DISABLED];
137
+ if (active) reasons.push(HOUSEKEEPER_REASON_CODES.PR_ACTIVE);
138
+ if (stale) reasons.push(HOUSEKEEPER_REASON_CODES.PR_STALE_REPORT);
139
+ if (stale && policy.pullRequests.label)
140
+ reasons.push(HOUSEKEEPER_REASON_CODES.PR_LABEL_ELIGIBLE);
141
+ if (forked) reasons.push(HOUSEKEEPER_REASON_CODES.PR_FORKED);
142
+ return {
143
+ kind: "pull-request",
144
+ repository: pullRequest.repository,
145
+ number: pullRequest.number,
146
+ headRepository: pullRequest.headRepository,
147
+ headRef: pullRequest.headRef,
148
+ headOid: pullRequest.headOid,
149
+ state: pullRequest.state,
150
+ actions: actions.sort(),
151
+ reasonCodes: sortedUnique(reasons),
152
+ };
153
+ }
154
+
155
+ export function createEngineeringHousekeeperPlan({
156
+ repository,
157
+ target,
158
+ branches = [],
159
+ pullRequests = [],
160
+ policy = {},
161
+ observedAt,
162
+ }) {
163
+ const normalizedPolicy = normalizePolicy(policy);
164
+ const inventory = [
165
+ ...branches.map((branch) =>
166
+ classifyHousekeeperBranch(branch, normalizedPolicy),
167
+ ),
168
+ ...pullRequests.map((pullRequest) =>
169
+ classifyHousekeeperPullRequest(pullRequest, normalizedPolicy),
170
+ ),
171
+ ].sort((left, right) =>
172
+ `${left.kind}:${left.name || String(left.number).padStart(12, "0")}`.localeCompare(
173
+ `${right.kind}:${right.name || String(right.number).padStart(12, "0")}`,
174
+ ),
175
+ );
176
+ const body = {
177
+ contract: ENGINEERING_HOUSEKEEPER_CONTRACT,
178
+ schemaVersion: ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION,
179
+ mode: "plan",
180
+ repository,
181
+ target,
182
+ observedAt,
183
+ policy: normalizedPolicy,
184
+ inventory,
185
+ actions: inventory.flatMap((entry) =>
186
+ entry.kind === "branch" && entry.eligible
187
+ ? [
188
+ {
189
+ kind: "delete-branch",
190
+ name: entry.name,
191
+ expectedHeadOid: entry.headOid,
192
+ targetName: entry.target.name,
193
+ expectedTargetHeadOid: entry.target.headOid,
194
+ },
195
+ ]
196
+ : entry.kind === "pull-request"
197
+ ? entry.actions.map((action) => ({
198
+ kind: `${action}-pull-request`,
199
+ number: entry.number,
200
+ expectedHeadOid: entry.headOid,
201
+ }))
202
+ : [],
203
+ ),
204
+ };
205
+ return { ...body, planRoot: engineeringHousekeeperRoot(body) };
206
+ }
207
+
208
+ export function revalidateHousekeeperBranchAction(
209
+ action,
210
+ current,
211
+ policy = {},
212
+ ) {
213
+ const reasons = [];
214
+ if (current.name !== action.name)
215
+ reasons.push(HOUSEKEEPER_REASON_CODES.RENAMED);
216
+ if (current.headOid !== action.expectedHeadOid)
217
+ reasons.push(HOUSEKEEPER_REASON_CODES.HEAD_ADVANCED);
218
+ if (current.target?.headOid !== action.expectedTargetHeadOid)
219
+ reasons.push(HOUSEKEEPER_REASON_CODES.TARGET_ADVANCED);
220
+ const classification = classifyHousekeeperBranch(current, policy);
221
+ if (!classification.eligible) reasons.push(...classification.reasonCodes);
222
+ return {
223
+ ok: reasons.length === 0,
224
+ action,
225
+ currentHeadOid: current.headOid,
226
+ currentTargetHeadOid: current.target?.headOid,
227
+ reasonCodes: sortedUnique(reasons),
228
+ };
229
+ }
230
+
231
+ export function createEngineeringHousekeeperReceipt({
232
+ plan,
233
+ outcomes,
234
+ appliedAt,
235
+ }) {
236
+ const orderedOutcomes = [...outcomes].sort((a, b) =>
237
+ stableJson(a).localeCompare(stableJson(b)),
238
+ );
239
+ const body = {
240
+ contract: ENGINEERING_HOUSEKEEPER_CONTRACT,
241
+ schemaVersion: ENGINEERING_HOUSEKEEPER_SCHEMA_VERSION,
242
+ mode: "receipt",
243
+ planRoot: plan.planRoot,
244
+ appliedAt,
245
+ outcomes: orderedOutcomes,
246
+ };
247
+ return { ...body, receiptRoot: engineeringHousekeeperRoot(body) };
248
+ }
249
+
250
+ export function classifyHousekeeperReplay(plan, priorReceipt) {
251
+ const alreadyApplied = priorReceipt?.planRoot === plan.planRoot;
252
+ return {
253
+ alreadyApplied,
254
+ action: alreadyApplied ? "no-op" : "apply",
255
+ reasonCodes: alreadyApplied
256
+ ? [HOUSEKEEPER_REASON_CODES.REPEATED_NO_OP]
257
+ : [],
258
+ };
259
+ }
@@ -58,6 +58,45 @@ export {
58
58
  writeReleaseTransaction,
59
59
  } from "./publish-transaction.js";
60
60
 
61
+ export {
62
+ RELEASE_TAIL_CAPABILITY_REGISTRY,
63
+ RELEASE_TAIL_DECLARATION_CONTRACT,
64
+ RELEASE_TAIL_EFFECT_SCHEMA,
65
+ RELEASE_TAIL_OBSERVATION_SCHEMA,
66
+ RELEASE_TAIL_RECEIPT_SCHEMA,
67
+ RELEASE_TAIL_STATES,
68
+ RELEASE_TAIL_TRANSACTION_POLICY,
69
+ RELEASE_TAIL_TRANSACTION_SCHEMA,
70
+ compileReleaseTailDeclaration,
71
+ createReleaseTailAdapterSet,
72
+ createReleaseTailTransaction,
73
+ executeReleaseTailTransaction,
74
+ parseReleaseTailDeclaration,
75
+ readReleaseTailTransaction,
76
+ releaseTailRetryPolicyFromDeclaration,
77
+ releaseTailRoot,
78
+ releaseTailStableJson,
79
+ validateReleaseTailEffectPlan,
80
+ validateReleaseTailTransaction,
81
+ writeReleaseTailTransaction,
82
+ } from "./release-tail-provider-plane.js";
83
+
84
+ export {
85
+ ReleaseTailProviderError,
86
+ createActivationReceiptProjectorAdapter,
87
+ createGitHubReleaseAssetsAdapter,
88
+ createHttpJsonReadback,
89
+ createSignedStaticChannelAdapter,
90
+ createSiteReleaseActivationAdapter,
91
+ githubReleaseAssetsTargetRoot,
92
+ } from "./release-tail-provider-adapters.js";
93
+
94
+ export {
95
+ RELEASE_TAIL_COMPATIBILITY_CONTRACT,
96
+ RELEASE_TAIL_LEGACY_HOOKS,
97
+ diagnoseLegacyReleaseTailHooks,
98
+ } from "./release-tail-compatibility.js";
99
+
61
100
  export {
62
101
  createPortableDevCachePlan,
63
102
  createPortableDevCacheReceipt,
@@ -248,6 +287,9 @@ export {
248
287
  verifyGithubGovernanceReceipt,
249
288
  } from "./github-governance-authority.js";
250
289
 
290
+ export * from "./engineering-housekeeper.js";
291
+ export * from "./engineering-housekeeper-github.js";
292
+
251
293
  export {
252
294
  ARTIFACT_PASSPORT_LOCATOR_CONTRACT,
253
295
  ARTIFACT_PASSPORT_POINTER_CONTRACT,
@@ -944,26 +944,49 @@ export function normalizeKfd1ContractWorldWitness(witness, { metadata = resolveK
944
944
  };
945
945
  }
946
946
 
947
- function resolveArtifactFile({ cwd, artifactRoot, artifacts = [], artifactPath }) {
948
- const candidates = [
949
- path.resolve(cwd, artifactPath),
950
- artifactRoot ? path.resolve(artifactRoot, artifactPath) : "",
951
- ].filter(Boolean);
947
+ function indexArtifactSearchRoots({ roots = [], artifactPaths = [] } = {}) {
948
+ const wanted = [...new Set(artifactPaths.filter(Boolean))];
949
+ const indexed = new Map();
950
+ const stack = roots.filter((root) => root && fs.existsSync(root) && fs.statSync(root).isDirectory());
951
+ while (stack.length > 0) {
952
+ const current = stack.pop();
953
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
954
+ const candidate = path.join(current, entry.name);
955
+ if (entry.isDirectory()) { stack.push(candidate); continue; }
956
+ if (!entry.isFile()) continue;
957
+ const normalized = candidate.replace(/\\/g, "/");
958
+ for (const artifactPath of wanted) {
959
+ if (normalized === artifactPath || normalized.endsWith(`/${artifactPath}`)) {
960
+ const candidates = indexed.get(artifactPath) || [];
961
+ candidates.push(candidate);
962
+ indexed.set(artifactPath, candidates);
963
+ }
964
+ }
965
+ }
966
+ }
967
+ return indexed;
968
+ }
969
+
970
+ function resolveArtifactFile({ cwd, artifactRoot, artifactIndex = new Map(), artifacts = [], artifactPath, expectedSha256 = "" }) {
971
+ const candidates = artifactIndex.size > 0
972
+ ? [...(artifactIndex.get(artifactPath) || []), artifactRoot ? path.resolve(artifactRoot, artifactPath) : "", path.resolve(cwd, artifactPath)]
973
+ : [path.resolve(cwd, artifactPath), artifactRoot ? path.resolve(artifactRoot, artifactPath) : ""];
952
974
  for (const artifact of artifacts) {
953
975
  const sourcePath = artifact.sourcePath || artifact.path || "";
954
- if (!sourcePath) {
955
- continue;
956
- }
976
+ if (!sourcePath) continue;
957
977
  const normalizedSource = sourcePath.replace(/\\/g, "/");
958
- if (normalizedSource === artifactPath || normalizedSource.endsWith(`/${artifactPath}`)) {
959
- candidates.push(path.resolve(sourcePath));
960
- }
978
+ if (normalizedSource === artifactPath || normalizedSource.endsWith(`/${artifactPath}`)) candidates.push(path.resolve(sourcePath));
961
979
  const name = artifact.name || artifact.filename || "";
962
- if (name && name === artifactPath) {
963
- candidates.push(path.resolve(sourcePath));
964
- }
980
+ if (name && name === artifactPath) candidates.push(path.resolve(sourcePath));
965
981
  }
966
- return candidates.find((candidate) => candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) || "";
982
+ const existing = [...new Set(candidates)].filter(
983
+ (candidate) => candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile(),
984
+ );
985
+ if (expectedSha256) {
986
+ const exact = existing.find((candidate) => sha256File(candidate) === expectedSha256);
987
+ if (exact) return exact;
988
+ }
989
+ return existing[0] || "";
967
990
  }
968
991
 
969
992
  function resolveSourceFile({ cwd, sourcePath }) {
@@ -977,6 +1000,7 @@ function resolveSourceFile({ cwd, sourcePath }) {
977
1000
  export function createKfd1ReleaseGateEvidence({
978
1001
  cwd = process.cwd(),
979
1002
  artifactRoot = "",
1003
+ artifactSearchRoots = [],
980
1004
  artifacts = [],
981
1005
  witnesses = [],
982
1006
  verifiedAt = new Date().toISOString(),
@@ -986,6 +1010,10 @@ export function createKfd1ReleaseGateEvidence({
986
1010
  if (normalizedWitnesses.length === 0) {
987
1011
  return undefined;
988
1012
  }
1013
+ const artifactIndex = indexArtifactSearchRoots({
1014
+ roots: artifactSearchRoots,
1015
+ artifactPaths: normalizedWitnesses.flatMap((witness) => witness.surfaces.map((surface) => surface.artifactPath)),
1016
+ });
989
1017
  const worlds = normalizedWitnesses.map((witness) => {
990
1018
  const preBuildWitnessSha256 = sha256Json(witness);
991
1019
  const sourceResults = witness.surfaces.map((surface) => {
@@ -1025,8 +1053,10 @@ export function createKfd1ReleaseGateEvidence({
1025
1053
  const filePath = resolveArtifactFile({
1026
1054
  cwd,
1027
1055
  artifactRoot,
1056
+ artifactIndex,
1028
1057
  artifacts,
1029
1058
  artifactPath: surface.artifactPath,
1059
+ expectedSha256: surface.expectedSha256,
1030
1060
  });
1031
1061
  if (!filePath) {
1032
1062
  return {
@@ -12,6 +12,7 @@ import {
12
12
  stableJson,
13
13
  workCheck,
14
14
  } from "./paper-repository.js";
15
+ import { mergePublicationRehearsalAgentInstructions } from "./publication-rehearsal-projection.js";
15
16
 
16
17
  export const PAPER_AGENT_ENTRY_CONTRACT = "kungfu-buildchain-paper-agent-entry";
17
18
  export const PAPER_AGENT_ENTRY_SCHEMA_VERSION = 1;
@@ -167,9 +168,11 @@ export function paperAgentEntryFiles({
167
168
  [PAPER_PATHS.agentEntry, jsonText(entry)],
168
169
  [
169
170
  PAPER_PATHS.agentInstructions,
170
- mergePaperAgentEntryInstructions(currentAgents, {
171
- developmentRef: resolvedDevelopmentRef,
172
- }),
171
+ mergePublicationRehearsalAgentInstructions(
172
+ mergePaperAgentEntryInstructions(currentAgents, {
173
+ developmentRef: resolvedDevelopmentRef,
174
+ }),
175
+ ),
173
176
  ],
174
177
  ]);
175
178
  }
@@ -205,8 +208,11 @@ function ciContext({ env, developmentRef }) {
205
208
  sourceBranch.startsWith(versionStatePrefix) &&
206
209
  /^[0-9a-f]{12}$/i.test(sourceBranch.slice(versionStatePrefix.length));
207
210
  const branchOk = pullRequest
208
- ? (PAPER_WORK_BRANCH_PATTERN.test(sourceBranch) && targetBranch === developmentRef) ||
209
- (channelIndex > 0 && sourceBranch === channelBranches[channelIndex - 1]) || generatedVersionState
211
+ ? (PAPER_WORK_BRANCH_PATTERN.test(sourceBranch) &&
212
+ targetBranch === developmentRef) ||
213
+ (channelIndex > 0 &&
214
+ sourceBranch === channelBranches[channelIndex - 1]) ||
215
+ generatedVersionState
210
216
  : channelBranches.includes(refName);
211
217
  return {
212
218
  mode: "ci",
@@ -13,6 +13,7 @@ export const PAPER_PATHS = Object.freeze({
13
13
  buildWorkflow: ".github/workflows/build.yml",
14
14
  verifyWorkflow: ".github/workflows/verify.yml",
15
15
  releaseWorkflow: ".github/workflows/paper-release.yml",
16
+ rehearsalWorkflow: ".github/workflows/publication-rehearsal.yml",
16
17
  pnpmWorkspace: "pnpm-workspace.yaml",
17
18
  reproducibilityReceipt:
18
19
  ".buildchain/publication/reproducibility-receipt.json",
@@ -1,3 +1,10 @@
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
+
1
8
  function jsonText(value) {
2
9
  return `${JSON.stringify(value, null, 2)}\n`;
3
10
  }
@@ -47,6 +54,7 @@ export function paperPackageScripts(current = {}) {
47
54
  "paper:work:start": "buildchain paper work start",
48
55
  "paper:work:submit": "buildchain paper work submit",
49
56
  "paper:status": "buildchain paper status --json",
57
+ "publication:rehearse": PUBLICATION_REHEARSAL_COMMAND,
50
58
  };
51
59
  }
52
60
 
@@ -106,6 +114,7 @@ pnpm paper:preflight
106
114
  pnpm paper:agent:verify
107
115
  pnpm paper:work:start -- <topic>
108
116
  pnpm paper:work:submit
117
+ pnpm publication:rehearse
109
118
  make pdf
110
119
  \`\`\`
111
120
 
@@ -114,6 +123,11 @@ artifact generation, sealed publication, npm Trusted Publishing, and recovery;
114
123
  this repository owns the paper source and review history.
115
124
 
116
125
  See [docs/MAP.md](docs/MAP.md) for the repository map.
126
+
127
+ Publication qualification follows the Buildchain
128
+ \`${RELEASE_LOCAL_CONSTRUCTIBILITY_ADR}\` invariant:
129
+ ${RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT} The capsule contract is
130
+ \`${PUBLICATION_REHEARSAL_CAPSULE_CONTRACT}\`.
117
131
  `;
118
132
  }
119
133
 
@@ -129,10 +143,17 @@ export function scaffoldMap() {
129
143
  - \`.github/workflows/build.yml\`: thin read-only build and reproducibility caller.
130
144
  - \`.github/workflows/verify.yml\`: thin required check that enforces the Paper entry and acceptance policy.
131
145
  - \`.github/workflows/paper-release.yml\`: thin protected sealed-release caller.
146
+ - \`.github/workflows/publication-rehearsal.yml\`: thin hosted adapter over the same local capsule runtime.
132
147
 
133
148
  The repository does not own npm transaction logic, publication authority,
134
149
  release-state recovery, or site deployment mechanics. Those remain Buildchain,
135
150
  npm/GitHub, and downstream site responsibilities respectively.
151
+
152
+ The exact local pre-publication command is:
153
+
154
+ \`\`\`sh
155
+ ${PUBLICATION_REHEARSAL_COMMAND}
156
+ \`\`\`
136
157
  `;
137
158
  }
138
159
 
@@ -46,6 +46,11 @@ import {
46
46
  scaffoldReadme,
47
47
  } from "./paper-scaffold-content.js";
48
48
  import { executePaperNpmBootstrap as executePaperNpmBootstrapOperation } from "./paper-npm-bootstrap.js";
49
+ import {
50
+ appendPublicationRehearsalToml,
51
+ projectPublicationRehearsalToml,
52
+ publicationRehearsalWorkflow,
53
+ } from "./publication-rehearsal-projection.js";
49
54
 
50
55
  export { PAPER_PATHS, resolvePaperRepository } from "./paper-repository.js";
51
56
  export {
@@ -114,6 +119,7 @@ const PAPER_SCAFFOLD_PATHS = Object.freeze([
114
119
  PAPER_PATHS.buildWorkflow,
115
120
  PAPER_PATHS.verifyWorkflow,
116
121
  PAPER_PATHS.releaseWorkflow,
122
+ PAPER_PATHS.rehearsalWorkflow,
117
123
  PAPER_PATHS.pnpmWorkspace,
118
124
  PAPER_PATHS.provisioningAuthority,
119
125
  "Makefile",
@@ -350,7 +356,7 @@ immutable_base_url = ${tomlString(joinUrl(siteBaseUrl, "archive").replace(/\/$/,
350
356
  registry_path = ".buildchain/publication/publication-registry.json"
351
357
  `
352
358
  : "";
353
- return `schema = 1
359
+ return appendPublicationRehearsalToml(`schema = 1
354
360
 
355
361
  [project]
356
362
  type = "publication-artifact"
@@ -384,7 +390,7 @@ command = "make pdf"
384
390
 
385
391
  [lifecycle.verify]
386
392
  command = "make check"
387
- `;
393
+ `);
388
394
  }
389
395
 
390
396
  function scaffoldBuildWorkflow(
@@ -525,6 +531,7 @@ export function createPaperProvisioningAuthority({
525
531
  buildWorkflow,
526
532
  verifyWorkflow,
527
533
  releaseWorkflow,
534
+ rehearsalWorkflow,
528
535
  agentEntry,
529
536
  agentInstructions,
530
537
  environment = "",
@@ -564,6 +571,12 @@ export function createPaperProvisioningAuthority({
564
571
  reusablePath: ".github/workflows/paper-release-sealed.yml",
565
572
  reusableRef: buildchainSha,
566
573
  },
574
+ rehearsal: {
575
+ path: PAPER_PATHS.rehearsalWorkflow,
576
+ sourceDigest: sha256Text(rehearsalWorkflow),
577
+ reusablePath: ".github/workflows/release-tail.yml",
578
+ reusableRef: buildchainSha,
579
+ },
567
580
  },
568
581
  agentEntry: {
569
582
  contract: PAPER_AGENT_ENTRY_CONTRACT,
@@ -630,6 +643,7 @@ function scaffoldFiles({
630
643
  releasePassportProductName: title,
631
644
  });
632
645
  const verifyWorkflow = scaffoldVerifyWorkflow(buildchainSha);
646
+ const rehearsalWorkflow = publicationRehearsalWorkflow(buildchainSha);
633
647
  const agentEntry = paperAgentEntryFiles({
634
648
  cwd,
635
649
  buildchainVersion,
@@ -647,6 +661,7 @@ function scaffoldFiles({
647
661
  buildWorkflow,
648
662
  verifyWorkflow,
649
663
  releaseWorkflow,
664
+ rehearsalWorkflow,
650
665
  agentEntry: agentEntry.get(PAPER_PATHS.agentEntry),
651
666
  agentInstructions: agentEntry.get(PAPER_PATHS.agentInstructions),
652
667
  });
@@ -667,6 +682,7 @@ function scaffoldFiles({
667
682
  [PAPER_PATHS.buildWorkflow, buildWorkflow],
668
683
  [PAPER_PATHS.verifyWorkflow, verifyWorkflow],
669
684
  [PAPER_PATHS.releaseWorkflow, releaseWorkflow],
685
+ [PAPER_PATHS.rehearsalWorkflow, rehearsalWorkflow],
670
686
  [PAPER_PATHS.pnpmWorkspace, paperPnpmWorkspace("", buildchainVersion)],
671
687
  [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
672
688
  ...agentEntry,
@@ -964,6 +980,7 @@ function migrationFiles({
964
980
  releasePassportProductName: config.publication.title,
965
981
  });
966
982
  const verifyWorkflow = scaffoldVerifyWorkflow(runtimeSha);
983
+ const rehearsalWorkflow = publicationRehearsalWorkflow(runtimeSha);
967
984
  const agentEntry = paperAgentEntryFiles({
968
985
  cwd,
969
986
  buildchainVersion: runtimeIdentity.version,
@@ -978,6 +995,7 @@ function migrationFiles({
978
995
  buildWorkflow,
979
996
  verifyWorkflow,
980
997
  releaseWorkflow,
998
+ rehearsalWorkflow,
981
999
  agentEntry: agentEntry.get(PAPER_PATHS.agentEntry),
982
1000
  agentInstructions: agentEntry.get(PAPER_PATHS.agentInstructions),
983
1001
  });
@@ -997,11 +1015,18 @@ function migrationFiles({
997
1015
  runtimeIdentity.version,
998
1016
  );
999
1017
  const files = new Map([
1018
+ [
1019
+ PAPER_PATHS.config,
1020
+ projectPublicationRehearsalToml(
1021
+ fs.readFileSync(path.resolve(cwd, PAPER_PATHS.config), "utf8"),
1022
+ ),
1023
+ ],
1000
1024
  [PAPER_PATHS.contractLock, contractLockText],
1001
1025
  [PAPER_PATHS.versionPin, `${runtimeIdentity.version}\n`],
1002
1026
  [PAPER_PATHS.buildWorkflow, buildWorkflow],
1003
1027
  [PAPER_PATHS.verifyWorkflow, verifyWorkflow],
1004
1028
  [PAPER_PATHS.releaseWorkflow, releaseWorkflow],
1029
+ [PAPER_PATHS.rehearsalWorkflow, rehearsalWorkflow],
1005
1030
  [PAPER_PATHS.pnpmWorkspace, pnpmWorkspace],
1006
1031
  [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
1007
1032
  ...agentEntry,
@@ -1755,6 +1780,7 @@ function validatePaperProvisioningAuthority(cwd) {
1755
1780
  value.workflows?.build,
1756
1781
  value.workflows?.verify,
1757
1782
  value.workflows?.release,
1783
+ value.workflows?.rehearsal,
1758
1784
  ]) {
1759
1785
  if (!workflow?.path || !workflow?.sourceDigest) {
1760
1786
  errors.push("paper workflow authority is incomplete");