@kungfu-tech/buildchain 4.0.2-alpha.41 → 4.0.2-alpha.43

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.
@@ -0,0 +1,348 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import {
4
+ createBuildchainContractLock,
5
+ createBuildchainContractWorld,
6
+ finalizeBuildchainContractWorld,
7
+ readBuildchainContractWorld,
8
+ sha256Json,
9
+ } from "./buildchain-contract.js";
10
+ import {
11
+ commandResult,
12
+ gitValue,
13
+ readJson,
14
+ sha256Text,
15
+ stableJson,
16
+ } from "./paper-repository.js";
17
+
18
+ export const PAPER_ALPHA_LOCK = ".buildchain/alpha-contract-lock.json";
19
+
20
+ export function resolvePaperNpmRuntimeSha(cwd, identity) {
21
+ if (!identity.version) return "";
22
+ const observed = commandResult(
23
+ "npm",
24
+ [
25
+ "view",
26
+ `${identity.name}@${identity.version}`,
27
+ "gitHead",
28
+ "--json",
29
+ "--registry=https://registry.npmjs.org",
30
+ ],
31
+ { cwd },
32
+ );
33
+ if (!observed.ok) return "";
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(observed.stdout || "null");
37
+ } catch {
38
+ return "";
39
+ }
40
+ const gitHead =
41
+ typeof parsed === "string" ? parsed : String(parsed?.gitHead || "");
42
+ return /^[0-9a-f]{40}$/i.test(gitHead)
43
+ ? gitHead
44
+ : resolvePaperPublishedTagSha(cwd, identity);
45
+ }
46
+
47
+ function resolvePaperPublishedTagSha(cwd, identity) {
48
+ if (
49
+ identity.name !== "@kungfu-tech/buildchain" ||
50
+ !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(
51
+ identity.version,
52
+ )
53
+ )
54
+ return "";
55
+ const ref = `refs/tags/v${identity.version}`;
56
+ const observed = commandResult(
57
+ "git",
58
+ [
59
+ "ls-remote",
60
+ "--exit-code",
61
+ "https://github.com/kungfu-systems/buildchain.git",
62
+ ref,
63
+ `${ref}^{}`,
64
+ ],
65
+ { cwd },
66
+ );
67
+ if (!observed.ok || !observed.stdout) return "";
68
+ const refs = new Map();
69
+ for (const line of observed.stdout.split(/\r?\n/)) {
70
+ const [sha, name, extra] = line.split(/\s+/);
71
+ if (
72
+ !/^[0-9a-f]{40}$/.test(sha) ||
73
+ ![ref, `${ref}^{}`].includes(name) ||
74
+ extra ||
75
+ refs.has(name)
76
+ )
77
+ return "";
78
+ refs.set(name, sha);
79
+ }
80
+ if (!refs.has(ref)) return "";
81
+ return refs.get(`${ref}^{}`) || refs.get(ref);
82
+ }
83
+
84
+ function fileDigestMatches(cwd, relative, digest) {
85
+ if (!relative || !/^sha256:[0-9a-f]{64}$/.test(digest || "")) return false;
86
+ const absolute = path.resolve(cwd, relative);
87
+ return (
88
+ fs.existsSync(absolute) &&
89
+ sha256Text(fs.readFileSync(absolute, "utf8")) === digest
90
+ );
91
+ }
92
+
93
+ function workflowErrors(cwd, authority, workflow) {
94
+ if (!workflow?.path || !workflow?.sourceDigest)
95
+ return ["paper workflow authority is incomplete"];
96
+ if (!fileDigestMatches(cwd, workflow.path, workflow.sourceDigest))
97
+ return [`paper workflow source digest mismatch: ${workflow.path}`];
98
+ const floating = /^v4(?:-alpha)?$/.test(authority.runtime?.ref || "");
99
+ const refs = floating
100
+ ? workflow.reusablePath === ".github/workflows/paper-release-sealed.yml"
101
+ ? ["v4-alpha", "v4"]
102
+ : ["v4-alpha"]
103
+ : [authority.runtime.resolvedSha];
104
+ const text = fs.readFileSync(path.resolve(cwd, workflow.path), "utf8");
105
+ const errors = [];
106
+ if (
107
+ refs.some(
108
+ (ref) =>
109
+ !text.includes(
110
+ `uses: ${authority.runtime.repository}/${workflow.reusablePath}@${ref}\n`,
111
+ ),
112
+ )
113
+ )
114
+ errors.push(
115
+ `paper workflow reusable source is not exact: ${workflow.path}`,
116
+ );
117
+ if (
118
+ (floating || workflow.reusablePath !== ".github/workflows/check.yml") &&
119
+ refs.some((ref) => !text.includes(`buildchain-ref: ${ref}\n`))
120
+ )
121
+ errors.push(`paper workflow runtime input is not exact: ${workflow.path}`);
122
+ if (
123
+ floating &&
124
+ [
125
+ ...text.matchAll(/uses:\s*kungfu-systems\/buildchain\/[^\s@]+@([^\s]+)/g),
126
+ ].some((match) => !refs.includes(match[1]))
127
+ )
128
+ errors.push(
129
+ `paper workflow contains an unaccepted Buildchain selector: ${workflow.path}`,
130
+ );
131
+ return errors;
132
+ }
133
+
134
+ function channelLockMatches(cwd, authority, ref, expectedPath) {
135
+ const channel = authority.admission?.channels?.[ref];
136
+ const lock = readJson(path.resolve(cwd, expectedPath)).value;
137
+ const entry = readJson(
138
+ path.resolve(cwd, ".buildchain/paper/agent-entry.json"),
139
+ ).value;
140
+ return (
141
+ channel?.ref === ref &&
142
+ channel?.lockPath === expectedPath &&
143
+ fileDigestMatches(cwd, expectedPath, channel?.lockDigest) &&
144
+ lock?.buildchain?.ref === ref &&
145
+ lock?.buildchain?.resolvedSha === channel?.resolvedSha &&
146
+ lock?.buildchain?.majorLine === "v4" &&
147
+ entry?.runtime?.channels?.[ref] === channel?.resolvedSha
148
+ );
149
+ }
150
+
151
+ export function paperProvisioningWorkflowErrors(cwd, authority) {
152
+ const errors = [
153
+ authority.workflows?.build,
154
+ authority.workflows?.verify,
155
+ authority.workflows?.release,
156
+ ].flatMap((workflow) => workflowErrors(cwd, authority, workflow));
157
+ if (
158
+ !fileDigestMatches(
159
+ cwd,
160
+ authority.admission?.contractLockPath || ".buildchain/contract-lock.json",
161
+ authority.admission?.contractLockDigest,
162
+ )
163
+ )
164
+ errors.push("paper contract lock bytes differ from provisioning authority");
165
+ if (/^v4(?:-alpha)?$/.test(authority.runtime?.ref || "")) {
166
+ for (const [ref, lockPath] of [
167
+ ["v4", ".buildchain/contract-lock.json"],
168
+ ["v4-alpha", PAPER_ALPHA_LOCK],
169
+ ]) {
170
+ if (!channelLockMatches(cwd, authority, ref, lockPath))
171
+ errors.push(
172
+ `paper ${ref} contract lock is not bound to provisioning authority`,
173
+ );
174
+ }
175
+ }
176
+ return errors;
177
+ }
178
+
179
+ export function paperRuntimeSourceMatches(runtime, sha, mode, admission) {
180
+ return (
181
+ runtime?.sourceSha === sha ||
182
+ (mode === "ci" &&
183
+ /^4\./.test(runtime?.version || "") &&
184
+ ([runtime?.channels?.v4, runtime?.channels?.["v4-alpha"]].includes(sha) ||
185
+ (admission?.compatible === true &&
186
+ admission.sha === sha &&
187
+ /^v4(?:-alpha)?$/.test(admission.ref))))
188
+ );
189
+ }
190
+
191
+ export function selectPaperRuntime(runtime, authority) {
192
+ const channel = Object.values(authority?.admission?.channels || {}).find(
193
+ (entry) => entry.resolvedSha === runtime.resolvedSha,
194
+ );
195
+ const ref =
196
+ channel?.ref || (runtime.version.includes("-") ? "v4-alpha" : "v4");
197
+ return authority?.admission?.channels?.[ref] ? { ...runtime, ref } : runtime;
198
+ }
199
+
200
+ export function paperRuntimeLockPath(cwd, runtime, authority) {
201
+ return path.resolve(
202
+ cwd,
203
+ authority?.admission?.channels?.[runtime.ref]?.lockPath ||
204
+ ".buildchain/contract-lock.json",
205
+ );
206
+ }
207
+
208
+ function channelWorld(root, ref, current) {
209
+ if (current) return current;
210
+ const sha = gitValue(root, [
211
+ "rev-parse",
212
+ "--verify",
213
+ `refs/tags/${ref}^{commit}`,
214
+ ]);
215
+ const text =
216
+ sha &&
217
+ gitValue(root, ["show", `${sha}:dist/site/buildchain-contract.json`]);
218
+ if (!/^[0-9a-f]{40}$/.test(sha) || !text) {
219
+ throw new Error(
220
+ `Paper migration needs the fetched ${ref} tag or an explicit ${ref === "v4" ? "stable" : "alpha"} Buildchain root`,
221
+ );
222
+ }
223
+ const published = JSON.parse(text);
224
+ const world = Array.isArray(published.compatibilityFacts)
225
+ ? finalizeBuildchainContractWorld(published)
226
+ : {
227
+ ...published,
228
+ contractDigest: `sha256:${sha256Json({ ...published, contractDigest: undefined, compatibilityDigest: undefined })}`,
229
+ compatibilityDigest: `sha256:${sha256Json({ schemaVersion: published.schemaVersion, contract: published.contract, majorLine: published.majorLine, surfaces: published.surfaces.map(({ id, kind, breakingDigest }) => ({ id, kind, breakingDigest })) })}`,
230
+ };
231
+ if (
232
+ published.contractDigest !== world.contractDigest ||
233
+ published.compatibilityDigest !== world.compatibilityDigest
234
+ ) {
235
+ throw new Error(`Paper ${ref} contract digest mismatch`);
236
+ }
237
+ return {
238
+ sha,
239
+ world,
240
+ acceptedAt: gitValue(root, ["show", "-s", "--format=%cI", sha]),
241
+ };
242
+ }
243
+
244
+ function explicitWorld(root, ref) {
245
+ const sha = gitValue(root, ["rev-parse", "HEAD"]);
246
+ if (!/^[0-9a-f]{40}$/.test(sha))
247
+ throw new Error("Paper channel root must have exact Git provenance");
248
+ if (gitValue(root, ["status", "--porcelain", "--untracked-files=no"]))
249
+ throw new Error("Paper channel root must have committed runtime bytes");
250
+ const pkg = readJson(path.join(root, "package.json")).value;
251
+ if (
252
+ pkg?.name !== "@kungfu-tech/buildchain" ||
253
+ !/^4\./.test(pkg?.version || "") ||
254
+ pkg.version.includes("-") !== (ref === "v4-alpha")
255
+ )
256
+ throw new Error(`Paper channel root does not belong to ${ref}`);
257
+ const contractPath = path.join(root, "dist/site/buildchain-contract.json");
258
+ return {
259
+ sha,
260
+ world: fs.existsSync(contractPath)
261
+ ? readBuildchainContractWorld(contractPath)
262
+ : createBuildchainContractWorld({ root }),
263
+ acceptedAt: gitValue(root, ["show", "-s", "--format=%cI", sha]),
264
+ };
265
+ }
266
+
267
+ export function paperV4Channels({
268
+ cwd,
269
+ buildchainRoot,
270
+ buildchainVersion,
271
+ buildchainSha,
272
+ contractWorld,
273
+ acceptedAt,
274
+ stableBuildchainRoot = "",
275
+ alphaBuildchainRoot = "",
276
+ }) {
277
+ const selectedRef = buildchainVersion.includes("-") ? "v4-alpha" : "v4";
278
+ const current = {
279
+ sha: buildchainSha,
280
+ world: contractWorld,
281
+ acceptedAt,
282
+ };
283
+ const channels = {};
284
+ for (const [ref, lockPath, explicitRoot] of [
285
+ ["v4", ".buildchain/contract-lock.json", stableBuildchainRoot],
286
+ ["v4-alpha", PAPER_ALPHA_LOCK, alphaBuildchainRoot],
287
+ ]) {
288
+ const resolved = explicitRoot
289
+ ? explicitWorld(explicitRoot, ref)
290
+ : channelWorld(buildchainRoot, ref, ref === selectedRef ? current : null);
291
+ if (resolved.world.majorLine !== "v4")
292
+ throw new Error(`Paper ${ref} contract must belong to v4`);
293
+ const existing = readJson(path.join(cwd, lockPath)).value;
294
+ const lock = createBuildchainContractLock({
295
+ buildchainRef: ref,
296
+ resolvedSha: resolved.sha,
297
+ contractWorld: resolved.world,
298
+ acceptedAt:
299
+ existing?.buildchain?.resolvedSha === resolved.sha
300
+ ? existing.buildchain.acceptedAt
301
+ : new Date(resolved.acceptedAt).toISOString(),
302
+ });
303
+ const content = `${JSON.stringify(lock, null, 2)}\n`;
304
+ channels[ref] = {
305
+ ref,
306
+ resolvedSha: resolved.sha,
307
+ lockPath,
308
+ lockDigest: sha256Text(content),
309
+ content,
310
+ };
311
+ }
312
+ return { selectedRef, channels };
313
+ }
314
+
315
+ export function bindPaperV4Authority(authority, channelPlan, files) {
316
+ const { selectedRef, channels } = channelPlan;
317
+ const entryPath = authority.agentEntry.policyPath;
318
+ const entry = JSON.parse(files.get(entryPath));
319
+ entry.runtime.channels = Object.fromEntries(
320
+ Object.entries(channels).map(([ref, channel]) => [
321
+ ref,
322
+ channel.resolvedSha,
323
+ ]),
324
+ );
325
+ const { entryDigest: _entryDigest, ...entryPayload } = entry;
326
+ entry.entryDigest = sha256Text(stableJson(entryPayload));
327
+ files.set(entryPath, `${JSON.stringify(entry, null, 2)}\n`);
328
+ authority.agentEntry.policyDigest = sha256Text(files.get(entryPath));
329
+ authority.runtime.ref = selectedRef;
330
+ authority.admission.acceptedRef = selectedRef;
331
+ authority.admission.contractLockPath = channels[selectedRef].lockPath;
332
+ authority.admission.contractLockDigest = channels[selectedRef].lockDigest;
333
+ authority.admission.channels = Object.fromEntries(
334
+ Object.entries(channels).map(([ref, { content: _content, ...channel }]) => [
335
+ ref,
336
+ channel,
337
+ ]),
338
+ );
339
+ for (const [role, workflow] of Object.entries(authority.workflows)) {
340
+ workflow.reusableRef = role === "release" ? "v4" : "v4-alpha";
341
+ workflow.reusableRefs =
342
+ role === "release" ? ["v4", "v4-alpha"] : ["v4-alpha"];
343
+ workflow.sourceDigest = sha256Text(files.get(workflow.path));
344
+ }
345
+ const { authorityDigest: _authorityDigest, ...payload } = authority;
346
+ authority.authorityDigest = sha256Text(stableJson(payload));
347
+ return authority;
348
+ }
@@ -10,6 +10,12 @@ function jsonText(value) {
10
10
  return `${JSON.stringify(value, null, 2)}\n`;
11
11
  }
