@kungfu-tech/buildchain 3.0.3-alpha.0 → 3.0.3-alpha.2

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