@kungfu-tech/buildchain 3.0.2 → 3.0.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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -1
  4. package/actions/promote-buildchain-ref/README.md +90 -19
  5. package/actions/report-buildchain-issue/README.md +20 -2
  6. package/actions/run-lifecycle/README.md +5 -0
  7. package/actions/validate-config/README.md +19 -1
  8. package/bin/buildchain.mjs +56 -2
  9. package/bin/internal/command-registry.mjs +82 -0
  10. package/bin/internal/trust-release-cli.mjs +1 -0
  11. package/contracts/auditable-demo-media-profiles-v1.json +116 -1
  12. package/contracts/buildchain-v2-residuals-v1.json +146 -0
  13. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
  14. package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
  15. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
  16. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
  17. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
  18. package/dist/site/buildchain-contract.json +188 -46
  19. package/dist/site/buildchain-site.json +181 -90
  20. package/dist/site/capability-registry.json +5 -5
  21. package/dist/site/cli-registry.json +860 -98
  22. package/dist/site/controller-registry.json +118 -5
  23. package/dist/site/kfd-claims.json +233 -19
  24. package/dist/site/kfd-upstream-aggregate.json +1 -1
  25. package/dist/site/manual-registry.json +20 -20
  26. package/dist/site/node-api-registry.json +374 -59
  27. package/dist/site/page-registry.json +156 -65
  28. package/dist/site/public-surface-audit.json +231 -22
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/release-provenance.json +3 -0
  31. package/dist/site/site-manifest.json +24 -24
  32. package/dist/site/workflow-registry.json +575 -79
  33. package/docs/MAP.md +22 -4
  34. package/docs/auditable-demo.md +54 -13
  35. package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
  36. package/docs/cli.md +119 -12
  37. package/docs/consumer-issue-reporting.md +20 -2
  38. package/docs/dev-alpha-candidate-patrol.md +24 -10
  39. package/docs/github-governance-authority.md +24 -31
  40. package/docs/lifecycle-protocol.md +20 -2
  41. package/docs/migration-inventory.md +24 -5
  42. package/docs/observed-evidence-patrol.md +28 -12
  43. package/docs/ownership.md +19 -1
  44. package/docs/publication-artifacts.md +158 -18
  45. package/docs/publication-authority.md +4 -4
  46. package/docs/publish-transaction.md +78 -31
  47. package/docs/release-candidate.md +24 -0
  48. package/docs/release-flow.md +50 -32
  49. package/docs/release-governance.md +39 -32
  50. package/docs/release-passport.md +4 -0
  51. package/docs/release-propagation.md +57 -12
  52. package/docs/reusable-build-surface.md +218 -90
  53. package/docs/runtime-train-validation.md +20 -2
  54. package/docs/shifu-gate-profiles.md +7 -1
  55. package/docs/stable-candidate-patrol.md +6 -6
  56. package/docs/toolkit-observability.md +22 -0
  57. package/docs/versioning.md +46 -24
  58. package/docs/web-surface-deployments.md +82 -1
  59. package/package.json +7 -5
  60. package/packages/core/README.md +60 -4
  61. package/packages/core/artifact-signing.js +1 -0
  62. package/packages/core/buildchain-contract.js +9 -4
  63. package/packages/core/cache-evidence.js +1 -0
  64. package/packages/core/controller-evidence.js +2 -0
  65. package/packages/core/diagnostics.js +105 -30
  66. package/packages/core/github-governance-authority.js +25 -17
  67. package/packages/core/index.js +40 -0
  68. package/packages/core/paper.js +3226 -0
  69. package/packages/core/public-surface-audit.js +3 -0
  70. package/packages/core/publication-artifact.js +1 -6
  71. package/packages/core/publication-authority.js +1 -0
  72. package/packages/core/publication-control-plane-audit.js +28 -0
  73. package/packages/core/publication-reproducibility.js +947 -0
  74. package/packages/core/publication-sealed-bundle.js +190 -0
  75. package/packages/core/publish-transaction.js +147 -17
  76. package/packages/core/release-candidate.js +234 -0
  77. package/packages/core/release-passport.js +519 -228
  78. package/packages/core/release-propagation.js +175 -1
  79. package/packages/core/stable-release-gate.js +4 -1
  80. package/scripts/artifact-signing-delegation.mjs +268 -0
  81. package/scripts/audit-github-governance.mjs +32 -13
  82. package/scripts/audit-publication-control-plane.mjs +17 -8
  83. package/scripts/auditable-demo.mjs +266 -7
  84. package/scripts/aws-codebuild-toolchain.mjs +285 -0
  85. package/scripts/aws-macos-jit-core.mjs +378 -0
  86. package/scripts/aws-macos-jit.mjs +123 -0
  87. package/scripts/aws-runner-burst-core.mjs +248 -0
  88. package/scripts/aws-runner-burst.mjs +79 -0
  89. package/scripts/aws-windows-jit-core.mjs +374 -0
  90. package/scripts/aws-windows-jit.mjs +121 -0
  91. package/scripts/build-contract-core.mjs +69 -6
  92. package/scripts/build-standalone-binary.mjs +23 -0
  93. package/scripts/buildchain-contract-lock.mjs +1 -1
  94. package/scripts/buildchain-patrol.mjs +1 -1
  95. package/scripts/check-internal-architecture.mjs +69 -2
  96. package/scripts/check-inventory.mjs +70 -2
  97. package/scripts/check-javascript-syntax.mjs +31 -0
  98. package/scripts/check-maintainability.mjs +371 -0
  99. package/scripts/compiler-cache-evidence.mjs +221 -0
  100. package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
  101. package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
  102. package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
  103. package/scripts/gate-profile-core.mjs +6 -1
  104. package/scripts/generate-channel-promotion-workflow.mjs +3 -12
  105. package/scripts/generate-release-candidate-passport.mjs +4 -0
  106. package/scripts/generate-site-bundle.mjs +84 -18
  107. package/scripts/import-artifact-signing-results.mjs +64 -5
  108. package/scripts/inspect-artifact-signing-requests.mjs +52 -14
  109. package/scripts/maintainability-metrics.mjs +339 -0
  110. package/scripts/npm-publish-transaction.mjs +57 -6
  111. package/scripts/observed-evidence.mjs +151 -33
  112. package/scripts/paper.mjs +645 -0
  113. package/scripts/promotion-channel-router.mjs +16 -4
  114. package/scripts/publication-reproducibility.mjs +62 -0
  115. package/scripts/reconcile-github-governance.mjs +8 -1
  116. package/scripts/release-propagation.mjs +39 -0
  117. package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
  118. package/scripts/resolve-build-contract.mjs +6 -0
  119. package/scripts/route-offline-runners.mjs +236 -0
  120. package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
  121. package/scripts/run-lifecycle-core.mjs +62 -1
  122. package/scripts/runtime-ref-core.mjs +36 -4
  123. package/scripts/seal-artifact-signing-requests.mjs +64 -12
  124. package/scripts/stable-candidate-qualification.mjs +50 -0
  125. package/scripts/verify-artifact-signing-results.mjs +26 -1
  126. package/scripts/web-surface-core.mjs +272 -86
  127. package/scripts/web-surface-production-decision.mjs +19 -3