12
12
 
13
+ export function paperDependencyIgnore(current = "") {
14
+ return /^(?:\/)?node_modules\/?\s*$/m.test(current)
15
+ ? current
16
+ : `${current}${current.endsWith("\n") || !current ? "" : "\n"}node_modules/\n`;
17
+ }
18
+
13
19
  function texEscape(value) {
14
20
  return String(value || "")
15
21
  .replaceAll("\\", "\\textbackslash{}")
@@ -380,6 +386,8 @@ function planPaperMigration(
380
386
  buildchainRoot = process.cwd(),
381
387
  buildchainVersion = "",
382
388
  buildchainSha = "",
389
+ stableBuildchainRoot = "",
390
+ alphaBuildchainRoot = "",
383
391
  } = {},
384
392
  ) {
385
393
  const resolvedCwd = path.resolve(cwd);
@@ -407,6 +415,8 @@ function planPaperMigration(
407
415
  buildchainRoot,
408
416
  buildchainVersion,
409
417
  buildchainSha,
418
+ stableBuildchainRoot,
419
+ alphaBuildchainRoot,
410
420
  }),
411
421
  ].map(([relativePath, content]) => {
412
422
  const target = path.resolve(resolvedCwd, relativePath);
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ import { bindPaperV4Authority, paperV4Channels, selectPaperRuntime, paperRuntimeLockPath, paperProvisioningWorkflowErrors, resolvePaperNpmRuntimeSha } from "./paper-runtime-channels.js";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -40,6 +41,7 @@ import {
40
41
  import {
41
42
  createPaperScaffoldOperations,
42
43
  managedPaperPackageJson,
44
+ paperDependencyIgnore,
43
45
  scaffoldMainTex,
44
46
  scaffoldMakefile,
45
47
  scaffoldMap,
@@ -255,23 +257,7 @@ export function resolvePaperRuntimeGitSha(
255
257
  const value = resolvePaperBuildchainSha(buildchainRoot);
256
258
  if (GIT_SHA_PATTERN.test(value)) return value;
257
259
  const identity = buildchainPackageIdentity(buildchainRoot, buildchainVersion);
258
- if (!identity.version) return "";
259
- const observed = commandResult(
260
- "npm",
261
- [
262
- "view",
263
- `${identity.name}@${identity.version}`,
264
- "gitHead",
265
- "--json",
266
- `--registry=${NPM_REGISTRY}`,
267
- ],
268
- { cwd: buildchainRoot },
269
- );
270
- if (!observed.ok) return "";
271
- const parsed = safeParseJson(observed.stdout);
272
- const gitHead =
273
- typeof parsed === "string" ? parsed : String(parsed?.gitHead || "");
274
- return GIT_SHA_PATTERN.test(gitHead) ? gitHead : "";
260
+ return resolvePaperNpmRuntimeSha(buildchainRoot, identity);
275
261
  }
276
262
 
277
263
  function runtimeAcceptedAt(buildchainRoot, sha, buildchainVersion = "") {
@@ -700,7 +686,7 @@ function scaffoldFiles({
700
686
  ["LICENSE", licenseText],
701
687
  [
702
688
  ".gitignore",
703
- "_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",
689
+ "node_modules/\n_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",
704
690
  ],
705
691
  ]);
706
692
  }
@@ -710,6 +696,8 @@ function migrationFiles({
710
696
  buildchainRoot,
711
697
  buildchainVersion,
712
698
  buildchainSha,
699
+ stableBuildchainRoot,
700
+ alphaBuildchainRoot,
713
701
  }) {
714
702
  const configResult = paperConfig(cwd);
715
703
  if (configResult.error) {
@@ -817,7 +805,26 @@ function migrationFiles({
817
805
  [PAPER_PATHS.provisioningAuthority, jsonText(provisioningAuthority)],
818
806
  ...agentEntry,
819
807
  ["package.json", jsonText(packageJson)],
808
+ [".gitignore", paperDependencyIgnore(fs.existsSync(path.join(cwd, ".gitignore")) ? fs.readFileSync(path.join(cwd, ".gitignore"), "utf8") : "")],
820
809
  ]);
810
+ if (runtimeIdentity.version.startsWith("4.")) {
811
+ const channelPlan = paperV4Channels({
812
+ cwd, buildchainRoot, buildchainVersion: runtimeIdentity.version,
813
+ buildchainSha: runtimeSha, contractWorld: runtimeContractWorld(buildchainRoot),
814
+ acceptedAt: contractLock.buildchain.acceptedAt,
815
+ stableBuildchainRoot, alphaBuildchainRoot,
816
+ });
817
+ for (const channel of Object.values(channelPlan.channels)) files.set(channel.lockPath, channel.content);
818
+ for (const workflowPath of [PAPER_PATHS.buildWorkflow, PAPER_PATHS.verifyWorkflow]) {
819
+ files.set(workflowPath, files.get(workflowPath).replaceAll(runtimeSha, "v4-alpha").replaceAll(".buildchain/contract-lock.json", ".buildchain/alpha-contract-lock.json"));
820
+ }
821
+ const releaseStart = releaseWorkflow.indexOf(" paper-release:\n");
822
+ const releaseJob = releaseWorkflow.slice(releaseStart);
823
+ const alphaJob = releaseJob.replace(" paper-release:\n", " paper-release-alpha:\n if: ${{ startsWith(github.ref_name, 'alpha/') }}\n").replaceAll(runtimeSha, "v4-alpha").replaceAll(".buildchain/contract-lock.json", ".buildchain/alpha-contract-lock.json");
824
+ const stableJob = releaseJob.replace(" paper-release:\n", " paper-release:\n if: ${{ startsWith(github.ref_name, 'release/') }}\n").replaceAll(runtimeSha, "v4");
825
+ files.set(PAPER_PATHS.releaseWorkflow, `${releaseWorkflow.slice(0, releaseStart)}${alphaJob}\n${stableJob}`);
826
+ files.set(PAPER_PATHS.provisioningAuthority, jsonText(bindPaperV4Authority(provisioningAuthority, channelPlan, files)));
827
+ }
821
828
  return files;
822
829
  }
823
830
 
@@ -1326,6 +1333,7 @@ function validatePaperProvisioningAuthority(cwd) {
1326
1333
  }
1327
1334
  const value = source.value;
1328
1335
  const errors = [];
1336
+ const floating = /^v4(?:-alpha)?$/.test(value.runtime?.ref || "");
1329
1337
  if (value.contract !== PAPER_PROVISIONING_CONTRACT) {
1330
1338
  errors.push("paper provisioning authority contract mismatch");
1331
1339
  }
@@ -1338,8 +1346,8 @@ function validatePaperProvisioningAuthority(cwd) {
1338
1346
  }
1339
1347
  if (
1340
1348
  !GIT_SHA_PATTERN.test(String(value.runtime?.resolvedSha || "")) ||
1341
- value.runtime?.ref !== value.runtime?.resolvedSha ||
1342
- value.admission?.acceptedRef !== value.runtime?.resolvedSha ||
1349
+ (!floating && value.runtime?.ref !== value.runtime?.resolvedSha) ||
1350
+ value.admission?.acceptedRef !== value.runtime?.ref ||
1343
1351
  value.admission?.acceptedSha !== value.runtime?.resolvedSha
1344
1352
  ) {
1345
1353
  errors.push("paper runtime and admission are not bound to one exact SHA");
@@ -1397,49 +1405,7 @@ function validatePaperProvisioningAuthority(cwd) {
1397
1405
  errors.push(`paper agent-entry source digest mismatch: ${entryPath}`);
1398
1406
  }
1399
1407
  }
1400
- for (const workflow of [
1401
- value.workflows?.build,
1402
- value.workflows?.verify,
1403
- value.workflows?.release,
1404
- ]) {
1405
- if (!workflow?.path || !workflow?.sourceDigest) {
1406
- errors.push("paper workflow authority is incomplete");
1407
- continue;
1408
- }
1409
- const absolute = path.resolve(cwd, workflow.path);
1410
- if (
1411
- !fs.existsSync(absolute) ||
1412
- sha256File(absolute) !== workflow.sourceDigest
1413
- ) {
1414
- errors.push(`paper workflow source digest mismatch: ${workflow.path}`);
1415
- continue;
1416
- }
1417
- const text = fs.readFileSync(absolute, "utf8");
1418
- const expectedUse = `${value.runtime.repository}/${workflow.reusablePath}@${value.runtime.resolvedSha}`;
1419
- if (!text.includes(`uses: ${expectedUse}`)) {
1420
- errors.push(
1421
- `paper workflow reusable source is not exact: ${workflow.path}`,
1422
- );
1423
- }
1424
- if (
1425
- workflow.reusablePath !== ".github/workflows/check.yml" &&
1426
- !text.includes(`buildchain-ref: ${value.runtime.resolvedSha}`)
1427
- ) {
1428
- errors.push(
1429
- `paper workflow runtime input is not exact: ${workflow.path}`,
1430
- );
1431
- }
1432
- }
1433
- const lockPath = path.resolve(
1434
- cwd,
1435
- value.admission?.contractLockPath || PAPER_PATHS.contractLock,
1436
- );
1437
- if (
1438
- !fs.existsSync(lockPath) ||
1439
- sha256File(lockPath) !== value.admission?.contractLockDigest
1440
- ) {
1441
- errors.push("paper contract lock bytes differ from provisioning authority");
1442
- }
1408
+ errors.push(...paperProvisioningWorkflowErrors(cwd, value));
1443
1409
  return {
1444
1410
  exists: true,
1445
1411
  valid: errors.length === 0,
@@ -2077,12 +2043,11 @@ export function collectPaperPreflight({
2077
2043
  } catch (error) {
2078
2044
  validationError = error.message;
2079
2045
  }
2080
- const runtime = runtimeFacts({
2046
+ const runtime = selectPaperRuntime(runtimeFacts({
2081
2047
  buildchainRoot, buildchainVersion,
2082
2048
  buildchainRef: provisioning.value?.runtime?.ref || buildchainRef, buildchainSha,
2083
- });
2084
- const agentEntry = collectPaperAgentEntry({ cwd: resolvedCwd, buildchainSha: runtime.resolvedSha, mode: agentEntryMode });
2085
- const lockPath = path.resolve(resolvedCwd, PAPER_PATHS.contractLock);
2049
+ }), provisioning.value);
2050
+ const lockPath = paperRuntimeLockPath(resolvedCwd, runtime, provisioning.value);
2086
2051
  let lockEvaluation = {
2087
2052
  status: "missing-lock",
2088
2053
  compatible: false,
@@ -2109,6 +2074,7 @@ export function collectPaperPreflight({
2109
2074
  reasons: [error.message],
2110
2075
  };
2111
2076
  }
2077
+ const agentEntry = collectPaperAgentEntry({ cwd: resolvedCwd, buildchainSha: runtime.resolvedSha, mode: agentEntryMode, runtimeAdmission: { compatible: lockEvaluation.compatible, sha: runtime.resolvedSha, ref: runtime.ref } });
2112
2078
  const source = {
2113
2079
  repositoryRoot: gitValue(resolvedCwd, ["rev-parse", "--show-toplevel"]),
2114
2080
  head: gitValue(resolvedCwd, ["rev-parse", "HEAD"]),
@@ -418,6 +418,8 @@ function runMigration(options) {
418
418
  options.buildchainVersion,
419
419
  ),
420
420
  buildchainSha: options.buildchainSha,
421
+ stableBuildchainRoot: readFlag(options.args, "stable-buildchain-root", ""),
422
+ alphaBuildchainRoot: readFlag(options.args, "alpha-buildchain-root", ""),
421
423
  });
422
424
  return options.args.some((entry) => ["--write", "--execute"].includes(entry))
423
425
  ? writePaperMigration(plan)