@@ -0,0 +1,3226 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ loadBuildchainConfig,
8
+ validateBuildchainConfig,
9
+ } from "./buildchain-config.js";
10
+ import {
11
+ createBuildchainContractLock,
12
+ createBuildchainContractWorld,
13
+ evaluateBuildchainContractLock,
14
+ readBuildchainContractLock,
15
+ } from "./buildchain-contract.js";
16
+ import {
17
+ releaseTransactionPublicationState,
18
+ readReleaseTransaction,
19
+ } from "./publish-transaction.js";
20
+ import { PUBLICATION_REPRODUCIBILITY_RECEIPT_CONTRACT } from "./publication-reproducibility.js";
21
+ import {
22
+ PUBLICATION_SEALED_BUNDLE_CONTRACT,
23
+ verifyPublicationSealedBundle,
24
+ } from "./publication-sealed-bundle.js";
25
+
26
+ export const PAPER_SCAFFOLD_CONTRACT = "kungfu-buildchain-paper-scaffold";
27
+ export const PAPER_MIGRATION_CONTRACT = "kungfu-buildchain-paper-migration";
28
+ export const PAPER_PREFLIGHT_CONTRACT = "kungfu-buildchain-paper-preflight";
29
+ export const PAPER_STATUS_CONTRACT = "kungfu-buildchain-paper-status";
30
+ export const PAPER_NPM_BOOTSTRAP_CONTRACT =
31
+ "kungfu-buildchain-paper-npm-bootstrap";
32
+ export const PAPER_PROVISIONING_CONTRACT =
33
+ "kungfu-buildchain-paper-provisioning-authority";
34
+ export const PAPER_BUILD_PLAN_CONTRACT = "kungfu-buildchain-paper-build-plan";
35
+ export const PAPER_ALPHA_PLAN_CONTRACT = "kungfu-buildchain-paper-alpha-plan";
36
+ export const PAPER_RESUME_PLAN_CONTRACT = "kungfu-buildchain-paper-resume-plan";
37
+ export const PAPER_VISIBILITY_CONTRACT = "kungfu-buildchain-paper-visibility";
38
+
39
+ export const PAPER_STATE_ORDER = Object.freeze([
40
+ "scaffolded",
41
+ "governed",
42
+ "admitted",
43
+ "bootstrapped",
44
+ "trust-bound",
45
+ "content-ready",
46
+ "artifact-sealed",
47
+ "package-published",
48
+ "alpha-complete",
49
+ "staging-visible",
50
+ "production-visible",
51
+ ]);
52
+
53
+ export const PAPER_PATHS = Object.freeze({
54
+ config: ".buildchain/buildchain.toml",
55
+ versionPin: ".buildchain-version",
56
+ contractLock: ".buildchain/contract-lock.json",
57
+ buildWorkflow: ".github/workflows/build.yml",
58
+ releaseWorkflow: ".github/workflows/paper-release.yml",
59
+ reproducibilityReceipt:
60
+ ".buildchain/publication/reproducibility-receipt.json",
61
+ sealedBundle: ".buildchain/admitted/sealed-bundle.json",
62
+ admission: ".buildchain/admitted/publication-admission.json",
63
+ capability: ".buildchain/admitted/publication-capability.json",
64
+ npmBootstrap: ".buildchain/paper/npm-bootstrap.json",
65
+ npmTrust: ".buildchain/paper/npm-trust.json",
66
+ provisioningAuthority: ".buildchain/paper/provisioning-authority.json",
67
+ visibility: ".buildchain/paper/visibility.json",
68
+ });
69
+ const PAPER_SCAFFOLD_PATHS = Object.freeze([
70
+ PAPER_PATHS.config,
71
+ PAPER_PATHS.contractLock,
72
+ PAPER_PATHS.versionPin,
73
+ PAPER_PATHS.buildWorkflow,
74
+ PAPER_PATHS.releaseWorkflow,
75
+ PAPER_PATHS.provisioningAuthority,
76
+ "Makefile",
77
+ "package.json",
78
+ "README.md",
79
+ "docs/MAP.md",
80
+ "paper/main.tex",
81
+ "paper/references.bib",
82
+ "LICENSE",
83
+ ".gitignore",
84
+ ]);
85
+
86
+ const NPM_REGISTRY = "https://registry.npmjs.org/";
87
+ const DEFAULT_BOOTSTRAP_VERSION = "0.0.0-bootstrap.0";
88
+ const DEFAULT_TOOLCHAIN_IMAGE =
89
+ "ghcr.io/kungfu-systems/build-images/latex-pdf-builder";
90
+ const DEFAULT_TOOLCHAIN_DIGEST =
91
+ "sha256:c20f3809e96836c1c78e97c76939d12f1de3fed0ea9b7c40c43332ec2ea480f8";
92
+ const DEFAULT_TOOLCHAIN_COMMAND = "latexmk -pdf -outdir=_build paper/main.tex";
93
+ const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/i;
94
+ const GIT_SHA_PATTERN = /^[0-9a-f]{40}$/i;
95
+ const PACKAGE_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i;
96
+
97
+ function toPosix(value) {
98
+ return String(value || "")
99
+ .split(path.sep)
100
+ .join("/");
101
+ }
102
+
103
+ function stableJson(value) {
104
+ if (Array.isArray(value)) {
105
+ return `[${value.map(stableJson).join(",")}]`;
106
+ }
107
+ if (value && typeof value === "object") {
108
+ return `{${Object.keys(value)
109
+ .sort()
110
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
111
+ .join(",")}}`;
112
+ }
113
+ return JSON.stringify(value);
114
+ }
115
+
116
+ function sha256Text(value) {
117
+ return `sha256:${crypto.createHash("sha256").update(String(value)).digest("hex")}`;
118
+ }
119
+
120
+ function sha256File(filePath) {
121
+ return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
122
+ }
123
+
124
+ function jsonText(value) {
125
+ return `${JSON.stringify(value, null, 2)}\n`;
126
+ }
127
+
128
+ function readJson(filePath) {
129
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
130
+ return { exists: false, value: undefined, error: "" };
131
+ }
132
+ try {
133
+ return {
134
+ exists: true,
135
+ value: JSON.parse(fs.readFileSync(filePath, "utf8")),
136
+ error: "",
137
+ };
138
+ } catch (error) {
139
+ return { exists: true, value: undefined, error: error.message };
140
+ }
141
+ }
142
+
143
+ function existingFileFact(cwd, relativePath) {
144
+ const filePath = path.resolve(cwd, relativePath);
145
+ if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
146
+ return undefined;
147
+ }
148
+ return {
149
+ path: toPosix(relativePath),
150
+ bytes: fs.statSync(filePath).size,
151
+ sha256: sha256File(filePath),
152
+ };
153
+ }
154
+
155
+ function normalizePackageName(value, label = "package name") {
156
+ const normalized = String(value || "").trim();
157
+ if (!PACKAGE_PATTERN.test(normalized) || normalized.length > 214) {
158
+ throw new Error(`${label} is invalid`);
159
+ }
160
+ return normalized;
161
+ }
162
+
163
+ function normalizeRepository(value) {
164
+ const normalized = String(value || "")
165
+ .trim()
166
+ .replace(/^git\+/, "")
167
+ .replace(/^git@github\.com:/, "")
168
+ .replace(/^ssh:\/\/git@github\.com\//, "")
169
+ .replace(/^https?:\/\/github\.com\//, "")
170
+ .replace(/\.git$/, "")
171
+ .replace(/^\/+|\/+$/g, "");
172
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
173
+ return "";
174
+ }
175
+ return normalized;
176
+ }
177
+
178
+ function commandResult(
179
+ command,
180
+ args,
181
+ { cwd, env = process.env, timeout = 15000 } = {},
182
+ ) {
183
+ const result = spawnSync(command, args, {
184
+ cwd,
185
+ env,
186
+ encoding: "utf8",
187
+ timeout,
188
+ maxBuffer: 2 * 1024 * 1024,
189
+ });
190
+ return {
191
+ ok: result.status === 0,
192
+ status: result.status ?? 1,
193
+ stdout: String(result.stdout || "").trim(),
194
+ stderr: String(result.stderr || "").trim(),
195
+ error: result.error?.message || "",
196
+ };
197
+ }
198
+
199
+ function gitResult(cwd, args) {
200
+ return commandResult("git", args, { cwd });
201
+ }
202
+
203
+ function gitValue(cwd, args) {
204
+ const result = gitResult(cwd, args);
205
+ return result.ok ? result.stdout : "";
206
+ }
207
+
208
+ function buildchainPackageIdentity(buildchainRoot, explicitVersion = "") {
209
+ const packageJson = readJson(
210
+ path.resolve(buildchainRoot, "package.json"),
211
+ ).value;
212
+ return {
213
+ name: String(packageJson?.name || "@kungfu-tech/buildchain"),
214
+ version: String(explicitVersion || packageJson?.version || ""),
215
+ };
216
+ }
217
+
218
+ function runtimeGitSha(buildchainRoot, buildchainVersion = "") {
219
+ const value = gitValue(buildchainRoot, ["rev-parse", "HEAD"]);
220
+ if (GIT_SHA_PATTERN.test(value)) return value;
221
+ const identity = buildchainPackageIdentity(buildchainRoot, buildchainVersion);
222
+ if (!identity.version) return "";
223
+ const observed = commandResult(
224
+ "npm",
225
+ [
226
+ "view",
227
+ `${identity.name}@${identity.version}`,
228
+ "gitHead",
229
+ "--json",
230
+ `--registry=${NPM_REGISTRY}`,
231
+ ],
232
+ { cwd: buildchainRoot },
233
+ );
234
+ if (!observed.ok) return "";
235
+ const parsed = safeParseJson(observed.stdout);
236
+ const gitHead =
237
+ typeof parsed === "string" ? parsed : String(parsed?.gitHead || "");
238
+ return GIT_SHA_PATTERN.test(gitHead) ? gitHead : "";
239
+ }
240
+
241
+ function runtimeAcceptedAt(buildchainRoot, sha) {
242
+ if (!sha) return "1970-01-01T00:00:00.000Z";
243
+ const value = gitValue(buildchainRoot, ["show", "-s", "--format=%cI", sha]);
244
+ if (!value) return "1970-01-01T00:00:00.000Z";
245
+ const parsed = new Date(value);
246
+ return Number.isNaN(parsed.valueOf())
247
+ ? "1970-01-01T00:00:00.000Z"
248
+ : parsed.toISOString();
249
+ }
250
+
251
+ function runtimeContractWorld(buildchainRoot) {
252
+ const embedded = String(process.env.BUILDCHAIN_EMBEDDED_CONTRACT_WORLD || "");
253
+ if (embedded) {
254
+ const parsed = safeParseJson(embedded);
255
+ if (parsed?.contract) return parsed;
256
+ throw new Error("embedded Buildchain contract world is invalid");
257
+ }
258
+ return createBuildchainContractWorld({ root: buildchainRoot });
259
+ }
260
+
261
+ function runtimeLicenseText(buildchainRoot) {
262
+ const licensePath = path.join(buildchainRoot, "LICENSE");
263
+ if (fs.existsSync(licensePath) && fs.statSync(licensePath).isFile()) {
264
+ return fs.readFileSync(licensePath, "utf8");
265
+ }
266
+ const embedded = String(process.env.BUILDCHAIN_EMBEDDED_LICENSE_TEXT || "");
267
+ if (embedded) return embedded;
268
+ throw new Error(
269
+ "Buildchain package is missing LICENSE; cannot create a governed paper scaffold",
270
+ );
271
+ }
272
+
273
+ function tomlString(value) {
274
+ return JSON.stringify(String(value || ""));
275
+ }
276
+
277
+ function texEscape(value) {
278
+ return String(value || "")
279
+ .replaceAll("\\", "\\textbackslash{}")
280
+ .replaceAll("&", "\\&")
281
+ .replaceAll("%", "\\%")
282
+ .replaceAll("$", "\\$")
283
+ .replaceAll("#", "\\#")
284
+ .replaceAll("_", "\\_")
285
+ .replaceAll("{", "\\{")
286
+ .replaceAll("}", "\\}");
287
+ }
288
+
289
+ function joinUrl(base, suffix = "") {
290
+ const normalizedBase = String(base || "")
291
+ .trim()
292
+ .replace(/\/+$/, "");
293
+ const normalizedSuffix = String(suffix || "")
294
+ .trim()
295
+ .replace(/^\/+|\/+$/g, "");
296
+ if (!normalizedBase) return "";
297
+ return normalizedSuffix
298
+ ? `${normalizedBase}/${normalizedSuffix}/`
299
+ : `${normalizedBase}/`;
300
+ }
301
+
302
+ function scaffoldConfig({ name, title, packageName, version, siteBaseUrl }) {
303
+ const archiveId = name.replace(/^paper-/, "");
304
+ const canonicalUrl = joinUrl(siteBaseUrl, archiveId);
305
+ const archive = canonicalUrl
306
+ ? `
307
+ [publication.archive]
308
+ id = ${tomlString(archiveId)}
309
+ canonical_url = ${tomlString(canonicalUrl)}
310
+ latest_url = ${tomlString(joinUrl(canonicalUrl, "latest"))}
311
+ latest_evidence_url = ${tomlString(`${joinUrl(canonicalUrl, "latest")}buildchain.release.json`)}
312
+ immutable_base_url = ${tomlString(joinUrl(siteBaseUrl, "archive").replace(/\/$/, ""))}
313
+ registry_path = ".buildchain/publication/publication-registry.json"
314
+ `
315
+ : "";
316
+ return `schema = 1
317
+
318
+ [project]
319
+ type = "publication-artifact"
320
+ name = ${tomlString(name)}
321
+
322
+ [publication]
323
+ kind = "paper"
324
+ title = ${tomlString(title)}
325
+ version = ${tomlString(version)}
326
+ primary_artifact = "_build/main.pdf"
327
+ artifact_paths = ["_build/main.pdf"]
328
+ metadata_paths = ["README.md", "docs/MAP.md"]
329
+ source_paths = ["paper", "README.md", "LICENSE", "Makefile"]
330
+ site_consumers = ${siteBaseUrl ? `[${tomlString(siteBaseUrl)}]` : "[]"}
331
+ manifest_path = ".buildchain/publication/publication-artifact.json"
332
+ source_bundle_path = ".buildchain/publication/source.tar.gz"
333
+ ${archive}
334
+ [publication.toolchain]
335
+ type = "latex-docker"
336
+ image = "${DEFAULT_TOOLCHAIN_IMAGE}"
337
+ digest = "${DEFAULT_TOOLCHAIN_DIGEST}"
338
+ command = "${DEFAULT_TOOLCHAIN_COMMAND}"
339
+
340
+ [publish]
341
+ kind = "npm-paper-package"
342
+ package = ${tomlString(packageName)}
343
+ auth = "trusted-publishing"
344
+
345
+ [lifecycle.build]
346
+ command = "make pdf"
347
+
348
+ [lifecycle.verify]
349
+ command = "make check"
350
+ `;
351
+ }
352
+
353
+ function scaffoldBuildWorkflow(
354
+ buildchainSha,
355
+ { artifactName = "paper-publication" } = {},
356
+ ) {
357
+ return `name: Build
358
+
359
+ on:
360
+ workflow_dispatch:
361
+ pull_request:
362
+ push:
363
+ branches:
364
+ - "dev/**"
365
+ - "alpha/**"
366
+ - "release/**"
367
+
368
+ permissions:
369
+ contents: read
370
+ issues: write
371
+
372
+ jobs:
373
+ publication:
374
+ uses: kungfu-systems/buildchain/.github/workflows/publication-artifact.yml@${buildchainSha}
375
+ with:
376
+ buildchain-ref: ${buildchainSha}
377
+ buildchain-contract-lock-path: .buildchain/contract-lock.json
378
+ toolchain-type: config
379
+ verify-command: make check
380
+ artifact-name: ${JSON.stringify(artifactName)}
381
+ `;
382
+ }
383
+
384
+ function scaffoldReleaseWorkflow(
385
+ buildchainSha,
386
+ { artifactPaths = "_build/main.pdf", releasePassportProductName = "" } = {},
387
+ ) {
388
+ const passportInput = releasePassportProductName
389
+ ? ` release-passport-product-name: ${JSON.stringify(releasePassportProductName)}\n`
390
+ : "";
391
+ return `name: Paper Release
392
+
393
+ on:
394
+ workflow_dispatch:
395
+ push:
396
+ branches:
397
+ - "alpha/**"
398
+ - "release/**"
399
+
400
+ permissions:
401
+ contents: read
402
+
403
+ jobs:
404
+ paper-release:
405
+ uses: kungfu-systems/buildchain/.github/workflows/paper-release-sealed.yml@${buildchainSha}
406
+ permissions:
407
+ actions: read
408
+ checks: write
409
+ contents: read
410
+ id-token: write
411
+ issues: write
412
+ pull-requests: write
413
+ with:
414
+ buildchain-ref: ${buildchainSha}
415
+ buildchain-contract-lock-path: .buildchain/contract-lock.json
416
+ publisher-workflow-path: .github/workflows/paper-release.yml
417
+ toolchain-type: config
418
+ verify-command: make check
419
+ artifact-paths: ${JSON.stringify(artifactPaths)}
420
+ ${passportInput} secrets:
421
+ KUNGFU_GOVERNANCE_AUDITOR_APP_PRIVATE_KEY: \${{ secrets.KUNGFU_GOVERNANCE_AUDITOR_APP_PRIVATE_KEY }}
422
+ BUILDCHAIN_GENERATED_WRITE_APP_CLIENT_ID: \${{ secrets.BUILDCHAIN_GENERATED_WRITE_APP_CLIENT_ID }}
423
+ BUILDCHAIN_GENERATED_WRITE_APP_PRIVATE_KEY: \${{ secrets.BUILDCHAIN_GENERATED_WRITE_APP_PRIVATE_KEY }}
424
+ BUILDCHAIN_GENERATED_WRITE_TOKEN: \${{ secrets.BUILDCHAIN_GENERATED_WRITE_TOKEN }}
425
+ `;
426
+ }
427
+
428
+ function paperProvisioningPolicy() {
429
+ return {
430
+ repositoryActions: {
431
+ defaultWorkflowPermissions: "read",
432
+ canApprovePullRequestReviews: false,
433
+ },
434
+ generatedWrites: {
435
+ preferredAuthority: "github-app",
436
+ compatibilityAuthority: "narrow-token",
437
+ githubTokenFallback: false,
438
+ permissions: ["checks:write", "contents:write", "pull-requests:write"],
439
+ tokenPersistence: "runtime-only",
440
+ },
441
+ release: {
442
+ protectedReviewRequired: true,
443
+ versionState: "not-required",
444
+ identityOnlyPullRequests: false,
445
+ manualVersionStateRepairPullRequests: false,
446
+ },
447
+ roles: {
448
+ actor: "repository-development-role",
449
+ pusher: "repository-development-role",
450
+ reviewer: "independent-review-role",
451
+ generatedWriteAuthority: "github-app-or-narrow-token",
452
+ },
453
+ };
454
+ }
455
+
456
+ export function createPaperProvisioningAuthority({
457
+ repository,
458
+ packageName,
459
+ buildchainVersion,
460
+ buildchainSha,
461
+ contractLock,
462
+ buildWorkflow,
463
+ releaseWorkflow,
464
+ environment = "",
465
+ }) {
466
+ const policy = paperProvisioningPolicy();
467
+ const payload = {
468
+ schemaVersion: 1,
469
+ contract: PAPER_PROVISIONING_CONTRACT,
470
+ repository,
471
+ package: {
472
+ name: packageName,
473
+ registry: NPM_REGISTRY,
474
+ bootstrapVersion: DEFAULT_BOOTSTRAP_VERSION,
475
+ },
476
+ runtime: {
477
+ repository: "kungfu-systems/buildchain",
478
+ version: buildchainVersion,
479
+ ref: buildchainSha,
480
+ resolvedSha: buildchainSha,
481
+ },
482
+ workflows: {
483
+ build: {
484
+ path: PAPER_PATHS.buildWorkflow,
485
+ sourceDigest: sha256Text(buildWorkflow),
486
+ reusablePath: ".github/workflows/publication-artifact.yml",
487
+ reusableRef: buildchainSha,
488
+ },
489
+ release: {
490
+ path: PAPER_PATHS.releaseWorkflow,
491
+ sourceDigest: sha256Text(releaseWorkflow),
492
+ reusablePath: ".github/workflows/paper-release-sealed.yml",
493
+ reusableRef: buildchainSha,
494
+ },
495
+ },
496
+ admission: {
497
+ contractLockPath: PAPER_PATHS.contractLock,
498
+ contractLockDigest: sha256Text(contractLock),
499
+ acceptedRef: buildchainSha,
500
+ acceptedSha: buildchainSha,
501
+ },
502
+ trustedPublisher: {
503
+ type: "github",
504
+ repository,
505
+ workflow: path.posix.basename(PAPER_PATHS.releaseWorkflow),
506
+ environment,
507
+ },
508
+ policy: {
509
+ ...policy,
510
+ policyDigest: sha256Text(stableJson(policy)),
511
+ },
512
+ };
513
+ return {
514
+ ...payload,
515
+ authorityDigest: sha256Text(stableJson(payload)),
516
+ };
517
+ }
518
+
519
+ function scaffoldMakefile() {
520
+ const image = `${DEFAULT_TOOLCHAIN_IMAGE}@${DEFAULT_TOOLCHAIN_DIGEST}`;
521
+ return `.PHONY: check pdf clean
522
+
523
+ BUILDER_IMAGE := ${image}
524
+ SOURCE_DATE_EPOCH ?= 0
525
+
526
+ check:
527
+ \t@test -f paper/main.tex
528
+ \t@test -f paper/references.bib
529
+ \t@git diff --check
530
+
531
+ pdf:
532
+ \t@mkdir -p _build
533
+ \tdocker run --rm --network=none \\
534
+ \t\t-e SOURCE_DATE_EPOCH="$(SOURCE_DATE_EPOCH)" \\
535
+ \t\t-e TZ=UTC -e LANG=C.UTF-8 -e LC_ALL=C.UTF-8 -e HOME=/tmp \\
536
+ \t\t-v "$(CURDIR):/workspace" -w /workspace \\
537
+ \t\t"$(BUILDER_IMAGE)" bash -lc '${DEFAULT_TOOLCHAIN_COMMAND}'
538
+
539
+ clean:
540
+ \trm -rf _build
541
+ `;
542
+ }
543
+
544
+ function scaffoldPackageJson({ name, packageName, repository }) {
545
+ return jsonText({
546
+ name: packageName,
547
+ private: true,
548
+ description: `${name} publication source repository.`,
549
+ repository: {
550
+ type: "git",
551
+ url: `git+https://github.com/${repository}.git`,
552
+ },
553
+ license: "Apache-2.0",
554
+ });
555
+ }
556
+
557
+ function scaffoldReadme({ title, packageName }) {
558
+ return `# ${title}
559
+
560
+ This repository is a Buildchain-governed publication artifact source.
561
+
562
+ ## Local workflow
563
+
564
+ \`\`\`sh
565
+ buildchain paper preflight --json
566
+ buildchain paper build
567
+ buildchain paper status --json
568
+ \`\`\`
569
+
570
+ The public package identity is \`${packageName}\`. Buildchain owns reproducible
571
+ artifact generation, sealed publication, npm Trusted Publishing, and recovery;
572
+ this repository owns the paper source and review history.
573
+
574
+ See [docs/MAP.md](docs/MAP.md) for the repository map.
575
+ `;
576
+ }
577
+
578
+ function scaffoldMap() {
579
+ return `# Repository Map
580
+
581
+ - \`paper/main.tex\`: paper source entrypoint.
582
+ - \`paper/references.bib\`: bibliography source.
583
+ - \`.buildchain/buildchain.toml\`: publication identity, toolchain, package, and lifecycle contract.
584
+ - \`.buildchain/contract-lock.json\`: accepted Buildchain runtime contract.
585
+ - \`.github/workflows/build.yml\`: thin read-only build and reproducibility caller.
586
+ - \`.github/workflows/paper-release.yml\`: thin protected sealed-release caller.
587
+
588
+ The repository does not own npm transaction logic, publication authority,
589
+ release-state recovery, or site deployment mechanics. Those remain Buildchain,
590
+ npm/GitHub, and downstream site responsibilities respectively.
591
+ `;
592
+ }
593
+
594
+ function scaffoldMainTex(title) {
595
+ return `\\documentclass[11pt]{article}
596
+ \\usepackage[T1]{fontenc}
597
+ \\usepackage{lmodern}
598
+ \\usepackage{hyperref}
599
+
600
+ \\title{${texEscape(title)}}
601
+ \\author{}
602
+ \\date{}
603
+
604
+ \\begin{document}
605
+ \\maketitle
606
+
607
+ \\begin{abstract}
608
+ Replace this paragraph with the paper abstract.
609
+ \\end{abstract}
610
+
611
+ \\section{Introduction}
612
+ Replace this section with the reviewed paper content.
613
+
614
+ \\bibliographystyle{plain}
615
+ \\bibliography{paper/references}
616
+ \\end{document}
617
+ `;
618
+ }
619
+
620
+ function scaffoldFiles({
621
+ buildchainRoot,
622
+ buildchainVersion,
623
+ buildchainRef,
624
+ buildchainSha,
625
+ cwd,
626
+ name,
627
+ title,
628
+ packageName,
629
+ repository,
630
+ version,
631
+ siteBaseUrl,
632
+ }) {
633
+ const contractWorld = runtimeContractWorld(buildchainRoot);
634
+ const existingLock = readJson(
635
+ path.resolve(cwd, PAPER_PATHS.contractLock),
636
+ ).value;
637
+ const acceptedAt =
638
+ existingLock?.buildchain?.acceptedAt ||
639
+ runtimeAcceptedAt(buildchainRoot, buildchainSha);
640
+ const contractLock = createBuildchainContractLock({
641
+ buildchainRef: buildchainSha,
642
+ resolvedSha: buildchainSha,
643
+ contractWorld,
644
+ acceptedAt,
645
+ });
646
+ const contractLockText = jsonText(contractLock);
647
+ const buildWorkflow = scaffoldBuildWorkflow(buildchainSha);
648
+ const releaseWorkflow = scaffoldReleaseWorkflow(buildchainSha);
649
+ const provisioningAuthority = createPaperProvisioningAuthority({
650
+ repository,
651
+ packageName,
652
+ buildchainVersion,
653
+ buildchainSha,
654
+ contractLock: contractLockText,
655
+ buildWorkflow,
656
+ releaseWorkflow,
657
+ });
658
+ const licenseText = runtimeLicenseText(buildchainRoot);
659
+ return new Map([
660
+ [
661
+ PAPER_PATHS.config,
662
+ scaffoldConfig({
663
+ name,
664
+ title,
665
+ packageName,
666
+ version,
667
+ siteBaseUrl,
668
+ }),
669
+ ],
670
+ [PAPER_PATHS.contractLock, contractLockText],
671
+ [PAPER_PATHS.versionPin, `${buildchainVersion}\n`],
672
+ [PAPER_PATHS.buildWorkflow, buildWorkflow],
673
+ [PAPER_PATHS.releaseWorkflow, releaseWorkflow],
674
+ [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
675
+ ["Makefile", scaffoldMakefile()],
676
+ ["package.json", scaffoldPackageJson({ name, packageName, repository })],
677
+ ["README.md", scaffoldReadme({ title, packageName })],
678
+ ["docs/MAP.md", scaffoldMap()],
679
+ ["paper/main.tex", scaffoldMainTex(title)],
680
+ ["paper/references.bib", "% Add reviewed bibliography entries here.\n"],
681
+ ["LICENSE", licenseText],
682
+ [
683
+ ".gitignore",
684
+ "_build/\n.buildchain/publication/\n.buildchain/release-state/\n.buildchain/release-evidence/\n.buildchain/paper/npm-bootstrap.json\n.buildchain/paper/npm-trust.json\n",
685
+ ],
686
+ ]);
687
+ }
688
+
689
+ export function planPaperScaffold({
690
+ cwd = process.cwd(),
691
+ buildchainRoot = process.cwd(),
692
+ buildchainVersion = "",
693
+ buildchainRef = "v3",
694
+ buildchainSha = "",
695
+ name = path.basename(path.resolve(cwd)),
696
+ title = "",
697
+ packageName = "",
698
+ repository = "",
699
+ version = "0.1.0-alpha.0",
700
+ siteBaseUrl = "",
701
+ } = {}) {
702
+ const resolvedCwd = path.resolve(cwd);
703
+ const normalizedName = String(name || "").trim();
704
+ if (!normalizedName) throw new Error("paper scaffold requires --name");
705
+ const normalizedPackage = normalizePackageName(packageName);
706
+ const normalizedRepository = normalizeRepository(repository);
707
+ if (!normalizedRepository) {
708
+ throw new Error("paper scaffold requires --repository <owner/repo>");
709
+ }
710
+ const normalizedVersion = String(version || "")
711
+ .trim()
712
+ .replace(/^v/, "");
713
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(normalizedVersion)) {
714
+ throw new Error("paper scaffold --version must be semver");
715
+ }
716
+ const runtimeIdentity = buildchainPackageIdentity(
717
+ buildchainRoot,
718
+ buildchainVersion,
719
+ );
720
+ const runtimeSha =
721
+ buildchainSha || runtimeGitSha(buildchainRoot, runtimeIdentity.version);
722
+ if (!GIT_SHA_PATTERN.test(runtimeSha)) {
723
+ throw new Error(
724
+ "paper scaffold cannot resolve the exact Buildchain runtime SHA; pass buildchainSha or use a published Buildchain package with npm gitHead provenance",
725
+ );
726
+ }
727
+ const files = scaffoldFiles({
728
+ buildchainRoot,
729
+ buildchainVersion: runtimeIdentity.version,
730
+ buildchainRef,
731
+ buildchainSha: runtimeSha,
732
+ cwd: resolvedCwd,
733
+ name: normalizedName,
734
+ title: title || normalizedName,
735
+ packageName: normalizedPackage,
736
+ repository: normalizedRepository,
737
+ version: normalizedVersion,
738
+ siteBaseUrl,
739
+ });
740
+ const changes = [...files.entries()].map(([relativePath, content]) => {
741
+ const filePath = path.resolve(resolvedCwd, relativePath);
742
+ const exists = fs.existsSync(filePath);
743
+ if (!exists) {
744
+ return {
745
+ path: relativePath,
746
+ action: "create",
747
+ sha256: sha256Text(content),
748
+ content,
749
+ };
750
+ }
751
+ if (!fs.statSync(filePath).isFile()) {
752
+ return {
753
+ path: relativePath,
754
+ action: "conflict",
755
+ reason: "path-exists-and-is-not-a-file",
756
+ sha256: sha256Text(content),
757
+ content,
758
+ };
759
+ }
760
+ const current = fs.readFileSync(filePath, "utf8");
761
+ return current === content
762
+ ? {
763
+ path: relativePath,
764
+ action: "unchanged",
765
+ sha256: sha256Text(content),
766
+ content,
767
+ }
768
+ : {
769
+ path: relativePath,
770
+ action: "conflict",
771
+ reason: "existing-content-differs",
772
+ currentSha256: sha256Text(current),
773
+ sha256: sha256Text(content),
774
+ content,
775
+ };
776
+ });
777
+ const publicChanges = changes.map(({ content: _content, ...entry }) => entry);
778
+ const conflicts = publicChanges.filter(
779
+ (entry) => entry.action === "conflict",
780
+ );
781
+ const result = {
782
+ schemaVersion: 1,
783
+ contract: PAPER_SCAFFOLD_CONTRACT,
784
+ ok: conflicts.length === 0,
785
+ cwd: resolvedCwd,
786
+ dryRun: true,
787
+ identity: {
788
+ project: normalizedName,
789
+ title: title || normalizedName,
790
+ package: normalizedPackage,
791
+ repository: normalizedRepository,
792
+ version: normalizedVersion,
793
+ },
794
+ buildchain: {
795
+ version: runtimeIdentity.version,
796
+ ref: runtimeSha,
797
+ resolvedSha: runtimeSha,
798
+ },
799
+ summary: {
800
+ create: publicChanges.filter((entry) => entry.action === "create").length,
801
+ unchanged: publicChanges.filter((entry) => entry.action === "unchanged")
802
+ .length,
803
+ conflict: conflicts.length,
804
+ },
805
+ changes: publicChanges,
806
+ conflicts,
807
+ nextActions:
808
+ conflicts.length > 0
809
+ ? [
810
+ {
811
+ id: "resolve-scaffold-conflicts",
812
+ command: "",
813
+ description:
814
+ "Resolve the listed semantic conflicts; scaffold never overwrites them.",
815
+ },
816
+ ]
817
+ : [
818
+ {
819
+ id: "write-scaffold",
820
+ command: "buildchain paper scaffold --write <same arguments>",
821
+ description:
822
+ "Write only missing files after the conflict-free plan is reviewed.",
823
+ },
824
+ ],
825
+ };
826
+ Object.defineProperty(result, "_plannedFiles", {
827
+ value: changes,
828
+ enumerable: false,
829
+ });
830
+ return result;
831
+ }
832
+
833
+ export function writePaperScaffold(plan) {
834
+ if (!plan || plan.contract !== PAPER_SCAFFOLD_CONTRACT) {
835
+ throw new Error("paper scaffold plan contract mismatch");
836
+ }
837
+ if (!plan.ok || plan.conflicts.length > 0) {
838
+ return {
839
+ ...plan,
840
+ dryRun: false,
841
+ written: [],
842
+ ok: false,
843
+ errorCode: "paper-scaffold-conflict",
844
+ };
845
+ }
846
+ const resolvedCwd = path.resolve(plan.cwd);
847
+ const creates = plan._plannedFiles.filter(
848
+ (entry) => entry.action === "create",
849
+ );
850
+ for (const entry of creates) {
851
+ const target = path.resolve(resolvedCwd, entry.path);
852
+ if (fs.existsSync(target)) {
853
+ const current = fs.statSync(target).isFile()
854
+ ? fs.readFileSync(target, "utf8")
855
+ : undefined;
856
+ if (current !== entry.content) {
857
+ throw new Error(
858
+ `paper scaffold race detected at ${entry.path}; no file was overwritten`,
859
+ );
860
+ }
861
+ continue;
862
+ }
863
+ }
864
+ const written = [];
865
+ for (const entry of creates) {
866
+ const target = path.resolve(resolvedCwd, entry.path);
867
+ if (fs.existsSync(target)) continue;
868
+ fs.mkdirSync(path.dirname(target), { recursive: true });
869
+ fs.writeFileSync(target, entry.content, { flag: "wx" });
870
+ written.push(entry.path);
871
+ }
872
+ return {
873
+ ...plan,
874
+ ok: true,
875
+ dryRun: false,
876
+ written,
877
+ idempotent: written.length === 0,
878
+ nextActions: [
879
+ {
880
+ id: "paper-preflight",
881
+ command: `buildchain paper preflight --cwd ${JSON.stringify(resolvedCwd)} --json`,
882
+ description:
883
+ "Verify the generated repository before any external mutation.",
884
+ },
885
+ ],
886
+ };
887
+ }
888
+
889
+ function migrationFiles({
890
+ cwd,
891
+ buildchainRoot,
892
+ buildchainVersion,
893
+ buildchainSha,
894
+ }) {
895
+ const configResult = paperConfig(cwd);
896
+ if (configResult.error) {
897
+ throw new Error(
898
+ `paper migration requires a valid publication config: ${configResult.error}`,
899
+ );
900
+ }
901
+ validateBuildchainConfig(cwd, { requireLifecycleStages: ["verify"] });
902
+ const config = configResult.loaded.config;
903
+ const repository = resolvePaperRepository(cwd);
904
+ if (!repository) {
905
+ throw new Error(
906
+ "paper migration requires an exact GitHub repository identity",
907
+ );
908
+ }
909
+ const packageName = normalizePackageName(
910
+ config.publish?.package || config.publish?.mainPackage || "",
911
+ "paper publish package",
912
+ );
913
+ const runtimeSha =
914
+ buildchainSha || runtimeGitSha(buildchainRoot, buildchainVersion);
915
+ if (!GIT_SHA_PATTERN.test(runtimeSha)) {
916
+ throw new Error("paper migration requires an exact Buildchain source SHA");
917
+ }
918
+ const runtimeIdentity = buildchainPackageIdentity(
919
+ buildchainRoot,
920
+ buildchainVersion,
921
+ );
922
+ if (!runtimeIdentity.version) {
923
+ throw new Error(
924
+ "paper migration requires an exact Buildchain package version",
925
+ );
926
+ }
927
+ const existingLock = readJson(
928
+ path.resolve(cwd, PAPER_PATHS.contractLock),
929
+ ).value;
930
+ const contractLock = createBuildchainContractLock({
931
+ buildchainRef: runtimeSha,
932
+ resolvedSha: runtimeSha,
933
+ contractWorld: runtimeContractWorld(buildchainRoot),
934
+ acceptedAt:
935
+ existingLock?.buildchain?.resolvedSha === runtimeSha
936
+ ? existingLock.buildchain.acceptedAt
937
+ : runtimeAcceptedAt(buildchainRoot, runtimeSha),
938
+ });
939
+ const contractLockText = jsonText(contractLock);
940
+ const buildWorkflow = scaffoldBuildWorkflow(runtimeSha, {
941
+ artifactName: config.project.name,
942
+ });
943
+ const releaseWorkflow = scaffoldReleaseWorkflow(runtimeSha, {
944
+ artifactPaths: config.publication.artifactPaths.join(","),
945
+ releasePassportProductName: config.publication.title,
946
+ });
947
+ const provisioningAuthority = createPaperProvisioningAuthority({
948
+ repository,
949
+ packageName,
950
+ buildchainVersion: runtimeIdentity.version,
951
+ buildchainSha: runtimeSha,
952
+ contractLock: contractLockText,
953
+ buildWorkflow,
954
+ releaseWorkflow,
955
+ });
956
+ return new Map([
957
+ [PAPER_PATHS.contractLock, contractLockText],
958
+ [PAPER_PATHS.versionPin, `${runtimeIdentity.version}\n`],
959
+ [PAPER_PATHS.buildWorkflow, buildWorkflow],
960
+ [PAPER_PATHS.releaseWorkflow, releaseWorkflow],
961
+ [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
962
+ ]);
963
+ }
964
+
965
+ export function planPaperMigration({
966
+ cwd = process.cwd(),
967
+ buildchainRoot = process.cwd(),
968
+ buildchainVersion = "",
969
+ buildchainSha = "",
970
+ } = {}) {
971
+ const resolvedCwd = path.resolve(cwd);
972
+ const repositoryRoot = gitValue(resolvedCwd, [
973
+ "rev-parse",
974
+ "--show-toplevel",
975
+ ]);
976
+ if (
977
+ !repositoryRoot ||
978
+ fs.realpathSync(repositoryRoot) !== fs.realpathSync(resolvedCwd)
979
+ ) {
980
+ throw new Error("paper migration must target the exact repository root");
981
+ }
982
+ const source = {
983
+ head: gitValue(resolvedCwd, ["rev-parse", "HEAD"]),
984
+ clean: gitResult(resolvedCwd, ["status", "--porcelain"]).stdout === "",
985
+ };
986
+ if (!GIT_SHA_PATTERN.test(source.head)) {
987
+ throw new Error("paper migration requires a committed Git source");
988
+ }
989
+ const plannedFiles = [
990
+ ...migrationFiles({
991
+ cwd: resolvedCwd,
992
+ buildchainRoot,
993
+ buildchainVersion,
994
+ buildchainSha,
995
+ }),
996
+ ].map(([relativePath, content]) => {
997
+ const target = path.resolve(resolvedCwd, relativePath);
998
+ if (fs.existsSync(target) && !fs.statSync(target).isFile()) {
999
+ return {
1000
+ path: relativePath,
1001
+ action: "conflict",
1002
+ currentSha256: "",
1003
+ sha256: sha256Text(content),
1004
+ content,
1005
+ };
1006
+ }
1007
+ const current = fs.existsSync(target)
1008
+ ? fs.readFileSync(target, "utf8")
1009
+ : undefined;
1010
+ return {
1011
+ path: relativePath,
1012
+ action:
1013
+ current === undefined
1014
+ ? "create"
1015
+ : current === content
1016
+ ? "unchanged"
1017
+ : "update",
1018
+ currentSha256: current === undefined ? "" : sha256Text(current),
1019
+ sha256: sha256Text(content),
1020
+ content,
1021
+ };
1022
+ });
1023
+ const changes = plannedFiles.map(({ content: _content, ...entry }) => entry);
1024
+ const conflicts = changes.filter((entry) => entry.action === "conflict");
1025
+ const ok = source.clean && conflicts.length === 0;
1026
+ const result = {
1027
+ schemaVersion: 1,
1028
+ contract: PAPER_MIGRATION_CONTRACT,
1029
+ ok,
1030
+ cwd: resolvedCwd,
1031
+ dryRun: true,
1032
+ source,
1033
+ summary: {
1034
+ create: changes.filter((entry) => entry.action === "create").length,
1035
+ update: changes.filter((entry) => entry.action === "update").length,
1036
+ unchanged: changes.filter((entry) => entry.action === "unchanged").length,
1037
+ conflict: conflicts.length,
1038
+ },
1039
+ changes,
1040
+ conflicts,
1041
+ nextActions: !source.clean
1042
+ ? [
1043
+ {
1044
+ id: "commit-source",
1045
+ command: "git status --short",
1046
+ description:
1047
+ "Migration only rewrites Buildchain-owned control files from a clean committed source.",
1048
+ },
1049
+ ]
1050
+ : conflicts.length > 0
1051
+ ? [
1052
+ {
1053
+ id: "resolve-migration-conflicts",
1054
+ command: "",
1055
+ description:
1056
+ "Resolve non-file targets; migration never replaces a directory or special path.",
1057
+ },
1058
+ ]
1059
+ : [
1060
+ {
1061
+ id: "write-migration",
1062
+ command: "buildchain paper migrate --write --json",
1063
+ description:
1064
+ "Write the reviewed Buildchain-owned control files without changing paper content or publication configuration.",
1065
+ },
1066
+ ],
1067
+ };
1068
+ Object.defineProperty(result, "_plannedFiles", {
1069
+ value: plannedFiles,
1070
+ enumerable: false,
1071
+ });
1072
+ return result;
1073
+ }
1074
+
1075
+ export function writePaperMigration(plan) {
1076
+ if (!plan || plan.contract !== PAPER_MIGRATION_CONTRACT) {
1077
+ throw new Error("paper migration plan contract mismatch");
1078
+ }
1079
+ if (!plan.ok) {
1080
+ return {
1081
+ ...plan,
1082
+ dryRun: false,
1083
+ written: [],
1084
+ updated: [],
1085
+ ok: false,
1086
+ errorCode: "paper-migration-blocked",
1087
+ };
1088
+ }
1089
+ const written = [];
1090
+ const updated = [];
1091
+ for (const entry of plan._plannedFiles) {
1092
+ if (entry.action === "unchanged") continue;
1093
+ const target = path.resolve(plan.cwd, entry.path);
1094
+ const exists = fs.existsSync(target);
1095
+ const current =
1096
+ exists && fs.statSync(target).isFile()
1097
+ ? fs.readFileSync(target, "utf8")
1098
+ : undefined;
1099
+ const currentSha256 = current === undefined ? "" : sha256Text(current);
1100
+ if (
1101
+ (entry.action === "create" && exists) ||
1102
+ (entry.action === "update" && currentSha256 !== entry.currentSha256)
1103
+ ) {
1104
+ throw new Error(
1105
+ `paper migration race detected at ${entry.path}; no stale plan was applied`,
1106
+ );
1107
+ }
1108
+ }
1109
+ for (const entry of plan._plannedFiles) {
1110
+ if (entry.action === "unchanged") continue;
1111
+ const target = path.resolve(plan.cwd, entry.path);
1112
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1113
+ fs.writeFileSync(target, entry.content, {
1114
+ flag: entry.action === "create" ? "wx" : "w",
1115
+ });
1116
+ (entry.action === "create" ? written : updated).push(entry.path);
1117
+ }
1118
+ return {
1119
+ ...plan,
1120
+ ok: true,
1121
+ dryRun: false,
1122
+ written,
1123
+ updated,
1124
+ idempotent: written.length === 0 && updated.length === 0,
1125
+ nextActions: [
1126
+ {
1127
+ id: "paper-preflight",
1128
+ command: `buildchain paper preflight --cwd ${JSON.stringify(plan.cwd)} --offline --json`,
1129
+ description:
1130
+ "Verify the migrated repository before any external mutation.",
1131
+ },
1132
+ ],
1133
+ };
1134
+ }
1135
+
1136
+ function paperConfig(cwd) {
1137
+ const loaded = loadBuildchainConfig(cwd);
1138
+ if (!loaded) {
1139
+ return {
1140
+ loaded: undefined,
1141
+ error: `${PAPER_PATHS.config} is missing`,
1142
+ };
1143
+ }
1144
+ if (loaded.config.project?.type !== "publication-artifact") {
1145
+ return {
1146
+ loaded,
1147
+ error: 'project.type must be "publication-artifact"',
1148
+ };
1149
+ }
1150
+ if (!loaded.config.publication) {
1151
+ return { loaded, error: "[publication] is missing" };
1152
+ }
1153
+ return { loaded, error: "" };
1154
+ }
1155
+
1156
+ function state(id, status, reason, evidence = []) {
1157
+ return {
1158
+ id,
1159
+ status,
1160
+ satisfied: status === "satisfied",
1161
+ reason,
1162
+ evidence: evidence.filter(Boolean),
1163
+ };
1164
+ }
1165
+
1166
+ function transactionCandidates(cwd, version) {
1167
+ const stateDir = path.resolve(cwd, ".buildchain/release-state");
1168
+ if (!fs.existsSync(stateDir) || !fs.statSync(stateDir).isDirectory()) {
1169
+ return [];
1170
+ }
1171
+ const preferred = new Set([`${version}.json`, `v${version}.json`]);
1172
+ return fs
1173
+ .readdirSync(stateDir)
1174
+ .filter((entry) => entry.endsWith(".json"))
1175
+ .sort((left, right) => {
1176
+ const leftRank = preferred.has(left) ? 0 : 1;
1177
+ const rightRank = preferred.has(right) ? 0 : 1;
1178
+ return leftRank - rightRank || left.localeCompare(right);
1179
+ })
1180
+ .map((entry) => {
1181
+ const filePath = path.join(stateDir, entry);
1182
+ try {
1183
+ const transaction = readReleaseTransaction(filePath);
1184
+ return {
1185
+ path: toPosix(path.relative(cwd, filePath)),
1186
+ transaction,
1187
+ error: "",
1188
+ };
1189
+ } catch (error) {
1190
+ return {
1191
+ path: toPosix(path.relative(cwd, filePath)),
1192
+ transaction: undefined,
1193
+ error: error.message,
1194
+ };
1195
+ }
1196
+ });
1197
+ }
1198
+
1199
+ function explicitVisibilityState(value, channel) {
1200
+ const entry = value?.channels?.[channel];
1201
+ return Boolean(
1202
+ value?.contract === PAPER_VISIBILITY_CONTRACT &&
1203
+ entry?.status === "visible" &&
1204
+ typeof entry.url === "string" &&
1205
+ entry.url &&
1206
+ SHA256_PATTERN.test(String(entry.evidenceDigest || "")),
1207
+ );
1208
+ }
1209
+
1210
+ function admissionSatisfied(value) {
1211
+ const digestPattern = /^(?:sha256:)?[0-9a-f]{64}$/i;
1212
+ if (value?.contract === "kungfu-buildchain-publication-admission") {
1213
+ return digestPattern.test(String(value.admissionDigest || ""));
1214
+ }
1215
+ if (value?.contract === "kungfu-buildchain-publication-capability") {
1216
+ return (
1217
+ value.decision === "allow" &&
1218
+ digestPattern.test(String(value.capabilityDigest || ""))
1219
+ );
1220
+ }
1221
+ return false;
1222
+ }
1223
+
1224
+ function bootstrapSatisfied(value) {
1225
+ return Boolean(
1226
+ value?.contract === PAPER_NPM_BOOTSTRAP_CONTRACT &&
1227
+ ["existing", "published"].includes(value?.publish?.status) &&
1228
+ value?.package?.name,
1229
+ );
1230
+ }
1231
+
1232
+ function trustSatisfied(value) {
1233
+ return Boolean(
1234
+ value?.contract === PAPER_NPM_BOOTSTRAP_CONTRACT &&
1235
+ value?.trust?.status === "configured",
1236
+ );
1237
+ }
1238
+
1239
+ function contentReadiness(cwd, publication) {
1240
+ const required = [...publication.sourcePaths, ...publication.metadataPaths];
1241
+ const missing = required.filter(
1242
+ (entry) => !fs.existsSync(path.resolve(cwd, entry)),
1243
+ );
1244
+ return {
1245
+ ok: required.length > 0 && missing.length === 0,
1246
+ required,
1247
+ missing,
1248
+ };
1249
+ }
1250
+
1251
+ function releaseTransactionFacts(candidates, version) {
1252
+ const matching = candidates.filter(({ transaction }) => {
1253
+ if (!transaction) return false;
1254
+ return (
1255
+ String(transaction.version || "").replace(/^v/, "") ===
1256
+ String(version || "").replace(/^v/, "")
1257
+ );
1258
+ });
1259
+ const selected =
1260
+ matching[0] ||
1261
+ candidates.find(({ transaction }) => transaction) ||
1262
+ undefined;
1263
+ const publicationState = selected?.transaction
1264
+ ? releaseTransactionPublicationState(selected.transaction)
1265
+ : "";
1266
+ return {
1267
+ matching,
1268
+ selected,
1269
+ publicationState,
1270
+ };
1271
+ }
1272
+
1273
+ export function collectPaperStatus({ cwd = process.cwd() } = {}) {
1274
+ const resolvedCwd = path.resolve(cwd);
1275
+ const configResult = paperConfig(resolvedCwd);
1276
+ const loaded = configResult.loaded;
1277
+ const publication = loaded?.config?.publication;
1278
+ const publish = loaded?.config?.publish;
1279
+ const packageName = publish?.package || publish?.mainPackage || "";
1280
+ const version = publication?.version || "";
1281
+ const configFact = existingFileFact(
1282
+ resolvedCwd,
1283
+ loaded?.path || PAPER_PATHS.config,
1284
+ );
1285
+ const scaffoldFacts = PAPER_SCAFFOLD_PATHS.map((relativePath) =>
1286
+ existingFileFact(resolvedCwd, relativePath),
1287
+ );
1288
+ const contractLockPath = path.resolve(resolvedCwd, PAPER_PATHS.contractLock);
1289
+ const contractLockJson = readJson(contractLockPath);
1290
+ let contractLock;
1291
+ let contractLockError = contractLockJson.error;
1292
+ if (contractLockJson.exists && !contractLockError) {
1293
+ try {
1294
+ contractLock = readBuildchainContractLock(contractLockPath);
1295
+ } catch (error) {
1296
+ contractLockError = error.message;
1297
+ }
1298
+ }
1299
+ const gitRepo = gitResult(resolvedCwd, [
1300
+ "rev-parse",
1301
+ "--is-inside-work-tree",
1302
+ ]);
1303
+ const scaffoldOk = Boolean(
1304
+ !configResult.error && scaffoldFacts.every(Boolean),
1305
+ );
1306
+ const governedOk = Boolean(
1307
+ scaffoldOk &&
1308
+ contractLock &&
1309
+ GIT_SHA_PATTERN.test(contractLock.buildchain?.resolvedSha || "") &&
1310
+ SHA256_PATTERN.test(contractLock.buildchain?.contractDigest || "") &&
1311
+ SHA256_PATTERN.test(contractLock.buildchain?.compatibilityDigest || "") &&
1312
+ gitRepo.ok &&
1313
+ gitRepo.stdout === "true",
1314
+ );
1315
+
1316
+ const admissionInputs = [PAPER_PATHS.capability, PAPER_PATHS.admission].map(
1317
+ (relativePath) => ({
1318
+ relativePath,
1319
+ ...readJson(path.resolve(resolvedCwd, relativePath)),
1320
+ }),
1321
+ );
1322
+ const admitted = admissionInputs.find((entry) =>
1323
+ admissionSatisfied(entry.value),
1324
+ );
1325
+ const bootstrap = readJson(
1326
+ path.resolve(resolvedCwd, PAPER_PATHS.npmBootstrap),
1327
+ );
1328
+ const trust = readJson(path.resolve(resolvedCwd, PAPER_PATHS.npmTrust));
1329
+ const bootstrapValue = bootstrap.value;
1330
+ const trustValue = trust.value || bootstrap.value;
1331
+ const content = publication
1332
+ ? contentReadiness(resolvedCwd, publication)
1333
+ : { ok: false, required: [], missing: [] };
1334
+ const reproducibility = readJson(
1335
+ path.resolve(resolvedCwd, PAPER_PATHS.reproducibilityReceipt),
1336
+ );
1337
+ const deterministic = Boolean(
1338
+ reproducibility.value?.contract ===
1339
+ PUBLICATION_REPRODUCIBILITY_RECEIPT_CONTRACT &&
1340
+ reproducibility.value?.status === "passed" &&
1341
+ reproducibility.value?.qualifying === true,
1342
+ );
1343
+ const sealed = readJson(path.resolve(resolvedCwd, PAPER_PATHS.sealedBundle));
1344
+ let sealedOk = false;
1345
+ let sealedError = sealed.error;
1346
+ if (sealed.value?.contract === PUBLICATION_SEALED_BUNDLE_CONTRACT) {
1347
+ try {
1348
+ verifyPublicationSealedBundle({
1349
+ bundleRoot: resolvedCwd,
1350
+ manifest: sealed.value,
1351
+ });
1352
+ sealedOk = true;
1353
+ } catch (error) {
1354
+ sealedError = error.message;
1355
+ }
1356
+ }
1357
+ const candidates = transactionCandidates(resolvedCwd, version);
1358
+ const transactionFacts = releaseTransactionFacts(candidates, version);
1359
+ const packagePublished = [
1360
+ "package-published",
1361
+ "alpha-complete",
1362
+ "release-complete",
1363
+ ].includes(transactionFacts.publicationState);
1364
+ const alphaComplete = transactionFacts.publicationState === "alpha-complete";
1365
+ const visibility = readJson(
1366
+ path.resolve(resolvedCwd, PAPER_PATHS.visibility),
1367
+ );
1368
+ const stagingVisible = explicitVisibilityState(visibility.value, "staging");
1369
+ const productionVisible = explicitVisibilityState(
1370
+ visibility.value,
1371
+ "production",
1372
+ );
1373
+
1374
+ const states = [
1375
+ state(
1376
+ "scaffolded",
1377
+ scaffoldOk ? "satisfied" : configFact ? "blocked" : "not-reached",
1378
+ scaffoldOk
1379
+ ? "The complete managed paper scaffold inventory is present."
1380
+ : configResult.error || "Paper scaffold is incomplete.",
1381
+ scaffoldFacts,
1382
+ ),
1383
+ state(
1384
+ "governed",
1385
+ governedOk ? "satisfied" : scaffoldOk ? "blocked" : "not-reached",
1386
+ governedOk
1387
+ ? "The repository is Git-governed and carries a valid Buildchain contract lock."
1388
+ : contractLockError ||
1389
+ "Git governance or the Buildchain contract lock is missing.",
1390
+ [existingFileFact(resolvedCwd, PAPER_PATHS.contractLock)],
1391
+ ),
1392
+ state(
1393
+ "admitted",
1394
+ admitted ? "satisfied" : "not-reached",
1395
+ admitted
1396
+ ? "Explicit publication admission/capability evidence is present."
1397
+ : "No explicit publication admission evidence is present; no admission is inferred.",
1398
+ admitted ? [existingFileFact(resolvedCwd, admitted.relativePath)] : [],
1399
+ ),
1400
+ state(
1401
+ "bootstrapped",
1402
+ bootstrapSatisfied(bootstrapValue) ? "satisfied" : "not-reached",
1403
+ bootstrapSatisfied(bootstrapValue)
1404
+ ? "A typed npm package bootstrap receipt exists."
1405
+ : "No typed npm bootstrap receipt proves that the public package exists.",
1406
+ [existingFileFact(resolvedCwd, PAPER_PATHS.npmBootstrap)],
1407
+ ),
1408
+ state(
1409
+ "trust-bound",
1410
+ trustSatisfied(trustValue) ? "satisfied" : "not-reached",
1411
+ trustSatisfied(trustValue)
1412
+ ? "A typed receipt proves npm Trusted Publishing is configured."
1413
+ : "No typed receipt proves the npm Trusted Publisher binding.",
1414
+ [
1415
+ existingFileFact(resolvedCwd, PAPER_PATHS.npmTrust),
1416
+ existingFileFact(resolvedCwd, PAPER_PATHS.npmBootstrap),
1417
+ ],
1418
+ ),
1419
+ state(
1420
+ "content-ready",
1421
+ content.ok ? "satisfied" : publication ? "blocked" : "not-reached",
1422
+ content.ok
1423
+ ? "Every declared source and metadata input exists."
1424
+ : `Declared content is missing: ${content.missing.join(", ") || "publication config unavailable"}.`,
1425
+ content.required.map((entry) => existingFileFact(resolvedCwd, entry)),
1426
+ ),
1427
+ state(
1428
+ "artifact-sealed",
1429
+ deterministic && sealedOk ? "satisfied" : "not-reached",
1430
+ deterministic && sealedOk
1431
+ ? "A qualifying reproducibility receipt and verified sealed bundle bind exact artifact bytes."
1432
+ : sealedError ||
1433
+ "Artifact sealing requires both qualifying reproducibility and an exact sealed bundle; neither is inferred.",
1434
+ [
1435
+ existingFileFact(resolvedCwd, PAPER_PATHS.reproducibilityReceipt),
1436
+ existingFileFact(resolvedCwd, PAPER_PATHS.sealedBundle),
1437
+ ],
1438
+ ),
1439
+ state(
1440
+ "package-published",
1441
+ packagePublished ? "satisfied" : "not-reached",
1442
+ packagePublished
1443
+ ? `Release transaction explicitly reports ${transactionFacts.publicationState}.`
1444
+ : "No release transaction explicitly proves package publication.",
1445
+ transactionFacts.selected
1446
+ ? [existingFileFact(resolvedCwd, transactionFacts.selected.path)]
1447
+ : [],
1448
+ ),
1449
+ state(
1450
+ "alpha-complete",
1451
+ alphaComplete ? "satisfied" : "not-reached",
1452
+ alphaComplete
1453
+ ? "The alpha release transaction is complete."
1454
+ : "No completed alpha transaction is present.",
1455
+ transactionFacts.selected
1456
+ ? [existingFileFact(resolvedCwd, transactionFacts.selected.path)]
1457
+ : [],
1458
+ ),
1459
+ state(
1460
+ "staging-visible",
1461
+ stagingVisible ? "satisfied" : "not-reached",
1462
+ stagingVisible
1463
+ ? "Explicit digest-bound staging visibility evidence is present."
1464
+ : "No explicit staging visibility evidence is present; visibility is not inferred from publication.",
1465
+ [existingFileFact(resolvedCwd, PAPER_PATHS.visibility)],
1466
+ ),
1467
+ state(
1468
+ "production-visible",
1469
+ productionVisible ? "satisfied" : "not-reached",
1470
+ productionVisible
1471
+ ? "Explicit digest-bound production visibility evidence is present."
1472
+ : "No explicit production visibility evidence is present; visibility is not inferred from staging.",
1473
+ [existingFileFact(resolvedCwd, PAPER_PATHS.visibility)],
1474
+ ),
1475
+ ];
1476
+ const achieved = states
1477
+ .filter((entry) => entry.satisfied)
1478
+ .map((entry) => entry.id);
1479
+ const highestEvidenceState =
1480
+ [...states].reverse().find((entry) => entry.satisfied)?.id || "none";
1481
+ const stateMap = Object.fromEntries(states.map((entry) => [entry.id, entry]));
1482
+ const blockingNextActions = [];
1483
+ if (!stateMap.scaffolded.satisfied) {
1484
+ blockingNextActions.push({
1485
+ id: "scaffold-paper",
1486
+ command: "buildchain paper scaffold --help",
1487
+ description: "Plan a no-overwrite Buildchain paper scaffold.",
1488
+ });
1489
+ } else if (!stateMap.governed.satisfied) {
1490
+ blockingNextActions.push({
1491
+ id: "restore-governance",
1492
+ command: "buildchain paper preflight --json",
1493
+ description:
1494
+ "Repair the Git/contract-lock governance checks reported by preflight.",
1495
+ });
1496
+ } else if (!stateMap["content-ready"].satisfied) {
1497
+ blockingNextActions.push({
1498
+ id: "complete-paper-content",
1499
+ command: "make check",
1500
+ description: "Add every declared paper source and metadata input.",
1501
+ });
1502
+ } else if (!deterministic) {
1503
+ blockingNextActions.push({
1504
+ id: "build-reproducible-artifact",
1505
+ command: "buildchain paper build --execute --json",
1506
+ description:
1507
+ "Run the existing two-clean-build reproducibility gate and promote exact bytes.",
1508
+ });
1509
+ } else if (
1510
+ !stateMap.bootstrapped.satisfied ||
1511
+ !stateMap["trust-bound"].satisfied
1512
+ ) {
1513
+ blockingNextActions.push({
1514
+ id: "bootstrap-npm",
1515
+ command: "buildchain paper bootstrap npm --json",
1516
+ description:
1517
+ "Inspect the dry-run npm bootstrap and Trusted Publishing handoff.",
1518
+ });
1519
+ } else if (!stateMap["alpha-complete"].satisfied) {
1520
+ blockingNextActions.push({
1521
+ id: transactionFacts.selected ? "resume-alpha" : "start-alpha",
1522
+ command: transactionFacts.selected
1523
+ ? "buildchain paper resume --json"
1524
+ : "buildchain paper alpha --json",
1525
+ description: transactionFacts.selected
1526
+ ? "Resume the exact sealed release transaction."
1527
+ : "Plan the protected dev-to-alpha publication PR.",
1528
+ });
1529
+ }
1530
+ return {
1531
+ schemaVersion: 1,
1532
+ contract: PAPER_STATUS_CONTRACT,
1533
+ ok: !configResult.error,
1534
+ cwd: resolvedCwd,
1535
+ identity: {
1536
+ project: loaded?.config?.project?.name || "",
1537
+ package: packageName,
1538
+ version,
1539
+ repository: resolvePaperRepository(resolvedCwd),
1540
+ },
1541
+ states,
1542
+ achieved,
1543
+ highestEvidenceState,
1544
+ deterministicBuild: {
1545
+ status: deterministic
1546
+ ? "qualifying"
1547
+ : reproducibility.exists
1548
+ ? "non-qualifying"
1549
+ : "not-run",
1550
+ receiptPath: PAPER_PATHS.reproducibilityReceipt,
1551
+ receiptDigest: reproducibility.value?.receiptDigest || "",
1552
+ },
1553
+ transaction: transactionFacts.selected
1554
+ ? {
1555
+ path: transactionFacts.selected.path,
1556
+ id: transactionFacts.selected.transaction.id || "",
1557
+ state: transactionFacts.selected.transaction.state || "",
1558
+ publicationState: transactionFacts.publicationState,
1559
+ targetRef: transactionFacts.selected.transaction.target_ref || "",
1560
+ sourceSha: transactionFacts.selected.transaction.source_sha || "",
1561
+ resumeCommand:
1562
+ transactionFacts.selected.transaction.resume_command || "",
1563
+ }
1564
+ : null,
1565
+ conflicts: [
1566
+ ...candidates
1567
+ .filter((entry) => entry.error)
1568
+ .map((entry) => ({
1569
+ code: "release-state-invalid",
1570
+ path: entry.path,
1571
+ message: entry.error,
1572
+ })),
1573
+ ...(transactionFacts.matching.length > 1
1574
+ ? [
1575
+ {
1576
+ code: "multiple-release-transactions-for-version",
1577
+ paths: transactionFacts.matching.map((entry) => entry.path),
1578
+ message:
1579
+ "More than one release transaction claims the configured publication version.",
1580
+ },
1581
+ ]
1582
+ : []),
1583
+ ],
1584
+ blockingNextActions,
1585
+ nonClaims: [
1586
+ "npm package existence is not inferred without a typed receipt or live preflight observation",
1587
+ "publication admission is not inferred from reproducibility",
1588
+ "package publication is not inferred from a local npm package directory",
1589
+ "staging and production visibility are never inferred from alpha completion",
1590
+ ],
1591
+ };
1592
+ }
1593
+
1594
+ export function resolvePaperRepository(cwd = process.cwd()) {
1595
+ const packagePath = path.resolve(cwd, "package.json");
1596
+ const sourcePackage = readJson(packagePath).value;
1597
+ const configured =
1598
+ typeof sourcePackage?.repository === "string"
1599
+ ? sourcePackage.repository
1600
+ : sourcePackage?.repository?.url;
1601
+ const fromPackage = normalizeRepository(configured);
1602
+ if (fromPackage) return fromPackage;
1603
+ return normalizeRepository(
1604
+ gitValue(cwd, ["config", "--get", "remote.origin.url"]),
1605
+ );
1606
+ }
1607
+
1608
+ function validatePaperProvisioningAuthority(cwd) {
1609
+ const authorityPath = path.resolve(cwd, PAPER_PATHS.provisioningAuthority);
1610
+ const source = readJson(authorityPath);
1611
+ if (!source.exists) {
1612
+ return {
1613
+ exists: false,
1614
+ valid: false,
1615
+ value: undefined,
1616
+ errors: ["paper provisioning authority is missing"],
1617
+ };
1618
+ }
1619
+ if (source.error || !source.value) {
1620
+ return {
1621
+ exists: true,
1622
+ valid: false,
1623
+ value: source.value,
1624
+ errors: [source.error || "paper provisioning authority is invalid"],
1625
+ };
1626
+ }
1627
+ const value = source.value;
1628
+ const errors = [];
1629
+ if (value.contract !== PAPER_PROVISIONING_CONTRACT) {
1630
+ errors.push("paper provisioning authority contract mismatch");
1631
+ }
1632
+ const { authorityDigest, ...payload } = value;
1633
+ if (
1634
+ !SHA256_PATTERN.test(String(authorityDigest || "")) ||
1635
+ authorityDigest !== sha256Text(stableJson(payload))
1636
+ ) {
1637
+ errors.push("paper provisioning authority digest mismatch");
1638
+ }
1639
+ if (
1640
+ !GIT_SHA_PATTERN.test(String(value.runtime?.resolvedSha || "")) ||
1641
+ value.runtime?.ref !== value.runtime?.resolvedSha ||
1642
+ value.admission?.acceptedRef !== value.runtime?.resolvedSha ||
1643
+ value.admission?.acceptedSha !== value.runtime?.resolvedSha
1644
+ ) {
1645
+ errors.push("paper runtime and admission are not bound to one exact SHA");
1646
+ }
1647
+ if (
1648
+ value.package?.registry !== NPM_REGISTRY ||
1649
+ value.package?.bootstrapVersion !== DEFAULT_BOOTSTRAP_VERSION
1650
+ ) {
1651
+ errors.push(
1652
+ "paper npm bootstrap authority is not fixed to the official registry and bootstrap version",
1653
+ );
1654
+ }
1655
+ const policy = value.policy || {};
1656
+ const { policyDigest, ...policyPayload } = policy;
1657
+ if (
1658
+ !SHA256_PATTERN.test(String(policyDigest || "")) ||
1659
+ policyDigest !== sha256Text(stableJson(policyPayload))
1660
+ ) {
1661
+ errors.push("paper provisioning policy digest mismatch");
1662
+ }
1663
+ if (
1664
+ policy.repositoryActions?.defaultWorkflowPermissions !== "read" ||
1665
+ policy.repositoryActions?.canApprovePullRequestReviews !== false
1666
+ ) {
1667
+ errors.push("paper repository Actions policy is not least privilege");
1668
+ }
1669
+ if (
1670
+ policy.generatedWrites?.preferredAuthority !== "github-app" ||
1671
+ policy.generatedWrites?.githubTokenFallback !== false
1672
+ ) {
1673
+ errors.push("paper generated-write policy permits an unbounded authority");
1674
+ }
1675
+ if (
1676
+ policy.release?.versionState !== "not-required" ||
1677
+ policy.release?.identityOnlyPullRequests !== false ||
1678
+ policy.release?.manualVersionStateRepairPullRequests !== false
1679
+ ) {
1680
+ errors.push(
1681
+ "paper release policy permits avoidable bookkeeping pull requests",
1682
+ );
1683
+ }
1684
+ for (const workflow of [value.workflows?.build, value.workflows?.release]) {
1685
+ if (!workflow?.path || !workflow?.sourceDigest) {
1686
+ errors.push("paper workflow authority is incomplete");
1687
+ continue;
1688
+ }
1689
+ const absolute = path.resolve(cwd, workflow.path);
1690
+ if (
1691
+ !fs.existsSync(absolute) ||
1692
+ sha256File(absolute) !== workflow.sourceDigest
1693
+ ) {
1694
+ errors.push(`paper workflow source digest mismatch: ${workflow.path}`);
1695
+ continue;
1696
+ }
1697
+ const text = fs.readFileSync(absolute, "utf8");
1698
+ const expectedUse = `${value.runtime.repository}/${workflow.reusablePath}@${value.runtime.resolvedSha}`;
1699
+ if (!text.includes(`uses: ${expectedUse}`)) {
1700
+ errors.push(
1701
+ `paper workflow reusable source is not exact: ${workflow.path}`,
1702
+ );
1703
+ }
1704
+ if (!text.includes(`buildchain-ref: ${value.runtime.resolvedSha}`)) {
1705
+ errors.push(
1706
+ `paper workflow runtime input is not exact: ${workflow.path}`,
1707
+ );
1708
+ }
1709
+ }
1710
+ const lockPath = path.resolve(
1711
+ cwd,
1712
+ value.admission?.contractLockPath || PAPER_PATHS.contractLock,
1713
+ );
1714
+ if (
1715
+ !fs.existsSync(lockPath) ||
1716
+ sha256File(lockPath) !== value.admission?.contractLockDigest
1717
+ ) {
1718
+ errors.push("paper contract lock bytes differ from provisioning authority");
1719
+ }
1720
+ return {
1721
+ exists: true,
1722
+ valid: errors.length === 0,
1723
+ value,
1724
+ errors,
1725
+ };
1726
+ }
1727
+
1728
+ function expectedPaperTrustedPublisher(authority, fallback = {}) {
1729
+ const value = authority?.trustedPublisher || {};
1730
+ return {
1731
+ type: String(value.type || "github").toLowerCase(),
1732
+ repository: normalizeRepository(
1733
+ value.repository || fallback.repository || "",
1734
+ ),
1735
+ workflow: toPosix(value.workflow || fallback.workflow || ""),
1736
+ environment: String(value.environment || fallback.environment || ""),
1737
+ };
1738
+ }
1739
+
1740
+ function normalizedTrustedPublisher(value) {
1741
+ return {
1742
+ type: String(value?.type || value?.provider || "")
1743
+ .trim()
1744
+ .toLowerCase()
1745
+ .replace(/^github-actions$/, "github"),
1746
+ repository: normalizeRepository(value?.repository || value?.repo || ""),
1747
+ workflow: toPosix(value?.workflow || value?.file || "").replace(/^\/+/, ""),
1748
+ environment: String(value?.environment || value?.env || "").trim(),
1749
+ };
1750
+ }
1751
+
1752
+ function trustedPublisherMatches(actual, expected) {
1753
+ const normalized = normalizedTrustedPublisher(actual);
1754
+ return (
1755
+ normalized.type === expected.type &&
1756
+ normalized.repository === expected.repository &&
1757
+ normalized.workflow === expected.workflow &&
1758
+ normalized.environment === expected.environment
1759
+ );
1760
+ }
1761
+
1762
+ function runtimeFacts({
1763
+ buildchainRoot,
1764
+ buildchainVersion,
1765
+ buildchainRef,
1766
+ buildchainSha,
1767
+ }) {
1768
+ const identity = buildchainPackageIdentity(buildchainRoot, buildchainVersion);
1769
+ const contractWorld = runtimeContractWorld(buildchainRoot);
1770
+ return {
1771
+ version: identity.version,
1772
+ ref: buildchainRef,
1773
+ resolvedSha:
1774
+ buildchainSha || runtimeGitSha(buildchainRoot, identity.version),
1775
+ contract: contractWorld.contract,
1776
+ contractDigest: contractWorld.contractDigest,
1777
+ compatibilityDigest: contractWorld.compatibilityDigest,
1778
+ majorLine: contractWorld.majorLine,
1779
+ };
1780
+ }
1781
+
1782
+ function liveNpmPackageObservation(packageName, registry, cwd) {
1783
+ const result = commandResult(
1784
+ "npm",
1785
+ ["view", packageName, "version", "--json", `--registry=${registry}`],
1786
+ { cwd },
1787
+ );
1788
+ if (result.ok) {
1789
+ let version = "";
1790
+ try {
1791
+ const parsed = JSON.parse(result.stdout || '""');
1792
+ version = Array.isArray(parsed)
1793
+ ? String(parsed.at(-1) || "")
1794
+ : String(parsed || "");
1795
+ } catch {
1796
+ version = result.stdout.replace(/^"|"$/g, "");
1797
+ }
1798
+ return {
1799
+ status: "observed",
1800
+ exists: true,
1801
+ version,
1802
+ registry,
1803
+ errorCode: "",
1804
+ };
1805
+ }
1806
+ const output = `${result.stderr}\n${result.stdout}`;
1807
+ if (/\bE404\b|404 Not Found|is not in this registry/i.test(output)) {
1808
+ return {
1809
+ status: "observed",
1810
+ exists: false,
1811
+ version: "",
1812
+ registry,
1813
+ errorCode: "package-not-found",
1814
+ };
1815
+ }
1816
+ return {
1817
+ status: "unknown",
1818
+ exists: null,
1819
+ version: "",
1820
+ registry,
1821
+ errorCode: result.error ? "npm-unavailable" : "npm-view-failed",
1822
+ };
1823
+ }
1824
+
1825
+ function liveNpmAuthObservation(registry, cwd) {
1826
+ const result = commandResult("npm", ["whoami", `--registry=${registry}`], {
1827
+ cwd,
1828
+ });
1829
+ return {
1830
+ status: result.ok ? "authenticated" : "unauthenticated-or-unavailable",
1831
+ authenticated: result.ok,
1832
+ identity: result.ok ? result.stdout : "",
1833
+ errorCode: result.ok
1834
+ ? ""
1835
+ : result.error
1836
+ ? "npm-unavailable"
1837
+ : "npm-auth-unavailable",
1838
+ };
1839
+ }
1840
+
1841
+ function liveNpmTrustObservation(
1842
+ packageName,
1843
+ registry,
1844
+ cwd,
1845
+ expectedPublisher = undefined,
1846
+ ) {
1847
+ const result = commandResult(
1848
+ "npm",
1849
+ ["trust", "list", packageName, "--json", `--registry=${registry}`],
1850
+ { cwd },
1851
+ );
1852
+ if (!result.ok) {
1853
+ return {
1854
+ status: "unknown",
1855
+ configured: null,
1856
+ publishers: [],
1857
+ errorCode: result.error
1858
+ ? "npm-unavailable"
1859
+ : "npm-trust-query-unavailable",
1860
+ };
1861
+ }
1862
+ let parsed;
1863
+ try {
1864
+ parsed = JSON.parse(result.stdout || "[]");
1865
+ } catch {
1866
+ return {
1867
+ status: "unknown",
1868
+ configured: null,
1869
+ publishers: [],
1870
+ errorCode: "npm-trust-response-invalid",
1871
+ };
1872
+ }
1873
+ const publishers = Array.isArray(parsed)
1874
+ ? parsed
1875
+ : Array.isArray(parsed?.publishers)
1876
+ ? parsed.publishers
1877
+ : Array.isArray(parsed?.trustedPublishers)
1878
+ ? parsed.trustedPublishers
1879
+ : [];
1880
+ const normalizedPublishers = publishers.map(normalizedTrustedPublisher);
1881
+ const exactMatches = expectedPublisher
1882
+ ? normalizedPublishers.filter((entry) =>
1883
+ trustedPublisherMatches(entry, expectedPublisher),
1884
+ )
1885
+ : [];
1886
+ return {
1887
+ status: "observed",
1888
+ configured: expectedPublisher
1889
+ ? exactMatches.length === 1
1890
+ : normalizedPublishers.length > 0,
1891
+ exactBinding: expectedPublisher ? exactMatches.length === 1 : null,
1892
+ expectedPublisher: expectedPublisher || null,
1893
+ publishers: normalizedPublishers,
1894
+ errorCode: "",
1895
+ };
1896
+ }
1897
+
1898
+ function liveRepositoryPermissionObservation(repository, cwd) {
1899
+ if (!repository) {
1900
+ return {
1901
+ status: "unknown",
1902
+ repository: "",
1903
+ canWrite: null,
1904
+ errorCode: "repository-unresolved",
1905
+ };
1906
+ }
1907
+ const result = commandResult("gh", ["api", `repos/${repository}`], { cwd });
1908
+ if (!result.ok) {
1909
+ return {
1910
+ status: "unknown",
1911
+ repository,
1912
+ canWrite: null,
1913
+ errorCode: result.error
1914
+ ? "gh-unavailable"
1915
+ : "github-permission-query-failed",
1916
+ };
1917
+ }
1918
+ let parsed;
1919
+ try {
1920
+ parsed = JSON.parse(result.stdout);
1921
+ } catch {
1922
+ return {
1923
+ status: "unknown",
1924
+ repository,
1925
+ canWrite: null,
1926
+ errorCode: "github-response-invalid",
1927
+ };
1928
+ }
1929
+ const permissions = parsed.permissions || {};
1930
+ return {
1931
+ status: "observed",
1932
+ repository,
1933
+ canWrite: Boolean(
1934
+ permissions.push || permissions.maintain || permissions.admin,
1935
+ ),
1936
+ defaultBranch: parsed.default_branch || "",
1937
+ archived: parsed.archived === true,
1938
+ visibility: parsed.visibility || (parsed.private ? "private" : "public"),
1939
+ errorCode: "",
1940
+ };
1941
+ }
1942
+
1943
+ function liveRepositoryActionsPolicyObservation(repository, cwd) {
1944
+ if (!repository) {
1945
+ return {
1946
+ status: "unknown",
1947
+ defaultWorkflowPermissions: "",
1948
+ canApprovePullRequestReviews: null,
1949
+ errorCode: "repository-unresolved",
1950
+ };
1951
+ }
1952
+ const result = commandResult(
1953
+ "gh",
1954
+ ["api", `repos/${repository}/actions/permissions/workflow`],
1955
+ { cwd },
1956
+ );
1957
+ if (!result.ok) {
1958
+ return {
1959
+ status: "unknown",
1960
+ defaultWorkflowPermissions: "",
1961
+ canApprovePullRequestReviews: null,
1962
+ errorCode: result.error
1963
+ ? "gh-unavailable"
1964
+ : "github-actions-policy-query-failed",
1965
+ };
1966
+ }
1967
+ const parsed = safeParseJson(result.stdout);
1968
+ if (!parsed || typeof parsed !== "object") {
1969
+ return {
1970
+ status: "unknown",
1971
+ defaultWorkflowPermissions: "",
1972
+ canApprovePullRequestReviews: null,
1973
+ errorCode: "github-actions-policy-response-invalid",
1974
+ };
1975
+ }
1976
+ return {
1977
+ status: "observed",
1978
+ defaultWorkflowPermissions: String(
1979
+ parsed.default_workflow_permissions || "",
1980
+ ),
1981
+ canApprovePullRequestReviews:
1982
+ parsed.can_approve_pull_request_reviews === true,
1983
+ errorCode: "",
1984
+ };
1985
+ }
1986
+
1987
+ function liveGeneratedWriteAuthorityObservation(repository, cwd) {
1988
+ if (!repository) {
1989
+ return {
1990
+ status: "unknown",
1991
+ configured: null,
1992
+ mode: "",
1993
+ errorCode: "repository-unresolved",
1994
+ };
1995
+ }
1996
+ const result = commandResult(
1997
+ "gh",
1998
+ ["secret", "list", "--repo", repository, "--json", "name"],
1999
+ { cwd },
2000
+ );
2001
+ if (!result.ok) {
2002
+ return {
2003
+ status: "unknown",
2004
+ configured: null,
2005
+ mode: "",
2006
+ errorCode: result.error
2007
+ ? "gh-unavailable"
2008
+ : "github-secret-metadata-query-failed",
2009
+ };
2010
+ }
2011
+ const parsed = safeParseJson(result.stdout);
2012
+ const names = new Set(
2013
+ (Array.isArray(parsed) ? parsed : [])
2014
+ .map((entry) => String(entry?.name || ""))
2015
+ .filter(Boolean),
2016
+ );
2017
+ const appConfigured =
2018
+ names.has("BUILDCHAIN_GENERATED_WRITE_APP_CLIENT_ID") &&
2019
+ names.has("BUILDCHAIN_GENERATED_WRITE_APP_PRIVATE_KEY");
2020
+ const narrowTokenConfigured =
2021
+ names.has("BUILDCHAIN_GENERATED_WRITE_TOKEN") ||
2022
+ names.has("BUILDCHAIN_PROMOTION_TOKEN");
2023
+ return {
2024
+ status: "observed",
2025
+ configured: appConfigured || narrowTokenConfigured,
2026
+ mode: appConfigured
2027
+ ? "github-app"
2028
+ : narrowTokenConfigured
2029
+ ? "narrow-token"
2030
+ : "",
2031
+ errorCode: "",
2032
+ };
2033
+ }
2034
+
2035
+ function localToolchainObservation(cwd, publication) {
2036
+ const toolchain = publication?.toolchain || {};
2037
+ const imageRef =
2038
+ toolchain.image && toolchain.digest
2039
+ ? `${toolchain.image}@${toolchain.digest}`
2040
+ : "";
2041
+ const dockerVersion = commandResult(
2042
+ "docker",
2043
+ ["version", "--format", "{{.Client.Version}}"],
2044
+ {
2045
+ cwd,
2046
+ timeout: 5000,
2047
+ },
2048
+ );
2049
+ const image = imageRef
2050
+ ? commandResult(
2051
+ "docker",
2052
+ ["image", "inspect", imageRef, "--format", "{{index .RepoDigests 0}}"],
2053
+ {
2054
+ cwd,
2055
+ timeout: 5000,
2056
+ },
2057
+ )
2058
+ : { ok: false };
2059
+ return {
2060
+ type: toolchain.type || "",
2061
+ image: toolchain.image || "",
2062
+ digest: toolchain.digest || "",
2063
+ command: toolchain.command || "",
2064
+ identityRoot: sha256Text(
2065
+ stableJson({
2066
+ type: toolchain.type || "",
2067
+ image: toolchain.image || "",
2068
+ digest: toolchain.digest || "",
2069
+ command: toolchain.command || "",
2070
+ }),
2071
+ ),
2072
+ machineVerifiable: Boolean(
2073
+ toolchain.type === "latex-docker" &&
2074
+ toolchain.image &&
2075
+ SHA256_PATTERN.test(toolchain.digest || "") &&
2076
+ toolchain.command,
2077
+ ),
2078
+ dockerAvailable: dockerVersion.ok,
2079
+ imageAvailableLocally: image.ok,
2080
+ };
2081
+ }
2082
+
2083
+ export function collectPaperPreflight({
2084
+ cwd = process.cwd(),
2085
+ buildchainRoot = process.cwd(),
2086
+ buildchainVersion = "",
2087
+ buildchainRef = "v3",
2088
+ buildchainSha = "",
2089
+ registry = NPM_REGISTRY,
2090
+ offline = false,
2091
+ } = {}) {
2092
+ const resolvedCwd = path.resolve(cwd);
2093
+ const status = collectPaperStatus({ cwd: resolvedCwd });
2094
+ const provisioning = validatePaperProvisioningAuthority(resolvedCwd);
2095
+ const configResult = paperConfig(resolvedCwd);
2096
+ let validation;
2097
+ let validationError = "";
2098
+ try {
2099
+ validation = validateBuildchainConfig(resolvedCwd, {
2100
+ requireLifecycleStages: ["verify"],
2101
+ });
2102
+ } catch (error) {
2103
+ validationError = error.message;
2104
+ }
2105
+ const runtime = runtimeFacts({
2106
+ buildchainRoot,
2107
+ buildchainVersion,
2108
+ buildchainRef: provisioning.value?.runtime?.ref || buildchainRef,
2109
+ buildchainSha,
2110
+ });
2111
+ const lockPath = path.resolve(resolvedCwd, PAPER_PATHS.contractLock);
2112
+ let lockEvaluation = {
2113
+ status: "missing-lock",
2114
+ compatible: false,
2115
+ drift: false,
2116
+ reasons: ["Buildchain contract lock is missing"],
2117
+ };
2118
+ try {
2119
+ const lock = readBuildchainContractLock(lockPath);
2120
+ if (lock) {
2121
+ const current = runtimeContractWorld(buildchainRoot);
2122
+ lockEvaluation = evaluateBuildchainContractLock({
2123
+ lock,
2124
+ current,
2125
+ runtimeRef: runtime.ref,
2126
+ runtimeSha: runtime.resolvedSha,
2127
+ runtimeClass: /alpha/i.test(runtime.ref) ? "alpha" : "stable",
2128
+ });
2129
+ }
2130
+ } catch (error) {
2131
+ lockEvaluation = {
2132
+ status: "invalid-lock",
2133
+ compatible: false,
2134
+ drift: false,
2135
+ reasons: [error.message],
2136
+ };
2137
+ }
2138
+ const source = {
2139
+ repositoryRoot: gitValue(resolvedCwd, ["rev-parse", "--show-toplevel"]),
2140
+ head: gitValue(resolvedCwd, ["rev-parse", "HEAD"]),
2141
+ tree: gitValue(resolvedCwd, ["rev-parse", "HEAD^{tree}"]),
2142
+ branch: gitValue(resolvedCwd, ["branch", "--show-current"]),
2143
+ clean: gitResult(resolvedCwd, ["status", "--porcelain"]).stdout === "",
2144
+ };
2145
+ const packageName = status.identity.package;
2146
+ const repository = status.identity.repository;
2147
+ const expectedPublisher = expectedPaperTrustedPublisher(provisioning.value, {
2148
+ repository,
2149
+ workflow: path.posix.basename(PAPER_PATHS.releaseWorkflow),
2150
+ });
2151
+ const npm =
2152
+ offline || !packageName
2153
+ ? {
2154
+ package: {
2155
+ status: "unknown",
2156
+ exists: null,
2157
+ version: "",
2158
+ registry,
2159
+ errorCode: offline ? "offline" : "package-unresolved",
2160
+ },
2161
+ auth: {
2162
+ status: "unknown",
2163
+ authenticated: null,
2164
+ identity: "",
2165
+ errorCode: offline ? "offline" : "package-unresolved",
2166
+ },
2167
+ trust: {
2168
+ status: "unknown",
2169
+ configured: null,
2170
+ publishers: [],
2171
+ errorCode: offline ? "offline" : "package-unresolved",
2172
+ },
2173
+ }
2174
+ : {
2175
+ package: liveNpmPackageObservation(
2176
+ packageName,
2177
+ registry,
2178
+ resolvedCwd,
2179
+ ),
2180
+ auth: liveNpmAuthObservation(registry, resolvedCwd),
2181
+ trust: liveNpmTrustObservation(
2182
+ packageName,
2183
+ registry,
2184
+ resolvedCwd,
2185
+ expectedPublisher,
2186
+ ),
2187
+ };
2188
+ const repositoryPermissions = offline
2189
+ ? {
2190
+ status: "unknown",
2191
+ repository,
2192
+ canWrite: null,
2193
+ errorCode: "offline",
2194
+ }
2195
+ : liveRepositoryPermissionObservation(repository, resolvedCwd);
2196
+ const repositoryActions = offline
2197
+ ? {
2198
+ status: "unknown",
2199
+ defaultWorkflowPermissions: "",
2200
+ canApprovePullRequestReviews: null,
2201
+ errorCode: "offline",
2202
+ }
2203
+ : liveRepositoryActionsPolicyObservation(repository, resolvedCwd);
2204
+ const generatedWriteAuthority = offline
2205
+ ? {
2206
+ status: "unknown",
2207
+ configured: null,
2208
+ mode: "",
2209
+ errorCode: "offline",
2210
+ }
2211
+ : liveGeneratedWriteAuthorityObservation(repository, resolvedCwd);
2212
+ const toolchain = localToolchainObservation(
2213
+ resolvedCwd,
2214
+ configResult.loaded?.config?.publication,
2215
+ );
2216
+ const checks = [
2217
+ {
2218
+ id: "provisioning.authority",
2219
+ status: provisioning.valid ? "pass" : "fail",
2220
+ blocking: true,
2221
+ scope: "local",
2222
+ message: provisioning.valid
2223
+ ? "Paper runtime, callers, contract lock, trust target, and policy share one exact authority root."
2224
+ : provisioning.errors.join("; "),
2225
+ },
2226
+ {
2227
+ id: "config.publication",
2228
+ status: validationError ? "fail" : "pass",
2229
+ blocking: true,
2230
+ scope: "local",
2231
+ message:
2232
+ validationError ||
2233
+ "Publication config, digest-pinned toolchain, and verify lifecycle are valid.",
2234
+ },
2235
+ {
2236
+ id: "source.exact-commit",
2237
+ status:
2238
+ GIT_SHA_PATTERN.test(source.head) && source.clean ? "pass" : "fail",
2239
+ blocking: false,
2240
+ scope: "external-mutation",
2241
+ message: !GIT_SHA_PATTERN.test(source.head)
2242
+ ? "Repository HEAD is unresolved."
2243
+ : source.clean
2244
+ ? "Source is bound to a clean exact commit."
2245
+ : "Working tree is dirty; reproducibility admits committed bytes only.",
2246
+ },
2247
+ {
2248
+ id: "toolchain.pinned",
2249
+ status: toolchain.machineVerifiable ? "pass" : "fail",
2250
+ blocking: true,
2251
+ scope: "local",
2252
+ message: toolchain.machineVerifiable
2253
+ ? "Publication toolchain identity is digest-pinned."
2254
+ : "Publication toolchain is not a qualifying digest-pinned latex-docker toolchain.",
2255
+ },
2256
+ {
2257
+ id: "runtime.exact-source",
2258
+ status:
2259
+ runtime.version && GIT_SHA_PATTERN.test(runtime.resolvedSha)
2260
+ ? "pass"
2261
+ : "fail",
2262
+ blocking: true,
2263
+ scope: "local",
2264
+ message:
2265
+ runtime.version && GIT_SHA_PATTERN.test(runtime.resolvedSha)
2266
+ ? "Buildchain runtime is bound to an exact package version and source SHA."
2267
+ : "Buildchain runtime version or exact source SHA is unresolved.",
2268
+ },
2269
+ {
2270
+ id: "runtime.contract-lock",
2271
+ status: lockEvaluation.compatible ? "pass" : "fail",
2272
+ blocking: true,
2273
+ scope: "local",
2274
+ message: lockEvaluation.compatible
2275
+ ? `Buildchain runtime contract is ${lockEvaluation.status}.`
2276
+ : `Buildchain runtime contract is not admitted: ${(lockEvaluation.reasons || []).join("; ")}`,
2277
+ },
2278
+ {
2279
+ id: "repository.write-permission",
2280
+ status:
2281
+ repositoryPermissions.canWrite === true
2282
+ ? "pass"
2283
+ : repositoryPermissions.canWrite === false
2284
+ ? "fail"
2285
+ : "unknown",
2286
+ blocking: false,
2287
+ scope: "external-mutation",
2288
+ message:
2289
+ repositoryPermissions.canWrite === true
2290
+ ? "Current GitHub identity can write the repository."
2291
+ : repositoryPermissions.canWrite === false
2292
+ ? "Current GitHub identity cannot write the repository."
2293
+ : `Repository permission is unknown (${repositoryPermissions.errorCode}).`,
2294
+ },
2295
+ {
2296
+ id: "repository.actions-policy",
2297
+ status:
2298
+ repositoryActions.defaultWorkflowPermissions === "read" &&
2299
+ repositoryActions.canApprovePullRequestReviews === false
2300
+ ? "pass"
2301
+ : repositoryActions.status === "observed"
2302
+ ? "fail"
2303
+ : "unknown",
2304
+ blocking: false,
2305
+ scope: "external-mutation",
2306
+ message:
2307
+ repositoryActions.defaultWorkflowPermissions === "read" &&
2308
+ repositoryActions.canApprovePullRequestReviews === false
2309
+ ? "Repository defaults workflow permissions to read and disables Actions pull-request approval."
2310
+ : repositoryActions.status === "observed"
2311
+ ? "Repository Actions policy is broader than the paper provisioning authority."
2312
+ : `Repository Actions policy is unknown (${repositoryActions.errorCode}).`,
2313
+ },
2314
+ {
2315
+ id: "repository.generated-write-authority",
2316
+ status:
2317
+ generatedWriteAuthority.configured === true
2318
+ ? "pass"
2319
+ : generatedWriteAuthority.configured === false
2320
+ ? "pending"
2321
+ : "unknown",
2322
+ blocking: false,
2323
+ scope: "external-mutation",
2324
+ message:
2325
+ generatedWriteAuthority.configured === true
2326
+ ? `Generated writes use ${generatedWriteAuthority.mode} metadata; no secret value was read.`
2327
+ : generatedWriteAuthority.configured === false
2328
+ ? "No GitHub App or compatible narrow generated-write credential is configured."
2329
+ : `Generated-write authority is unknown (${generatedWriteAuthority.errorCode}).`,
2330
+ },
2331
+ {
2332
+ id: "npm.package",
2333
+ status:
2334
+ npm.package.exists === true
2335
+ ? "pass"
2336
+ : npm.package.exists === false
2337
+ ? "pending"
2338
+ : "unknown",
2339
+ blocking: false,
2340
+ scope: "external-mutation",
2341
+ message:
2342
+ npm.package.exists === true
2343
+ ? `npm package exists at ${npm.package.version}.`
2344
+ : npm.package.exists === false
2345
+ ? "npm package does not exist and requires bootstrap."
2346
+ : `npm package existence is unknown (${npm.package.errorCode}).`,
2347
+ },
2348
+ {
2349
+ id: "npm.trusted-publisher",
2350
+ status:
2351
+ npm.trust.configured === true
2352
+ ? "pass"
2353
+ : npm.trust.configured === false
2354
+ ? "pending"
2355
+ : "unknown",
2356
+ blocking: false,
2357
+ scope: "external-mutation",
2358
+ message:
2359
+ npm.trust.configured === true
2360
+ ? "npm reports exactly the expected repository, workflow, and environment Trusted Publisher binding."
2361
+ : npm.trust.configured === false
2362
+ ? "npm does not report the exact expected Trusted Publisher binding."
2363
+ : `npm Trusted Publisher status is unknown (${npm.trust.errorCode}).`,
2364
+ },
2365
+ {
2366
+ id: "build.reproducible",
2367
+ status:
2368
+ status.deterministicBuild.status === "qualifying" ? "pass" : "pending",
2369
+ blocking: false,
2370
+ scope: "local",
2371
+ message:
2372
+ status.deterministicBuild.status === "qualifying"
2373
+ ? "A qualifying two-clean-build receipt exists."
2374
+ : "No qualifying two-clean-build receipt exists for the exact source.",
2375
+ },
2376
+ {
2377
+ id: "release.state-conflicts",
2378
+ status: status.conflicts.length === 0 ? "pass" : "fail",
2379
+ blocking: true,
2380
+ scope: "local",
2381
+ message:
2382
+ status.conflicts.length === 0
2383
+ ? "No local release-state conflict was detected."
2384
+ : `${status.conflicts.length} release-state conflict(s) require repair.`,
2385
+ },
2386
+ ];
2387
+ const blockingChecks = checks.filter(
2388
+ (entry) => entry.blocking && ["fail", "unknown"].includes(entry.status),
2389
+ );
2390
+ const externalMutationChecks = checks.filter(
2391
+ (entry) =>
2392
+ entry.scope === "external-mutation" &&
2393
+ ["fail", "pending", "unknown"].includes(entry.status),
2394
+ );
2395
+ const nextActions = [];
2396
+ if (validationError) {
2397
+ nextActions.push({
2398
+ id: "repair-config",
2399
+ command: "buildchain validate --require-lifecycle-stages verify",
2400
+ description: validationError,
2401
+ });
2402
+ }
2403
+ if (!source.clean) {
2404
+ nextActions.push({
2405
+ id: "commit-source",
2406
+ command: "git status --short",
2407
+ description:
2408
+ "Review and commit the exact source before deterministic publication.",
2409
+ });
2410
+ }
2411
+ if (!lockEvaluation.compatible) {
2412
+ nextActions.push({
2413
+ id: "refresh-contract-lock",
2414
+ command: "buildchain paper scaffold --json",
2415
+ description:
2416
+ "Review the current runtime contract and resolve the contract-lock difference without overwriting repository files.",
2417
+ });
2418
+ }
2419
+ if (npm.package.exists === false || npm.trust.configured === false) {
2420
+ nextActions.push({
2421
+ id: "bootstrap-npm",
2422
+ command: "buildchain paper bootstrap npm --json",
2423
+ description:
2424
+ "Run the public-package bootstrap and Trusted Publishing dry-run.",
2425
+ });
2426
+ }
2427
+ if (
2428
+ repositoryActions.status === "observed" &&
2429
+ (repositoryActions.defaultWorkflowPermissions !== "read" ||
2430
+ repositoryActions.canApprovePullRequestReviews !== false)
2431
+ ) {
2432
+ nextActions.push({
2433
+ id: "constrain-repository-actions",
2434
+ command: "",
2435
+ description:
2436
+ "Set default workflow permissions to read and disable Actions pull-request approval through the repository provisioner.",
2437
+ });
2438
+ }
2439
+ if (generatedWriteAuthority.configured === false) {
2440
+ nextActions.push({
2441
+ id: "configure-generated-write-authority",
2442
+ command: "",
2443
+ description:
2444
+ "Install a least-privilege GitHub App or configure an equivalent narrow generated-write token without exposing its value.",
2445
+ });
2446
+ }
2447
+ if (status.deterministicBuild.status !== "qualifying") {
2448
+ nextActions.push({
2449
+ id: "build-paper",
2450
+ command: "buildchain paper build --execute --json",
2451
+ description: "Run the existing qualifying two-clean-build gate.",
2452
+ });
2453
+ }
2454
+ return {
2455
+ schemaVersion: 1,
2456
+ contract: PAPER_PREFLIGHT_CONTRACT,
2457
+ ok: blockingChecks.length === 0,
2458
+ localReady: blockingChecks.length === 0,
2459
+ readyForExternalMutation:
2460
+ blockingChecks.length === 0 && externalMutationChecks.length === 0,
2461
+ cwd: resolvedCwd,
2462
+ offline,
2463
+ identity: status.identity,
2464
+ source,
2465
+ toolchain,
2466
+ runtime: {
2467
+ ...runtime,
2468
+ admission: {
2469
+ contractLockPath: PAPER_PATHS.contractLock,
2470
+ status: lockEvaluation.status,
2471
+ compatible: lockEvaluation.compatible === true,
2472
+ drift: lockEvaluation.drift === true,
2473
+ reasons: lockEvaluation.reasons || [],
2474
+ },
2475
+ },
2476
+ provisioning: {
2477
+ path: PAPER_PATHS.provisioningAuthority,
2478
+ valid: provisioning.valid,
2479
+ authorityDigest: provisioning.value?.authorityDigest || "",
2480
+ policyDigest: provisioning.value?.policy?.policyDigest || "",
2481
+ errors: provisioning.errors,
2482
+ },
2483
+ repositoryPermissions,
2484
+ repositoryActions,
2485
+ generatedWriteAuthority,
2486
+ npm,
2487
+ deterministicBuild: status.deterministicBuild,
2488
+ releaseState: {
2489
+ transaction: status.transaction,
2490
+ conflicts: status.conflicts,
2491
+ },
2492
+ checks,
2493
+ blockingNextActions: nextActions,
2494
+ status,
2495
+ };
2496
+ }
2497
+
2498
+ export function createPaperBuildPlan({
2499
+ cwd = process.cwd(),
2500
+ sourceSha = "",
2501
+ pullToolchain = true,
2502
+ } = {}) {
2503
+ const resolvedCwd = path.resolve(cwd);
2504
+ const configResult = paperConfig(resolvedCwd);
2505
+ if (configResult.error) throw new Error(configResult.error);
2506
+ const resolvedSha = sourceSha || gitValue(resolvedCwd, ["rev-parse", "HEAD"]);
2507
+ return {
2508
+ schemaVersion: 1,
2509
+ contract: PAPER_BUILD_PLAN_CONTRACT,
2510
+ ok: GIT_SHA_PATTERN.test(resolvedSha),
2511
+ cwd: resolvedCwd,
2512
+ dryRun: true,
2513
+ sourceSha: resolvedSha,
2514
+ toolchain: configResult.loaded.config.publication.toolchain,
2515
+ pullToolchain,
2516
+ output: PAPER_PATHS.reproducibilityReceipt,
2517
+ promotion: "first-byte-identical-clean-build",
2518
+ delegatesTo: "buildchain publication-artifact reproducibility --promote",
2519
+ nextActions: [
2520
+ {
2521
+ id: "execute-build",
2522
+ command: "buildchain paper build --execute --json",
2523
+ description:
2524
+ "Run two independent clean builds and promote only byte-identical qualifying outputs.",
2525
+ },
2526
+ ],
2527
+ };
2528
+ }
2529
+
2530
+ function parsePaperVersion(version) {
2531
+ const normalized = String(version || "")
2532
+ .trim()
2533
+ .replace(/^v/, "");
2534
+ const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
2535
+ if (!match) {
2536
+ throw new Error("publication.version must be semver before planning Alpha");
2537
+ }
2538
+ return {
2539
+ version: normalized,
2540
+ major: Number(match[1]),
2541
+ minor: Number(match[2]),
2542
+ patch: Number(match[3]),
2543
+ prerelease: match[4] || "",
2544
+ };
2545
+ }
2546
+
2547
+ function resolvePaperChannelRef(cwd, ref) {
2548
+ const candidates = [
2549
+ {
2550
+ observedRef: `refs/remotes/origin/${ref}`,
2551
+ observation: "origin-tracking-ref",
2552
+ },
2553
+ {
2554
+ observedRef: `refs/heads/${ref}`,
2555
+ observation: "local-branch-ref",
2556
+ },
2557
+ ];
2558
+ for (const candidate of candidates) {
2559
+ const sha = gitValue(cwd, [
2560
+ "rev-parse",
2561
+ "--verify",
2562
+ `${candidate.observedRef}^{commit}`,
2563
+ ]);
2564
+ if (GIT_SHA_PATTERN.test(sha)) {
2565
+ return {
2566
+ sha,
2567
+ ...candidate,
2568
+ };
2569
+ }
2570
+ }
2571
+ return {
2572
+ sha: "",
2573
+ observedRef: "",
2574
+ observation: "unresolved",
2575
+ };
2576
+ }
2577
+
2578
+ export function createPaperAlphaPlan({
2579
+ cwd = process.cwd(),
2580
+ sourceRef = "",
2581
+ targetRef = "",
2582
+ } = {}) {
2583
+ const resolvedCwd = path.resolve(cwd);
2584
+ const configResult = paperConfig(resolvedCwd);
2585
+ if (configResult.error) throw new Error(configResult.error);
2586
+ const publication = configResult.loaded.config.publication;
2587
+ const parsed = parsePaperVersion(publication.version);
2588
+ const line = `v${parsed.major}.${parsed.minor}`;
2589
+ const source = sourceRef || `dev/v${parsed.major}/${line}`;
2590
+ const target = targetRef || `alpha/v${parsed.major}/${line}`;
2591
+ const repository = resolvePaperRepository(resolvedCwd);
2592
+ const sourceObservation = resolvePaperChannelRef(resolvedCwd, source);
2593
+ const targetObservation = resolvePaperChannelRef(resolvedCwd, target);
2594
+ const currentBranch = gitValue(resolvedCwd, ["branch", "--show-current"]);
2595
+ return {
2596
+ schemaVersion: 1,
2597
+ contract: PAPER_ALPHA_PLAN_CONTRACT,
2598
+ ok: Boolean(repository && source && target && source !== target),
2599
+ cwd: resolvedCwd,
2600
+ dryRun: true,
2601
+ repository,
2602
+ package: configResult.loaded.config.publish?.package || "",
2603
+ publicationVersion: parsed.version,
2604
+ channel: "alpha",
2605
+ source: {
2606
+ ref: source,
2607
+ ...sourceObservation,
2608
+ },
2609
+ target: {
2610
+ ref: target,
2611
+ ...targetObservation,
2612
+ },
2613
+ currentBranch,
2614
+ mutation: {
2615
+ kind: "github-protected-channel-pr",
2616
+ directPublish: false,
2617
+ directMerge: false,
2618
+ command: `gh pr create --repo ${repository || "<owner/repo>"} --base ${target} --head ${source}`,
2619
+ },
2620
+ gates: [
2621
+ "source is the protected dev channel for the configured semver line",
2622
+ "target is the protected alpha channel",
2623
+ "the paper release workflow seals exact bytes and uses npm OIDC",
2624
+ "this command opens a PR but never merges it or publishes directly",
2625
+ ],
2626
+ nextActions: [
2627
+ {
2628
+ id: "open-alpha-pr",
2629
+ command: "buildchain paper alpha --execute --json",
2630
+ description: "Open or reuse the protected dev-to-alpha publication PR.",
2631
+ },
2632
+ ],
2633
+ };
2634
+ }
2635
+
2636
+ export function createPaperResumePlan({
2637
+ cwd = process.cwd(),
2638
+ buildchainRef = "",
2639
+ } = {}) {
2640
+ const resolvedCwd = path.resolve(cwd);
2641
+ const status = collectPaperStatus({ cwd: resolvedCwd });
2642
+ const transaction = status.transaction;
2643
+ if (!transaction) {
2644
+ return {
2645
+ schemaVersion: 1,
2646
+ contract: PAPER_RESUME_PLAN_CONTRACT,
2647
+ ok: false,
2648
+ cwd: resolvedCwd,
2649
+ dryRun: true,
2650
+ resumable: false,
2651
+ reason: "no-release-transaction",
2652
+ transaction: null,
2653
+ nextActions: [
2654
+ {
2655
+ id: "start-alpha",
2656
+ command: "buildchain paper alpha --json",
2657
+ description:
2658
+ "No transaction exists; plan the protected Alpha publication first.",
2659
+ },
2660
+ ],
2661
+ };
2662
+ }
2663
+ if (
2664
+ ["alpha-complete", "release-complete"].includes(
2665
+ transaction.publicationState,
2666
+ )
2667
+ ) {
2668
+ return {
2669
+ schemaVersion: 1,
2670
+ contract: PAPER_RESUME_PLAN_CONTRACT,
2671
+ ok: true,
2672
+ cwd: resolvedCwd,
2673
+ dryRun: true,
2674
+ resumable: false,
2675
+ reason: "transaction-complete",
2676
+ transaction,
2677
+ nextActions: [],
2678
+ };
2679
+ }
2680
+ const targetRef = transaction.targetRef;
2681
+ const command = [
2682
+ "gh workflow run .github/workflows/paper-release.yml",
2683
+ targetRef ? `--ref ${targetRef}` : "",
2684
+ buildchainRef ? `-f buildchain-ref=${buildchainRef}` : "",
2685
+ ]
2686
+ .filter(Boolean)
2687
+ .join(" ");
2688
+ return {
2689
+ schemaVersion: 1,
2690
+ contract: PAPER_RESUME_PLAN_CONTRACT,
2691
+ ok: Boolean(targetRef),
2692
+ cwd: resolvedCwd,
2693
+ dryRun: true,
2694
+ resumable: Boolean(targetRef),
2695
+ reason: targetRef
2696
+ ? "rerun-sealed-paper-release"
2697
+ : "transaction-target-ref-missing",
2698
+ transaction,
2699
+ mutation: {
2700
+ kind: "github-workflow-dispatch",
2701
+ directPublish: false,
2702
+ command,
2703
+ },
2704
+ nextActions: targetRef
2705
+ ? [
2706
+ {
2707
+ id: "dispatch-resume",
2708
+ command: "buildchain paper resume --execute --json",
2709
+ description:
2710
+ "Dispatch the thin repository workflow against the exact transaction target ref.",
2711
+ },
2712
+ ]
2713
+ : [],
2714
+ };
2715
+ }
2716
+
2717
+ function extractUrls(value, output = new Set()) {
2718
+ if (typeof value === "string") {
2719
+ for (const match of value.matchAll(/https?:\/\/[^\s"'<>]+/g)) {
2720
+ output.add(match[0].replace(/[),.;]+$/, ""));
2721
+ }
2722
+ return output;
2723
+ }
2724
+ if (Array.isArray(value)) {
2725
+ for (const entry of value) extractUrls(entry, output);
2726
+ return output;
2727
+ }
2728
+ if (value && typeof value === "object") {
2729
+ for (const entry of Object.values(value)) extractUrls(entry, output);
2730
+ }
2731
+ return output;
2732
+ }
2733
+
2734
+ function safeParseJson(value) {
2735
+ try {
2736
+ return JSON.parse(value);
2737
+ } catch {
2738
+ return undefined;
2739
+ }
2740
+ }
2741
+
2742
+ function writePaperReceipt(cwd, relativePath, value) {
2743
+ const target = path.resolve(cwd, relativePath);
2744
+ fs.mkdirSync(path.dirname(target), { recursive: true });
2745
+ fs.writeFileSync(target, jsonText(value));
2746
+ return relativePath;
2747
+ }
2748
+
2749
+ function bootstrapPackageShape({ packageName, version }) {
2750
+ return {
2751
+ name: packageName,
2752
+ version,
2753
+ private: false,
2754
+ license: "Apache-2.0",
2755
+ description: "Bootstrap package for npm Trusted Publishing setup.",
2756
+ publishConfig: {
2757
+ access: "public",
2758
+ registry: NPM_REGISTRY,
2759
+ },
2760
+ };
2761
+ }
2762
+
2763
+ export function executePaperNpmBootstrap({
2764
+ cwd = process.cwd(),
2765
+ packageName = "",
2766
+ bootstrapVersion = DEFAULT_BOOTSTRAP_VERSION,
2767
+ registry = NPM_REGISTRY,
2768
+ repository = "",
2769
+ workflow = "paper-release.yml",
2770
+ environment = "",
2771
+ execute = false,
2772
+ confirmedPackage = "",
2773
+ userconfig = "",
2774
+ offline = false,
2775
+ } = {}) {
2776
+ const resolvedCwd = path.resolve(cwd);
2777
+ if (registry !== NPM_REGISTRY) {
2778
+ throw new Error(
2779
+ `paper npm bootstrap requires the official registry ${NPM_REGISTRY}`,
2780
+ );
2781
+ }
2782
+ if (bootstrapVersion !== DEFAULT_BOOTSTRAP_VERSION) {
2783
+ throw new Error(
2784
+ `paper npm bootstrap version is fixed at ${DEFAULT_BOOTSTRAP_VERSION}`,
2785
+ );
2786
+ }
2787
+ const configResult = paperConfig(resolvedCwd);
2788
+ if (configResult.error) throw new Error(configResult.error);
2789
+ const name = normalizePackageName(
2790
+ packageName ||
2791
+ configResult.loaded.config.publish?.package ||
2792
+ configResult.loaded.config.publish?.mainPackage,
2793
+ );
2794
+ const repo = normalizeRepository(
2795
+ repository || resolvePaperRepository(resolvedCwd),
2796
+ );
2797
+ const provisioning = validatePaperProvisioningAuthority(resolvedCwd);
2798
+ if (!provisioning.valid) {
2799
+ throw new Error(
2800
+ `paper npm bootstrap requires a valid provisioning authority: ${provisioning.errors.join("; ")}`,
2801
+ );
2802
+ }
2803
+ const expectedPublisher = expectedPaperTrustedPublisher(provisioning.value);
2804
+ if (
2805
+ provisioning.value.package?.name !== name ||
2806
+ expectedPublisher.repository !== repo ||
2807
+ expectedPublisher.workflow !== toPosix(workflow).replace(/^\/+/, "") ||
2808
+ expectedPublisher.environment !== String(environment || "")
2809
+ ) {
2810
+ throw new Error(
2811
+ "paper npm bootstrap coordinates differ from the exact provisioning authority",
2812
+ );
2813
+ }
2814
+ if (execute && confirmedPackage !== name) {
2815
+ throw new Error(
2816
+ `real npm bootstrap requires --confirm-public-package ${name}`,
2817
+ );
2818
+ }
2819
+ if (execute && offline) {
2820
+ throw new Error("real npm bootstrap cannot run with --offline");
2821
+ }
2822
+ if (execute && (!repo || !workflow)) {
2823
+ throw new Error(
2824
+ "real npm bootstrap requires GitHub repository and workflow coordinates",
2825
+ );
2826
+ }
2827
+ const packageObservation = offline
2828
+ ? {
2829
+ status: "unknown",
2830
+ exists: null,
2831
+ version: "",
2832
+ registry,
2833
+ errorCode: "offline",
2834
+ }
2835
+ : liveNpmPackageObservation(name, registry, resolvedCwd);
2836
+ const auth = offline
2837
+ ? {
2838
+ status: "unknown",
2839
+ authenticated: null,
2840
+ identity: "",
2841
+ errorCode: "offline",
2842
+ }
2843
+ : liveNpmAuthObservation(registry, resolvedCwd);
2844
+ const plan = {
2845
+ schemaVersion: 1,
2846
+ contract: PAPER_NPM_BOOTSTRAP_CONTRACT,
2847
+ ok: true,
2848
+ dryRun: !execute,
2849
+ externalMutation: execute,
2850
+ package: {
2851
+ name,
2852
+ bootstrapVersion,
2853
+ registry,
2854
+ existsBefore: packageObservation.exists,
2855
+ observedVersion: packageObservation.version,
2856
+ },
2857
+ repository: {
2858
+ github: repo,
2859
+ workflow,
2860
+ environment,
2861
+ },
2862
+ authority: {
2863
+ digest: provisioning.value.authorityDigest,
2864
+ policyDigest: provisioning.value.policy.policyDigest,
2865
+ },
2866
+ auth: {
2867
+ authenticated: auth.authenticated,
2868
+ identity: auth.identity,
2869
+ errorCode: auth.errorCode,
2870
+ },
2871
+ publish: {
2872
+ status: packageObservation.exists === true ? "existing" : "planned",
2873
+ distTag: "bootstrap",
2874
+ access: "public",
2875
+ },
2876
+ trust: {
2877
+ status: "planned",
2878
+ urls: [],
2879
+ },
2880
+ receipts: [],
2881
+ nextActions: execute
2882
+ ? []
2883
+ : [
2884
+ {
2885
+ id: "confirm-public-bootstrap",
2886
+ command: `buildchain paper bootstrap npm --execute --confirm-public-package ${name} --json`,
2887
+ description:
2888
+ "After reviewing this dry-run, bootstrap the exact public package and configure GitHub Trusted Publishing.",
2889
+ },
2890
+ ],
2891
+ };
2892
+ if (!execute) {
2893
+ const dryRunRoot = fs.mkdtempSync(
2894
+ path.join(os.tmpdir(), "buildchain-paper-bootstrap-dry-run-"),
2895
+ );
2896
+ try {
2897
+ fs.writeFileSync(
2898
+ path.join(dryRunRoot, "package.json"),
2899
+ jsonText(
2900
+ bootstrapPackageShape({
2901
+ packageName: name,
2902
+ version: bootstrapVersion,
2903
+ }),
2904
+ ),
2905
+ );
2906
+ const configArgs = userconfig ? ["--userconfig", userconfig] : [];
2907
+ const pack = commandResult(
2908
+ "npm",
2909
+ [
2910
+ "pack",
2911
+ "--dry-run",
2912
+ "--json",
2913
+ "--ignore-scripts",
2914
+ `--registry=${registry}`,
2915
+ ...configArgs,
2916
+ ],
2917
+ { cwd: dryRunRoot },
2918
+ );
2919
+ const publish = commandResult(
2920
+ "npm",
2921
+ [
2922
+ "publish",
2923
+ "--dry-run",
2924
+ "--ignore-scripts",
2925
+ "--access",
2926
+ "public",
2927
+ "--tag",
2928
+ "bootstrap",
2929
+ `--registry=${registry}`,
2930
+ ...configArgs,
2931
+ ],
2932
+ { cwd: dryRunRoot },
2933
+ );
2934
+ return {
2935
+ ...plan,
2936
+ ok: pack.ok && publish.ok,
2937
+ errorCode: !pack.ok
2938
+ ? "npm-pack-dry-run-failed"
2939
+ : !publish.ok
2940
+ ? "npm-publish-dry-run-failed"
2941
+ : "",
2942
+ dryRunChecks: {
2943
+ minimalPackageOnly: true,
2944
+ pack: {
2945
+ status: pack.ok ? "pass" : "fail",
2946
+ entryCount: (() => {
2947
+ const parsed = safeParseJson(pack.stdout);
2948
+ const value = Array.isArray(parsed) ? parsed[0] : parsed;
2949
+ return Array.isArray(value?.files) ? value.files.length : 0;
2950
+ })(),
2951
+ },
2952
+ publish: {
2953
+ status: publish.ok ? "pass" : "fail",
2954
+ registry,
2955
+ access: "public",
2956
+ distTag: "bootstrap",
2957
+ },
2958
+ },
2959
+ };
2960
+ } finally {
2961
+ fs.rmSync(dryRunRoot, { recursive: true, force: true });
2962
+ }
2963
+ }
2964
+ if (packageObservation.exists === null) {
2965
+ return {
2966
+ ...plan,
2967
+ ok: false,
2968
+ errorCode: "npm-package-status-unknown",
2969
+ publish: {
2970
+ ...plan.publish,
2971
+ status: "blocked",
2972
+ },
2973
+ trust: {
2974
+ status: "blocked",
2975
+ urls: [],
2976
+ },
2977
+ nextActions: [
2978
+ {
2979
+ id: "verify-npm-package",
2980
+ command: `npm view ${name} version --json --registry=${registry}`,
2981
+ description:
2982
+ "Resolve whether the exact public package exists before any publish mutation.",
2983
+ },
2984
+ ],
2985
+ };
2986
+ }
2987
+ if (auth.authenticated !== true) {
2988
+ return {
2989
+ ...plan,
2990
+ ok: false,
2991
+ errorCode: "npm-auth-required",
2992
+ publish: {
2993
+ ...plan.publish,
2994
+ status: "blocked",
2995
+ },
2996
+ trust: {
2997
+ status: "blocked",
2998
+ urls: [],
2999
+ },
3000
+ nextActions: [
3001
+ {
3002
+ id: "authenticate-npm",
3003
+ command: `npm whoami --registry=${registry}`,
3004
+ description:
3005
+ "Authenticate npm without sharing token contents, then rerun the exact bootstrap command.",
3006
+ },
3007
+ ],
3008
+ };
3009
+ }
3010
+ const tempRoot = fs.mkdtempSync(
3011
+ path.join(os.tmpdir(), "buildchain-paper-bootstrap-"),
3012
+ );
3013
+ try {
3014
+ fs.writeFileSync(
3015
+ path.join(tempRoot, "package.json"),
3016
+ jsonText(
3017
+ bootstrapPackageShape({ packageName: name, version: bootstrapVersion }),
3018
+ ),
3019
+ );
3020
+ const configArgs = userconfig ? ["--userconfig", userconfig] : [];
3021
+ const pack = commandResult(
3022
+ "npm",
3023
+ [
3024
+ "pack",
3025
+ "--dry-run",
3026
+ "--json",
3027
+ "--ignore-scripts",
3028
+ `--registry=${registry}`,
3029
+ ...configArgs,
3030
+ ],
3031
+ { cwd: tempRoot },
3032
+ );
3033
+ if (!pack.ok) {
3034
+ return {
3035
+ ...plan,
3036
+ ok: false,
3037
+ errorCode: "npm-pack-dry-run-failed",
3038
+ publish: { ...plan.publish, status: "blocked" },
3039
+ trust: { status: "blocked", urls: [] },
3040
+ };
3041
+ }
3042
+ const publishDryRun = commandResult(
3043
+ "npm",
3044
+ [
3045
+ "publish",
3046
+ "--dry-run",
3047
+ "--ignore-scripts",
3048
+ "--access",
3049
+ "public",
3050
+ "--tag",
3051
+ "bootstrap",
3052
+ `--registry=${registry}`,
3053
+ ...configArgs,
3054
+ ],
3055
+ { cwd: tempRoot },
3056
+ );
3057
+ if (!publishDryRun.ok) {
3058
+ return {
3059
+ ...plan,
3060
+ ok: false,
3061
+ errorCode: "npm-publish-dry-run-failed",
3062
+ publish: { ...plan.publish, status: "blocked" },
3063
+ trust: { status: "blocked", urls: [] },
3064
+ };
3065
+ }
3066
+ let publishStatus =
3067
+ packageObservation.exists === true ? "existing" : "published";
3068
+ if (packageObservation.exists !== true) {
3069
+ const published = commandResult(
3070
+ "npm",
3071
+ [
3072
+ "publish",
3073
+ "--ignore-scripts",
3074
+ "--access",
3075
+ "public",
3076
+ "--tag",
3077
+ "bootstrap",
3078
+ `--registry=${registry}`,
3079
+ ...configArgs,
3080
+ ],
3081
+ { cwd: tempRoot, timeout: 60000 },
3082
+ );
3083
+ if (!published.ok) {
3084
+ const failure = {
3085
+ ...plan,
3086
+ ok: false,
3087
+ errorCode: "npm-publish-failed",
3088
+ publish: { ...plan.publish, status: "failed" },
3089
+ trust: { status: "blocked", urls: [] },
3090
+ };
3091
+ failure.receipts = [
3092
+ writePaperReceipt(resolvedCwd, PAPER_PATHS.npmBootstrap, failure),
3093
+ ];
3094
+ return failure;
3095
+ }
3096
+ }
3097
+ const trustArgs = [
3098
+ "trust",
3099
+ "github",
3100
+ name,
3101
+ "--repo",
3102
+ repo,
3103
+ "--file",
3104
+ workflow,
3105
+ ...(environment ? ["--env", environment] : []),
3106
+ "--allow-publish",
3107
+ "--yes",
3108
+ "--json",
3109
+ `--registry=${registry}`,
3110
+ ...configArgs,
3111
+ ];
3112
+ const trustCommand = commandResult("npm", trustArgs, {
3113
+ cwd: resolvedCwd,
3114
+ timeout: 60000,
3115
+ });
3116
+ const parsedTrust = safeParseJson(trustCommand.stdout);
3117
+ const urls = [
3118
+ ...extractUrls(
3119
+ parsedTrust || `${trustCommand.stdout}\n${trustCommand.stderr}`,
3120
+ ),
3121
+ ];
3122
+ const trustList =
3123
+ trustCommand.ok && urls.length === 0
3124
+ ? liveNpmTrustObservation(
3125
+ name,
3126
+ registry,
3127
+ resolvedCwd,
3128
+ expectedPublisher,
3129
+ )
3130
+ : {
3131
+ status: "unknown",
3132
+ configured: null,
3133
+ publishers: [],
3134
+ errorCode:
3135
+ urls.length > 0 ? "web-action-required" : "npm-trust-failed",
3136
+ };
3137
+ const trustStatus =
3138
+ trustCommand.ok && trustList.configured === true
3139
+ ? "configured"
3140
+ : urls.length > 0
3141
+ ? "action-required"
3142
+ : "failed";
3143
+ const packageAfter = liveNpmPackageObservation(name, registry, resolvedCwd);
3144
+ const packageVerified = packageAfter.exists === true;
3145
+ const receipt = {
3146
+ ...plan,
3147
+ ok: trustStatus !== "failed" && packageVerified,
3148
+ dryRun: false,
3149
+ externalMutation: true,
3150
+ errorCode:
3151
+ trustStatus === "failed"
3152
+ ? "npm-trust-failed"
3153
+ : packageVerified
3154
+ ? ""
3155
+ : "npm-package-readback-failed",
3156
+ package: {
3157
+ ...plan.package,
3158
+ existsAfter: packageAfter.exists,
3159
+ observedVersionAfter: packageAfter.version,
3160
+ readbackStatus: packageAfter.status,
3161
+ },
3162
+ publish: {
3163
+ ...plan.publish,
3164
+ status: publishStatus,
3165
+ },
3166
+ trust: {
3167
+ status: trustStatus,
3168
+ urls,
3169
+ expectedPublisher,
3170
+ exactBinding: trustList.exactBinding === true,
3171
+ publishers: trustList.publishers,
3172
+ },
3173
+ nextActions:
3174
+ trustStatus === "configured" && packageVerified
3175
+ ? [
3176
+ {
3177
+ id: "paper-preflight",
3178
+ command: "buildchain paper preflight --json",
3179
+ description:
3180
+ "Verify package existence and Trusted Publisher binding from live read-only sources.",
3181
+ },
3182
+ ]
3183
+ : trustStatus === "action-required" && packageVerified
3184
+ ? [
3185
+ {
3186
+ id: "complete-npm-web-action",
3187
+ command: "",
3188
+ description:
3189
+ "Open the exact npm URL returned below, complete the web step, then rerun preflight.",
3190
+ urls,
3191
+ },
3192
+ ]
3193
+ : !packageVerified
3194
+ ? [
3195
+ {
3196
+ id: "verify-npm-package-readback",
3197
+ command: `npm view ${name} version --json --registry=${registry}`,
3198
+ description:
3199
+ "The mutation returned but the official registry did not prove package existence; do not infer success.",
3200
+ },
3201
+ ]
3202
+ : [
3203
+ {
3204
+ id: "retry-npm-trust",
3205
+ command: `buildchain paper bootstrap npm --execute --confirm-public-package ${name} --json`,
3206
+ description:
3207
+ "The package exists; retry only the idempotent Trusted Publisher configuration path.",
3208
+ },
3209
+ ],
3210
+ };
3211
+ receipt.receipts = [
3212
+ writePaperReceipt(resolvedCwd, PAPER_PATHS.npmBootstrap, receipt),
3213
+ ];
3214
+ if (
3215
+ packageVerified &&
3216
+ (trustStatus === "configured" || trustStatus === "action-required")
3217
+ ) {
3218
+ receipt.receipts.push(
3219
+ writePaperReceipt(resolvedCwd, PAPER_PATHS.npmTrust, receipt),
3220
+ );
3221
+ }
3222
+ return receipt;
3223
+ } finally {
3224
+ fs.rmSync(tempRoot, { recursive: true, force: true });
3225
+ }
3226
+ }