@clossys/launcher 0.3.0 → 0.4.0

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 (286) hide show
  1. package/README.md +1328 -60
  2. package/contracts/conversation-contract.md +2 -1
  3. package/contracts/product-ci-workflow.yml +74 -0
  4. package/contracts/repository-inventory.json +53 -0
  5. package/dist/admission-fixture.d.ts +168 -0
  6. package/dist/admission-fixture.d.ts.map +1 -0
  7. package/dist/admission-fixture.js +453 -0
  8. package/dist/admission-fixture.js.map +1 -0
  9. package/dist/admission.d.ts +124 -0
  10. package/dist/admission.d.ts.map +1 -0
  11. package/dist/admission.js +799 -0
  12. package/dist/admission.js.map +1 -0
  13. package/dist/agents-guide.d.ts +9 -0
  14. package/dist/agents-guide.d.ts.map +1 -0
  15. package/dist/agents-guide.js +26 -0
  16. package/dist/agents-guide.js.map +1 -0
  17. package/dist/apply-command-options.check.d.ts +12 -0
  18. package/dist/apply-command-options.check.d.ts.map +1 -0
  19. package/dist/apply-command-options.check.js +20 -0
  20. package/dist/apply-command-options.check.js.map +1 -0
  21. package/dist/apply-plan-cli.d.ts +39 -1
  22. package/dist/apply-plan-cli.d.ts.map +1 -1
  23. package/dist/apply-plan-cli.js +432 -15
  24. package/dist/apply-plan-cli.js.map +1 -1
  25. package/dist/apply-plan.d.ts +46 -59
  26. package/dist/apply-plan.d.ts.map +1 -1
  27. package/dist/apply-plan.js +112 -97
  28. package/dist/apply-plan.js.map +1 -1
  29. package/dist/apply-step-fixture.d.ts +87 -0
  30. package/dist/apply-step-fixture.d.ts.map +1 -0
  31. package/dist/apply-step-fixture.js +199 -0
  32. package/dist/apply-step-fixture.js.map +1 -0
  33. package/dist/apply-store.d.ts +93 -0
  34. package/dist/apply-store.d.ts.map +1 -0
  35. package/dist/apply-store.js +625 -0
  36. package/dist/apply-store.js.map +1 -0
  37. package/dist/approval-sheet.d.ts +21 -0
  38. package/dist/approval-sheet.d.ts.map +1 -0
  39. package/dist/approval-sheet.js +157 -0
  40. package/dist/approval-sheet.js.map +1 -0
  41. package/dist/body-command.d.ts +42 -0
  42. package/dist/body-command.d.ts.map +1 -0
  43. package/dist/body-command.js +143 -0
  44. package/dist/body-command.js.map +1 -0
  45. package/dist/change-set-contract.d.ts +381 -0
  46. package/dist/change-set-contract.d.ts.map +1 -0
  47. package/dist/change-set-contract.js +738 -0
  48. package/dist/change-set-contract.js.map +1 -0
  49. package/dist/change-set-digest.d.ts +28 -0
  50. package/dist/change-set-digest.d.ts.map +1 -0
  51. package/dist/change-set-digest.js +65 -0
  52. package/dist/change-set-digest.js.map +1 -0
  53. package/dist/check-cli.d.ts.map +1 -1
  54. package/dist/check-cli.js +14 -3
  55. package/dist/check-cli.js.map +1 -1
  56. package/dist/cli.d.ts +17 -6
  57. package/dist/cli.d.ts.map +1 -1
  58. package/dist/cli.js +84 -23
  59. package/dist/cli.js.map +1 -1
  60. package/dist/core.d.ts +79 -22
  61. package/dist/core.d.ts.map +1 -1
  62. package/dist/core.js +843 -268
  63. package/dist/core.js.map +1 -1
  64. package/dist/dry-materialize.d.ts +63 -0
  65. package/dist/dry-materialize.d.ts.map +1 -0
  66. package/dist/dry-materialize.js +330 -0
  67. package/dist/dry-materialize.js.map +1 -0
  68. package/dist/generated/contract-schema.generated.d.ts +97 -0
  69. package/dist/generated/contract-schema.generated.d.ts.map +1 -0
  70. package/dist/generated/contract-schema.generated.js +496 -0
  71. package/dist/generated/contract-schema.generated.js.map +1 -0
  72. package/dist/generated/package-scope.generated.d.ts +6 -0
  73. package/dist/generated/package-scope.generated.d.ts.map +1 -0
  74. package/dist/generated/package-scope.generated.js +10 -0
  75. package/dist/generated/package-scope.generated.js.map +1 -0
  76. package/dist/generated/plan-contracts.generated.d.ts +3 -0
  77. package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
  78. package/dist/generated/plan-contracts.generated.js +2840 -0
  79. package/dist/generated/plan-contracts.generated.js.map +1 -0
  80. package/dist/host.d.ts.map +1 -1
  81. package/dist/host.js +11 -0
  82. package/dist/host.js.map +1 -1
  83. package/dist/identity.d.ts +15 -0
  84. package/dist/identity.d.ts.map +1 -0
  85. package/dist/identity.js +48 -0
  86. package/dist/identity.js.map +1 -0
  87. package/dist/index.d.ts +34 -5
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +19 -2
  90. package/dist/index.js.map +1 -1
  91. package/dist/inventory-adoption.d.ts +24 -5
  92. package/dist/inventory-adoption.d.ts.map +1 -1
  93. package/dist/inventory-adoption.js +70 -25
  94. package/dist/inventory-adoption.js.map +1 -1
  95. package/dist/inventory-choice.d.ts +40 -0
  96. package/dist/inventory-choice.d.ts.map +1 -0
  97. package/dist/inventory-choice.js +156 -0
  98. package/dist/inventory-choice.js.map +1 -0
  99. package/dist/inventory-contract.d.ts +89 -0
  100. package/dist/inventory-contract.d.ts.map +1 -0
  101. package/dist/inventory-contract.js +121 -0
  102. package/dist/inventory-contract.js.map +1 -0
  103. package/dist/key-editor.d.ts +30 -0
  104. package/dist/key-editor.d.ts.map +1 -0
  105. package/dist/key-editor.js +445 -0
  106. package/dist/key-editor.js.map +1 -0
  107. package/dist/ledger-contract.d.ts +187 -0
  108. package/dist/ledger-contract.d.ts.map +1 -0
  109. package/dist/ledger-contract.js +532 -0
  110. package/dist/ledger-contract.js.map +1 -0
  111. package/dist/ledger-trust.d.ts +90 -0
  112. package/dist/ledger-trust.d.ts.map +1 -0
  113. package/dist/ledger-trust.js +198 -0
  114. package/dist/ledger-trust.js.map +1 -0
  115. package/dist/lockfile-invariants.d.ts +48 -0
  116. package/dist/lockfile-invariants.d.ts.map +1 -0
  117. package/dist/lockfile-invariants.js +375 -0
  118. package/dist/lockfile-invariants.js.map +1 -0
  119. package/dist/lockfile-readers.d.ts +72 -0
  120. package/dist/lockfile-readers.d.ts.map +1 -0
  121. package/dist/lockfile-readers.js +713 -0
  122. package/dist/lockfile-readers.js.map +1 -0
  123. package/dist/lockfile-regen.d.ts +106 -0
  124. package/dist/lockfile-regen.d.ts.map +1 -0
  125. package/dist/lockfile-regen.js +760 -0
  126. package/dist/lockfile-regen.js.map +1 -0
  127. package/dist/lockfile-tool-env.d.ts +29 -0
  128. package/dist/lockfile-tool-env.d.ts.map +1 -0
  129. package/dist/lockfile-tool-env.js +111 -0
  130. package/dist/lockfile-tool-env.js.map +1 -0
  131. package/dist/materialize.d.ts +113 -0
  132. package/dist/materialize.d.ts.map +1 -0
  133. package/dist/materialize.js +840 -0
  134. package/dist/materialize.js.map +1 -0
  135. package/dist/observe-repository.d.ts +90 -0
  136. package/dist/observe-repository.d.ts.map +1 -0
  137. package/dist/observe-repository.js +1367 -0
  138. package/dist/observe-repository.js.map +1 -0
  139. package/dist/plan-bundle-setup-fixture.d.ts +68 -0
  140. package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
  141. package/dist/plan-bundle-setup-fixture.js +167 -0
  142. package/dist/plan-bundle-setup-fixture.js.map +1 -0
  143. package/dist/plan-bundle.d.ts +250 -0
  144. package/dist/plan-bundle.d.ts.map +1 -0
  145. package/dist/plan-bundle.js +827 -0
  146. package/dist/plan-bundle.js.map +1 -0
  147. package/dist/plan-command.d.ts +29 -0
  148. package/dist/plan-command.d.ts.map +1 -0
  149. package/dist/plan-command.js +493 -0
  150. package/dist/plan-command.js.map +1 -0
  151. package/dist/plan-contract.d.ts +153 -0
  152. package/dist/plan-contract.d.ts.map +1 -0
  153. package/dist/plan-contract.js +61 -0
  154. package/dist/plan-contract.js.map +1 -0
  155. package/dist/plan-digest.d.ts +25 -0
  156. package/dist/plan-digest.d.ts.map +1 -0
  157. package/dist/plan-digest.js +106 -0
  158. package/dist/plan-digest.js.map +1 -0
  159. package/dist/plan-rules.d.ts +23 -0
  160. package/dist/plan-rules.d.ts.map +1 -0
  161. package/dist/plan-rules.js +177 -0
  162. package/dist/plan-rules.js.map +1 -0
  163. package/dist/planned-bundle.d.ts +20 -0
  164. package/dist/planned-bundle.d.ts.map +1 -0
  165. package/dist/planned-bundle.js +191 -0
  166. package/dist/planned-bundle.js.map +1 -0
  167. package/dist/product-repository.d.ts +4 -0
  168. package/dist/product-repository.d.ts.map +1 -1
  169. package/dist/product-repository.js +9 -1
  170. package/dist/product-repository.js.map +1 -1
  171. package/dist/provenance-gate.d.ts +48 -0
  172. package/dist/provenance-gate.d.ts.map +1 -0
  173. package/dist/provenance-gate.js +324 -0
  174. package/dist/provenance-gate.js.map +1 -0
  175. package/dist/pull-request-body.d.ts +45 -0
  176. package/dist/pull-request-body.d.ts.map +1 -0
  177. package/dist/pull-request-body.js +232 -0
  178. package/dist/pull-request-body.js.map +1 -0
  179. package/dist/registry-snapshot.d.ts +141 -0
  180. package/dist/registry-snapshot.d.ts.map +1 -0
  181. package/dist/registry-snapshot.js +483 -0
  182. package/dist/registry-snapshot.js.map +1 -0
  183. package/dist/release-age-edit.d.ts +52 -0
  184. package/dist/release-age-edit.d.ts.map +1 -0
  185. package/dist/release-age-edit.js +413 -0
  186. package/dist/release-age-edit.js.map +1 -0
  187. package/dist/root-entries.d.ts +36 -0
  188. package/dist/root-entries.d.ts.map +1 -0
  189. package/dist/root-entries.js +80 -0
  190. package/dist/root-entries.js.map +1 -0
  191. package/dist/setup-template-scripts.d.ts +34 -0
  192. package/dist/setup-template-scripts.d.ts.map +1 -0
  193. package/dist/setup-template-scripts.js +557 -0
  194. package/dist/setup-template-scripts.js.map +1 -0
  195. package/dist/setup-templates.d.ts +54 -0
  196. package/dist/setup-templates.d.ts.map +1 -0
  197. package/dist/setup-templates.js +427 -0
  198. package/dist/setup-templates.js.map +1 -0
  199. package/dist/skills.d.ts +34 -1
  200. package/dist/skills.d.ts.map +1 -1
  201. package/dist/skills.js +129 -17
  202. package/dist/skills.js.map +1 -1
  203. package/dist/status.d.ts +63 -0
  204. package/dist/status.d.ts.map +1 -0
  205. package/dist/status.js +539 -0
  206. package/dist/status.js.map +1 -0
  207. package/dist/types.d.ts +151 -13
  208. package/dist/types.d.ts.map +1 -1
  209. package/package.json +4 -5
  210. package/skeleton/README.md +14 -9
  211. package/skeleton/package.json +2 -1
  212. package/skill/SKILL.md +20 -16
  213. package/skill-catalogue/advisor/SKILL.md +100 -11
  214. package/skill-catalogue/architect/SKILL.md +2 -13
  215. package/skill-catalogue/bouncer/SKILL.md +2 -13
  216. package/skill-catalogue/builder/SKILL.md +2 -13
  217. package/skill-catalogue/butler/SKILL.md +2 -13
  218. package/skill-catalogue/controller/SKILL.md +2 -13
  219. package/skill-catalogue/customer/SKILL.md +4 -13
  220. package/skill-catalogue/designer/SKILL.md +9 -14
  221. package/skill-catalogue/giver/SKILL.md +2 -13
  222. package/skill-catalogue/influencer/SKILL.md +2 -13
  223. package/skill-catalogue/inspector/SKILL.md +2 -13
  224. package/skill-catalogue/integrator/SKILL.md +2 -13
  225. package/skill-catalogue/keeper/SKILL.md +2 -13
  226. package/skill-catalogue/launcher/SKILL.md +20 -16
  227. package/skill-catalogue/locksmith/SKILL.md +2 -13
  228. package/skill-catalogue/messenger/SKILL.md +2 -13
  229. package/skill-catalogue/observer/SKILL.md +2 -13
  230. package/skill-catalogue/publisher/SKILL.md +11 -17
  231. package/skill-catalogue/starter/SKILL.md +3 -13
  232. package/skill-catalogue/strategist/SKILL.md +14 -17
  233. package/skill-catalogue/writer/SKILL.md +7 -14
  234. package/src/admission-fixture.ts +572 -0
  235. package/src/admission.ts +816 -0
  236. package/src/agents-guide.ts +29 -0
  237. package/src/apply-command-options.check.ts +27 -0
  238. package/src/apply-plan-cli.ts +454 -14
  239. package/src/apply-plan.ts +112 -124
  240. package/src/apply-step-fixture.ts +236 -0
  241. package/src/apply-store.ts +584 -0
  242. package/src/approval-sheet.ts +164 -0
  243. package/src/body-command.ts +162 -0
  244. package/src/change-set-contract.ts +937 -0
  245. package/src/change-set-digest.ts +70 -0
  246. package/src/check-cli.ts +14 -3
  247. package/src/cli.ts +90 -22
  248. package/src/core.ts +973 -275
  249. package/src/dry-materialize.ts +353 -0
  250. package/src/generated/contract-schema.generated.ts +520 -0
  251. package/src/generated/package-scope.generated.ts +10 -0
  252. package/src/generated/plan-contracts.generated.ts +2840 -0
  253. package/src/host.ts +10 -0
  254. package/src/identity.ts +51 -0
  255. package/src/index.ts +72 -3
  256. package/src/inventory-adoption.ts +107 -29
  257. package/src/inventory-choice.ts +172 -0
  258. package/src/inventory-contract.ts +166 -0
  259. package/src/key-editor.ts +446 -0
  260. package/src/ledger-contract.ts +637 -0
  261. package/src/ledger-trust.ts +267 -0
  262. package/src/lockfile-invariants.ts +421 -0
  263. package/src/lockfile-readers.ts +749 -0
  264. package/src/lockfile-regen.ts +851 -0
  265. package/src/lockfile-tool-env.ts +131 -0
  266. package/src/materialize.ts +886 -0
  267. package/src/observe-repository.ts +1365 -0
  268. package/src/plan-bundle-setup-fixture.ts +200 -0
  269. package/src/plan-bundle.ts +964 -0
  270. package/src/plan-command.ts +509 -0
  271. package/src/plan-contract.ts +179 -0
  272. package/src/plan-digest.ts +102 -0
  273. package/src/plan-rules.ts +188 -0
  274. package/src/planned-bundle.ts +211 -0
  275. package/src/product-repository.ts +10 -1
  276. package/src/provenance-gate.ts +352 -0
  277. package/src/pull-request-body.ts +261 -0
  278. package/src/registry-snapshot.ts +534 -0
  279. package/src/release-age-edit.ts +430 -0
  280. package/src/root-entries.ts +81 -0
  281. package/src/setup-template-scripts.ts +571 -0
  282. package/src/setup-templates.ts +471 -0
  283. package/src/skills.ts +161 -18
  284. package/src/status.ts +557 -0
  285. package/src/types.ts +148 -13
  286. package/CHANGELOG.md +0 -131
@@ -0,0 +1,840 @@
1
+ // Write an approved repository change set into a clean local clone and verify
2
+ // the result (RFC apply-approved-plan §7 V4 and V8, migration step 3).
3
+ import { spawnSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { decideSetBinding, planPackagesFor, readHubAuthority } from "./admission.js";
8
+ import { AGENTS_GUIDE_PATH, verifyAgentsGuide } from "./agents-guide.js";
9
+ import { storeChangeSet } from "./apply-store.js";
10
+ import { CANONICAL_KEYS, LEDGER_PATH, canonicalOrder, contentDigest, discoveryLinkRole, discoveryLinkTarget, lockfilePath, matchesPathPattern, validateRepositoryChangeSet, } from "./change-set-contract.js";
11
+ import { JsonEditUnstableError, editJsonPointer, valueAtJsonPointer } from "./key-editor.js";
12
+ import { renderInstalledLedger } from "./ledger-contract.js";
13
+ import { trustInstalledLedger } from "./ledger-trust.js";
14
+ import { checkLockfileInvariants } from "./lockfile-invariants.js";
15
+ import { regenerateLockfile } from "./lockfile-regen.js";
16
+ import { verifyReleaseAgeExemption } from "./release-age-edit.js";
17
+ const BRANCH_SHAPE = /^clossys\/apply-[0-9a-f]{12}$/u;
18
+ const DEFAULT_BRANCH_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/u;
19
+ const COMMIT_SHAPE = /^[0-9a-f]{40}$/u;
20
+ const TOOL_VERSION_SHAPE = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-[0-9A-Za-z.-]+)?$/u;
21
+ const LEFTOVER_SKILL = /^\.agents\/skills\/clossys-[^/]+(?:\/.*)?$/u;
22
+ const RESERVED_ROOTS = new Set(["clossys", ".github", ".starter"]);
23
+ const isWhole = (file) => !("derived" in file);
24
+ function result(exitCode, verdict, reason, detail) {
25
+ return detail === undefined ? { exitCode, verdict, reason } : { exitCode, verdict, reason, detail };
26
+ }
27
+ /**
28
+ * The time limit `status` puts on the git calls of the preconditions it shares with verify, through this variable; unset (verify's
29
+ * own case) there is no limit. A call that runs out of time has no status, which is read as a failure.
30
+ */
31
+ export const GIT_TIMEOUT_ENV = "CLOSSYS_LAUNCHER_GIT_TIMEOUT_MS";
32
+ function gitTimeout() {
33
+ const limit = Number(process.env[GIT_TIMEOUT_ENV]);
34
+ return Number.isSafeInteger(limit) && limit > 0 ? limit : undefined;
35
+ }
36
+ function git(root, args) {
37
+ const run = spawnSync("git", ["-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", ...args], {
38
+ cwd: root,
39
+ encoding: "utf8",
40
+ stdio: ["ignore", "pipe", "ignore"],
41
+ timeout: gitTimeout(),
42
+ killSignal: "SIGKILL",
43
+ });
44
+ return { status: run.status ?? 1, stdout: run.stdout ?? "" };
45
+ }
46
+ function safeDefaultBranch(branch) {
47
+ if (branch.includes(":") || branch.includes("+") || branch.includes("*"))
48
+ return false;
49
+ return DEFAULT_BRANCH_SHAPE.test(branch);
50
+ }
51
+ function refusalResult(refusal) {
52
+ return result(refusal.exitCode, refusal.exitCode === 1 ? "violated" : "indeterminate", refusal.reason, refusal.detail);
53
+ }
54
+ function digestAtPath(root, relPath) {
55
+ const path = join(root, relPath);
56
+ try {
57
+ const stat = lstatSync(path);
58
+ if (stat.isSymbolicLink())
59
+ return contentDigest(readlinkSync(path));
60
+ if (stat.isFile())
61
+ return contentDigest(readFileSync(path, "utf8"));
62
+ return null;
63
+ }
64
+ catch (cause) {
65
+ if (cause.code === "ENOENT")
66
+ return null;
67
+ throw cause;
68
+ }
69
+ }
70
+ function gitShowUtf8(root, ref, path) {
71
+ const { status, stdout } = git(root, ["show", `${ref}:${path}`]);
72
+ if (status !== 0)
73
+ return null;
74
+ return stdout;
75
+ }
76
+ function gitShowBytes(root, ref, path) {
77
+ const run = spawnSync("git", ["-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", "show", `${ref}:${path}`], {
78
+ cwd: root,
79
+ stdio: ["ignore", "pipe", "ignore"],
80
+ timeout: gitTimeout(),
81
+ killSignal: "SIGKILL",
82
+ });
83
+ if ((run.status ?? 1) !== 0)
84
+ return null;
85
+ return run.stdout ?? null;
86
+ }
87
+ function declaredPaths(set) {
88
+ const paths = new Set(set.files.map((file) => file.path));
89
+ if (set.keys.length > 0)
90
+ paths.add("package.json");
91
+ return paths;
92
+ }
93
+ function writePaths(set) {
94
+ const paths = set.files.filter(isWhole).filter((file) => file.before !== file.after).map((file) => file.path);
95
+ if (set.keys.length > 0)
96
+ paths.push("package.json");
97
+ paths.push(LEDGER_PATH);
98
+ return paths;
99
+ }
100
+ function hasSymlinkAncestor(root, relPath) {
101
+ const parts = relPath.split("/");
102
+ if (parts.length <= 1)
103
+ return false;
104
+ let current = root;
105
+ for (let index = 0; index < parts.length - 1; index += 1) {
106
+ current = join(current, parts[index]);
107
+ try {
108
+ if (lstatSync(current).isSymbolicLink())
109
+ return true;
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ }
115
+ return false;
116
+ }
117
+ function ensureParentDir(root, relPath) {
118
+ const parent = dirname(relPath);
119
+ if (parent === ".")
120
+ return null;
121
+ const segments = parent.split("/");
122
+ let current = root;
123
+ for (const segment of segments) {
124
+ current = join(current, segment);
125
+ try {
126
+ const stat = lstatSync(current);
127
+ if (stat.isSymbolicLink())
128
+ return result(2, "indeterminate", "symlink-ancestor");
129
+ if (!stat.isDirectory())
130
+ return result(2, "indeterminate", "symlink-ancestor");
131
+ }
132
+ catch (cause) {
133
+ if (cause.code !== "ENOENT")
134
+ throw cause;
135
+ mkdirSync(current);
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+ export function writeRegularFile(root, relPath, text, after) {
141
+ const parent = ensureParentDir(root, relPath);
142
+ if (parent !== null)
143
+ return parent;
144
+ const path = join(root, relPath);
145
+ try {
146
+ if (lstatSync(path).isSymbolicLink())
147
+ return result(2, "indeterminate", "symlink-ancestor");
148
+ }
149
+ catch (cause) {
150
+ if (cause.code !== "ENOENT")
151
+ throw cause;
152
+ }
153
+ writeFileSync(path, text, "utf8");
154
+ try {
155
+ const mode = lstatSync(path).mode & 0o777;
156
+ if ((mode & 0o111) !== 0)
157
+ chmodSync(path, 0o644);
158
+ }
159
+ catch {
160
+ // Best effort.
161
+ }
162
+ const readBack = readFileSync(path, "utf8");
163
+ if (readBack !== text)
164
+ return result(1, "violated", "content-mismatch");
165
+ if (contentDigest(readBack) !== after)
166
+ return result(1, "violated", "content-mismatch");
167
+ return null;
168
+ }
169
+ export function writeSymlink(root, relPath, target, after) {
170
+ const parent = ensureParentDir(root, relPath);
171
+ if (parent !== null)
172
+ return parent;
173
+ const path = join(root, relPath);
174
+ try {
175
+ if (lstatSync(path).isSymbolicLink())
176
+ unlinkSync(path);
177
+ else if (existsSync(path))
178
+ unlinkSync(path);
179
+ }
180
+ catch (cause) {
181
+ if (cause.code !== "ENOENT")
182
+ throw cause;
183
+ }
184
+ symlinkSync(target, path);
185
+ if (contentDigest(readlinkSync(path)) !== after)
186
+ return result(1, "violated", "content-mismatch");
187
+ return null;
188
+ }
189
+ export function removePath(root, relPath) {
190
+ if (hasSymlinkAncestor(root, relPath))
191
+ return result(2, "indeterminate", "symlink-ancestor");
192
+ const path = join(root, relPath);
193
+ try {
194
+ unlinkSync(path);
195
+ }
196
+ catch (cause) {
197
+ if (cause.code === "ENOENT")
198
+ return null;
199
+ throw cause;
200
+ }
201
+ if (existsSync(path))
202
+ return result(1, "violated", "removal-present");
203
+ return null;
204
+ }
205
+ export function resolveCloneRoot(clone) {
206
+ try {
207
+ const stat = lstatSync(clone);
208
+ if (!stat.isDirectory() || stat.isSymbolicLink())
209
+ return result(2, "indeterminate", "missing-clone");
210
+ }
211
+ catch {
212
+ return result(2, "indeterminate", "missing-clone");
213
+ }
214
+ let root;
215
+ try {
216
+ root = realpathSync(clone);
217
+ }
218
+ catch {
219
+ return result(2, "indeterminate", "missing-clone");
220
+ }
221
+ if (root !== clone)
222
+ return result(2, "indeterminate", "missing-clone");
223
+ return { root };
224
+ }
225
+ function checkSymlinksSupported(root) {
226
+ const { status, stdout } = git(root, ["config", "--get", "core.symlinks"]);
227
+ if (status !== 0)
228
+ return null;
229
+ if (stdout.trim() === "false")
230
+ return result(2, "indeterminate", "symlinks-unsupported");
231
+ return null;
232
+ }
233
+ function checkRemoteTip(root, set) {
234
+ const branch = set.repository.defaultBranch;
235
+ if (!safeDefaultBranch(branch))
236
+ return result(2, "indeterminate", "change-set-invalid");
237
+ const fetched = git(root, ["fetch", "--no-tags", "--refmap=", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]);
238
+ if (fetched.status !== 0)
239
+ return result(2, "indeterminate", "remote-tip-unreadable");
240
+ const local = git(root, ["rev-parse", `refs/heads/${branch}`]);
241
+ const remote = git(root, ["rev-parse", `refs/remotes/origin/${branch}`]);
242
+ if (local.status !== 0 || remote.status !== 0)
243
+ return result(2, "indeterminate", "remote-tip-unreadable");
244
+ if (local.stdout.trim() !== remote.stdout.trim())
245
+ return result(1, "violated", "remote-tip-mismatch");
246
+ return null;
247
+ }
248
+ function digestAtRef(root, ref, relPath, mode) {
249
+ if (mode === "120000") {
250
+ const target = gitShowUtf8(root, ref, relPath);
251
+ if (target === null)
252
+ return null;
253
+ return contentDigest(target);
254
+ }
255
+ const text = gitShowUtf8(root, ref, relPath);
256
+ if (text === null)
257
+ return null;
258
+ return contentDigest(text);
259
+ }
260
+ /**
261
+ * Whether the bytes an exempt-release-age item writes at `path` are the base's bytes plus exactly the one scope entry, judged by
262
+ * verifyReleaseAgeExemption() over the base commit's own bytes (never the working tree) and its .npmrc. True for a file no such
263
+ * item names. The set carries the file's digest only, so this is the one place the "one entry, nothing else" rule is proved.
264
+ */
265
+ function provesReleaseAgeEdit(root, set, file, after) {
266
+ return provesReleaseAgeEditWith((path) => gitShowUtf8(root, set.repository.baseCommit, path), set, file, after);
267
+ }
268
+ /**
269
+ * provesReleaseAgeEdit() over any reader of the base commit's own bytes: `readBase` returns the text of a path at the base
270
+ * commit, or null when there is none. The plan command's dry tree reads them from the clone's object database and never runs
271
+ * `git show`.
272
+ */
273
+ export function provesReleaseAgeEditWith(readBase, set, file, after) {
274
+ const item = set.items.find((entry) => entry.id === file.item);
275
+ if (item === undefined || item.act !== "exempt-release-age")
276
+ return true;
277
+ if (item.path !== file.path)
278
+ return false;
279
+ const before = readBase(file.path);
280
+ // The digest the set names as `before` must be these very bytes, or the proof would be over another file.
281
+ if ((before === null ? null : contentDigest(before)) !== file.before)
282
+ return false;
283
+ const npmrc = readBase(".npmrc");
284
+ return verifyReleaseAgeExemption({ surface: item.surface, before, after, npmrc }).verified;
285
+ }
286
+ function checkBaseCommitMovement(root, set) {
287
+ const branch = set.repository.defaultBranch;
288
+ const tip = git(root, ["rev-parse", `refs/heads/${branch}`]);
289
+ if (tip.status !== 0)
290
+ return result(2, "indeterminate", "remote-tip-unreadable");
291
+ const tipCommit = tip.stdout.trim();
292
+ if (tipCommit === set.repository.baseCommit)
293
+ return null;
294
+ for (const file of set.files.filter(isWhole)) {
295
+ if (digestAtRef(root, tipCommit, file.path, file.mode) !== file.before)
296
+ return result(2, "indeterminate", "base-conflict");
297
+ }
298
+ return result(1, "violated", "superseded");
299
+ }
300
+ function persistChangeSet(hub, set) {
301
+ let hubReal;
302
+ try {
303
+ hubReal = realpathSync(hub);
304
+ }
305
+ catch {
306
+ return result(2, "indeterminate", "change-set-not-stored");
307
+ }
308
+ try {
309
+ storeChangeSet(hubReal, set);
310
+ }
311
+ catch {
312
+ return result(2, "indeterminate", "change-set-not-stored");
313
+ }
314
+ return null;
315
+ }
316
+ export function checkChangeSetShape(set) {
317
+ if (!BRANCH_SHAPE.test(set.branch) || !COMMIT_SHAPE.test(set.repository.baseCommit))
318
+ return result(2, "indeterminate", "change-set-invalid");
319
+ if (!validateRepositoryChangeSet(set).valid)
320
+ return result(2, "indeterminate", "change-set-invalid");
321
+ return null;
322
+ }
323
+ /**
324
+ * The base ledger, trusted against the acts of the plan committed at the hub
325
+ * (never the acts of the set being judged: a set cannot vouch for itself), with
326
+ * the exact bytes it was read from (null when the base has none).
327
+ */
328
+ function baseLedgerTrust(root, set, heldChangeSets, planDigest, planPackages) {
329
+ const bytes = gitShowBytes(root, set.repository.baseCommit, LEDGER_PATH);
330
+ const ledgerBytes = bytes ?? new Uint8Array(0);
331
+ const trust = trustInstalledLedger(ledgerBytes.length === 0 ? null : ledgerBytes, { id: set.repository.id, nodeId: set.repository.nodeId }, heldChangeSets, { planPackageActs: [{ planDigest, packages: planPackages }] });
332
+ if (trust.state === "refused")
333
+ return result(2, "indeterminate", trust.rule);
334
+ const generation = trust.ledger?.generation ?? 0;
335
+ if (generation !== set.ledger.generation)
336
+ return result(1, "violated", "ledger-mismatch");
337
+ return { ledger: trust.ledger, bytes: ledgerBytes.length === 0 ? null : ledgerBytes };
338
+ }
339
+ export function refuseReservedSymlinks(root, paths) {
340
+ for (const reserved of RESERVED_ROOTS) {
341
+ try {
342
+ if (lstatSync(join(root, reserved)).isSymbolicLink())
343
+ return result(2, "indeterminate", "symlink-ancestor");
344
+ }
345
+ catch (cause) {
346
+ if (cause.code !== "ENOENT")
347
+ throw cause;
348
+ }
349
+ }
350
+ for (const rel of paths) {
351
+ const segments = rel.split("/");
352
+ let current = root;
353
+ for (let index = 0; index < segments.length; index += 1) {
354
+ current = join(current, segments[index]);
355
+ try {
356
+ if (lstatSync(current).isSymbolicLink())
357
+ return result(2, "indeterminate", "symlink-ancestor");
358
+ }
359
+ catch (cause) {
360
+ if (cause.code === "ENOENT")
361
+ break;
362
+ throw cause;
363
+ }
364
+ }
365
+ }
366
+ return null;
367
+ }
368
+ function declareRootEntryText(root, set, path) {
369
+ const item = set.items.find((entry) => entry.act === "declare-root-entry" && entry.path === path);
370
+ if (item === undefined || item.act !== "declare-root-entry")
371
+ return null;
372
+ let onDisk;
373
+ try {
374
+ onDisk = readFileSync(join(root, path), "utf8");
375
+ }
376
+ catch {
377
+ return null;
378
+ }
379
+ try {
380
+ return editJsonPointer(onDisk, item.entries.map((entry) => ({ pointer: "/rootEntries/-", value: { name: entry.name, classification: entry.classification, disposition: entry.disposition } })));
381
+ }
382
+ catch (cause) {
383
+ if (cause instanceof JsonEditUnstableError)
384
+ return result(2, "indeterminate", "json-edit-unstable");
385
+ throw cause;
386
+ }
387
+ }
388
+ export function resolveFileText(root, set, file, texts) {
389
+ if (texts[file.path] !== undefined)
390
+ return texts[file.path];
391
+ if (file.mode === "120000") {
392
+ const role = discoveryLinkRole(file.path);
393
+ if (role !== null)
394
+ return discoveryLinkTarget(role);
395
+ }
396
+ const item = set.items.find((entry) => entry.id === file.item);
397
+ if (item?.act === "declare-root-entry" && item.path === file.path) {
398
+ const edited = declareRootEntryText(root, set, file.path);
399
+ if (edited === null)
400
+ return result(2, "indeterminate", "file-text-unavailable");
401
+ if (typeof edited !== "string")
402
+ return edited;
403
+ return edited;
404
+ }
405
+ return result(2, "indeterminate", "file-text-unavailable");
406
+ }
407
+ export function packagesForLockfile(set, invariants) {
408
+ const packages = [];
409
+ for (const row of invariants) {
410
+ const item = set.items.find((entry) => entry.id === row.item);
411
+ if (item === undefined || (item.act !== "install" && item.act !== "pin-starter"))
412
+ return null;
413
+ if (item.package.name !== row.name || item.package.version !== row.version || item.package.integrity !== row.integrity)
414
+ return null;
415
+ packages.push({ name: row.name, version: row.version, integrity: row.integrity, placement: item.placement });
416
+ }
417
+ return packages;
418
+ }
419
+ export function derivedLockfile(set) {
420
+ const derived = set.files.find((file) => "derived" in file && file.path !== LEDGER_PATH);
421
+ if (derived === undefined || !("derived" in derived))
422
+ return null;
423
+ const invariants = derived.invariants.filter((row) => "name" in row);
424
+ return { path: derived.path, invariants };
425
+ }
426
+ function porcelainPaths(root) {
427
+ const { status, stdout } = git(root, ["status", "--porcelain", "--untracked-files=all"]);
428
+ if (status !== 0)
429
+ return { ok: false };
430
+ const paths = [];
431
+ for (const line of stdout.split("\n")) {
432
+ if (line.length < 4)
433
+ continue;
434
+ let path = line.slice(3).trim();
435
+ const arrow = path.indexOf(" -> ");
436
+ if (arrow !== -1)
437
+ path = path.slice(arrow + 4);
438
+ if (path.length > 0)
439
+ paths.push(path);
440
+ }
441
+ return { ok: true, paths };
442
+ }
443
+ function diffPaths(root, baseCommit) {
444
+ const { status, stdout } = git(root, ["diff", "--name-only", baseCommit]);
445
+ if (status !== 0)
446
+ return { ok: false };
447
+ return {
448
+ ok: true,
449
+ paths: stdout
450
+ .split("\n")
451
+ .map((line) => line.trim())
452
+ .filter((line) => line.length > 0),
453
+ };
454
+ }
455
+ /** The reader verify uses: the clone's working tree and its git status. */
456
+ function workingTreeReader(root) {
457
+ return {
458
+ onBranch(branch) {
459
+ const head = git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
460
+ return head.status === 0 && head.stdout.trim() === branch;
461
+ },
462
+ changedPaths: (baseCommit) => diffPaths(root, baseCommit),
463
+ dirtyPaths: () => porcelainPaths(root),
464
+ hasSymlinkAncestor: (relPath) => hasSymlinkAncestor(root, relPath),
465
+ entry(relPath) {
466
+ const path = join(root, relPath);
467
+ let stat;
468
+ try {
469
+ stat = lstatSync(path);
470
+ }
471
+ catch (cause) {
472
+ if (cause.code === "ENOENT")
473
+ return null;
474
+ throw cause;
475
+ }
476
+ if (stat.isSymbolicLink())
477
+ return { kind: "symlink", target: readlinkSync(path) };
478
+ if (stat.isFile())
479
+ return { kind: "file", executable: (stat.mode & 0o111) !== 0 };
480
+ return { kind: "other" };
481
+ },
482
+ bytes: (relPath) => readFileSync(join(root, relPath)),
483
+ };
484
+ }
485
+ function symlinkBeforeRead(reader, relPaths) {
486
+ for (const relPath of relPaths) {
487
+ if (reader.hasSymlinkAncestor(relPath))
488
+ return result(2, "indeterminate", "symlink-ancestor");
489
+ }
490
+ return null;
491
+ }
492
+ function expectedLedgerBytes(previous, set, binding, planPackages) {
493
+ return Buffer.from(renderInstalledLedger(previous, set, binding, planPackages), "utf8");
494
+ }
495
+ export async function runPreconditions(clone, hub, set, heldChangeSets, admission) {
496
+ const resolved = resolveCloneRoot(clone);
497
+ if ("exitCode" in resolved)
498
+ return resolved;
499
+ const { root } = resolved;
500
+ const shape = checkChangeSetShape(set);
501
+ if (shape !== null)
502
+ return shape;
503
+ const symlinks = checkSymlinksSupported(root);
504
+ if (symlinks !== null)
505
+ return symlinks;
506
+ const remote = checkRemoteTip(root, set);
507
+ if (remote !== null)
508
+ return remote;
509
+ let hubRoot = hub;
510
+ try {
511
+ hubRoot = realpathSync(hub);
512
+ }
513
+ catch {
514
+ // readHubAuthority refuses a hub it cannot read.
515
+ }
516
+ const authority = readHubAuthority(hubRoot);
517
+ if ("state" in authority)
518
+ return refusalResult(authority);
519
+ const planPackages = planPackagesFor(authority, set.repository.id);
520
+ const trust = baseLedgerTrust(root, set, heldChangeSets, authority.planDigest, planPackages);
521
+ if ("exitCode" in trust)
522
+ return trust;
523
+ const decided = await decideSetBinding({
524
+ hub: hubRoot,
525
+ clone: root,
526
+ set,
527
+ authority,
528
+ baseLedger: trust.ledger,
529
+ baseLedgerBytes: trust.bytes,
530
+ now: admission.now,
531
+ runReadiness: admission.runReadiness,
532
+ });
533
+ if (decided.state !== "bound")
534
+ return refusalResult(decided);
535
+ return { root, previousLedger: trust.ledger, binding: decided.binding, planPackages };
536
+ }
537
+ export async function verifyRepository(input) {
538
+ const pre = await runPreconditions(input.clone, input.hub, input.set, input.heldChangeSets ?? [], input);
539
+ if ("exitCode" in pre)
540
+ return pre;
541
+ return verifyPrepared(input.set, pre);
542
+ }
543
+ /**
544
+ * The body of verify, over preconditions (and so an admission) that were already computed, and over a reader of the tree it
545
+ * judges: the clone's working tree by default (verify), or one commit's tree (status).
546
+ */
547
+ export function verifyPrepared(set, pre, reader = workingTreeReader(pre.root)) {
548
+ const { root, previousLedger, binding, planPackages } = pre;
549
+ const allowed = (path) => set.pathAllowList.some((pattern) => matchesPathPattern(path, pattern));
550
+ const declared = declaredPaths(set);
551
+ if (!reader.onBranch(set.branch))
552
+ return result(1, "violated", "diverged");
553
+ const porcelainEarly = reader.dirtyPaths();
554
+ if (!porcelainEarly.ok)
555
+ return result(2, "indeterminate", "status-unreadable");
556
+ const diffEarly = reader.changedPaths(set.repository.baseCommit);
557
+ if (!diffEarly.ok)
558
+ return result(2, "indeterminate", "status-unreadable");
559
+ const lockEarly = derivedLockfile(set);
560
+ const readPaths = [...declared, ...(lockEarly !== null ? [lockEarly.path] : [])];
561
+ const symlink = symlinkBeforeRead(reader, readPaths);
562
+ if (symlink !== null)
563
+ return symlink;
564
+ for (const path of declared) {
565
+ if (!allowed(path))
566
+ return result(1, "violated", "outside-allow-list");
567
+ }
568
+ // The Launcher's guide is checked before the whole-file loop so a changed byte is named for what it is: the set's own digest,
569
+ // then the constant text (G5). The file's text is read here and never echoed.
570
+ if (set.items.some((item) => item.act === "write-record" && item.source === "agents-guide")) {
571
+ const guide = set.files.find((file) => isWhole(file) && file.path === AGENTS_GUIDE_PATH);
572
+ if (guide !== undefined && guide.after !== null) {
573
+ let text = null;
574
+ try {
575
+ const entry = reader.entry(AGENTS_GUIDE_PATH);
576
+ if (entry !== null && entry.kind === "file")
577
+ text = reader.bytes(AGENTS_GUIDE_PATH).toString("utf8");
578
+ }
579
+ catch {
580
+ text = null;
581
+ }
582
+ if (text === null || contentDigest(text) !== guide.after || !verifyAgentsGuide(text))
583
+ return result(1, "violated", "agents-guide-mismatch");
584
+ }
585
+ }
586
+ for (const file of set.files.filter(isWhole)) {
587
+ if (file.after !== null) {
588
+ try {
589
+ const entry = reader.entry(file.path);
590
+ if (entry === null)
591
+ return result(1, "violated", "content-mismatch");
592
+ if (file.mode === "120000") {
593
+ if (entry.kind !== "symlink")
594
+ return result(1, "violated", "mode-mismatch");
595
+ if (contentDigest(entry.target) !== file.after)
596
+ return result(1, "violated", "content-mismatch");
597
+ }
598
+ else {
599
+ if (entry.kind !== "file")
600
+ return result(1, "violated", "mode-mismatch");
601
+ if (entry.executable)
602
+ return result(1, "violated", "mode-mismatch");
603
+ const onDisk = reader.bytes(file.path).toString("utf8");
604
+ if (contentDigest(onDisk) !== file.after)
605
+ return result(1, "violated", "content-mismatch");
606
+ if (!provesReleaseAgeEdit(root, set, file, onDisk))
607
+ return result(1, "violated", "content-mismatch");
608
+ }
609
+ }
610
+ catch {
611
+ return result(1, "violated", "content-mismatch");
612
+ }
613
+ }
614
+ else {
615
+ const present = reader.entry(file.path);
616
+ if (present !== null && present.kind !== "other")
617
+ return result(1, "violated", "removal-present");
618
+ }
619
+ }
620
+ if (set.keys.length > 0) {
621
+ let manifest;
622
+ try {
623
+ manifest = reader.bytes("package.json").toString("utf8");
624
+ }
625
+ catch {
626
+ return result(1, "violated", "content-mismatch");
627
+ }
628
+ for (const key of set.keys) {
629
+ const at = valueAtJsonPointer(manifest, key.pointer);
630
+ if (key.after === null) {
631
+ if (at.found)
632
+ return result(1, "violated", "content-mismatch");
633
+ }
634
+ else if (!at.found || at.value !== key.after)
635
+ return result(1, "violated", "content-mismatch");
636
+ }
637
+ }
638
+ const lock = derivedLockfile(set);
639
+ if (lock !== null) {
640
+ const format = lock.path === "package-lock.json" ? "npm" : lock.path === "pnpm-lock.yaml" ? "pnpm" : lock.path === "yarn.lock" ? null : null;
641
+ if (format === null)
642
+ return result(2, "indeterminate", "lockfile-format-unsupported");
643
+ const base = gitShowUtf8(root, set.repository.baseCommit, lock.path) ?? "";
644
+ let current;
645
+ try {
646
+ current = reader.bytes(lock.path).toString("utf8");
647
+ }
648
+ catch {
649
+ return result(1, "violated", "lockfile-invariants");
650
+ }
651
+ const packages = packagesForLockfile(set, lock.invariants);
652
+ if (packages === null)
653
+ return result(2, "indeterminate", "lockfile-invariants");
654
+ const checked = checkLockfileInvariants({ format, base, regenerated: current, packages });
655
+ if (checked.verdict === "violated")
656
+ return result(1, "violated", "lockfile-invariants");
657
+ if (checked.verdict === "indeterminate")
658
+ return result(2, "indeterminate", checked.reason);
659
+ }
660
+ const expectedLedger = expectedLedgerBytes(previousLedger, set, binding, planPackages);
661
+ let ledgerOnDisk;
662
+ try {
663
+ ledgerOnDisk = reader.bytes(LEDGER_PATH);
664
+ }
665
+ catch {
666
+ return result(1, "violated", "ledger-mismatch");
667
+ }
668
+ if (!ledgerOnDisk.equals(expectedLedger))
669
+ return result(1, "violated", "ledger-mismatch");
670
+ for (const path of diffEarly.paths) {
671
+ if (!declared.has(path))
672
+ return result(1, "violated", "undeclared-path");
673
+ }
674
+ for (const path of porcelainEarly.paths) {
675
+ if (!declared.has(path)) {
676
+ if (LEFTOVER_SKILL.test(path))
677
+ return result(1, "violated", "dirty", `${path} left by an earlier run`);
678
+ return result(1, "violated", "dirty");
679
+ }
680
+ }
681
+ return result(0, "materialized");
682
+ }
683
+ export function textsFromChangeSet(set) {
684
+ if (set.texts === undefined)
685
+ return {};
686
+ return Object.fromEntries(set.texts.map((row) => [row.path, row.text]));
687
+ }
688
+ export async function materializeRepository(input) {
689
+ const pre = await runPreconditions(input.clone, input.hub, input.set, input.heldChangeSets ?? [], input);
690
+ if ("exitCode" in pre)
691
+ return pre;
692
+ const { root, previousLedger, binding, planPackages } = pre;
693
+ const set = input.set;
694
+ const texts = { ...textsFromChangeSet(set), ...input.texts };
695
+ const baseMovement = checkBaseCommitMovement(root, set);
696
+ if (baseMovement !== null)
697
+ return baseMovement;
698
+ if (git(root, ["show-ref", "--verify", "--quiet", `refs/heads/${set.branch}`]).status === 0) {
699
+ const verified = verifyPrepared(set, pre);
700
+ if (verified.exitCode === 0) {
701
+ const stored = persistChangeSet(input.hub, set);
702
+ if (stored !== null)
703
+ return stored;
704
+ return verified;
705
+ }
706
+ if (verified.verdict === "indeterminate")
707
+ return verified;
708
+ return { exitCode: 1, verdict: "violated", reason: "diverged", detail: verified.reason === "agents-guide-mismatch" ? verified.reason : verified.detail };
709
+ }
710
+ for (const file of set.files.filter(isWhole)) {
711
+ const onDisk = digestAtPath(root, file.path);
712
+ if (onDisk !== file.before)
713
+ return result(1, "violated", "base-mismatch");
714
+ }
715
+ const porcelain = porcelainPaths(root);
716
+ if (!porcelain.ok)
717
+ return result(2, "indeterminate", "status-unreadable");
718
+ if (porcelain.paths.length > 0)
719
+ return result(1, "violated", "dirty");
720
+ const toWrite = set.files.filter(isWhole).filter((file) => file.before !== file.after);
721
+ const resolvedTexts = new Map();
722
+ for (const file of toWrite) {
723
+ const resolved = resolveFileText(root, set, file, texts);
724
+ if (typeof resolved !== "string")
725
+ return resolved;
726
+ if (file.after !== null && contentDigest(resolved) !== file.after)
727
+ return result(1, "violated", "content-mismatch");
728
+ if (!provesReleaseAgeEdit(root, set, file, resolved))
729
+ return result(1, "violated", "content-mismatch");
730
+ resolvedTexts.set(file.path, resolved);
731
+ }
732
+ let packageText;
733
+ if (set.keys.length > 0) {
734
+ const existing = gitShowUtf8(root, set.repository.baseCommit, "package.json");
735
+ if (existing === null)
736
+ return result(2, "indeterminate", "manifest-absent");
737
+ try {
738
+ packageText = editJsonPointer(existing, set.keys.map((key) => (key.after === null ? { pointer: key.pointer, remove: true } : { pointer: key.pointer, value: key.after })));
739
+ }
740
+ catch (cause) {
741
+ if (cause instanceof JsonEditUnstableError)
742
+ return result(2, "indeterminate", "json-edit-unstable");
743
+ throw cause;
744
+ }
745
+ }
746
+ const pathsToTouch = writePaths(set);
747
+ const symlinkCheck = refuseReservedSymlinks(root, pathsToTouch);
748
+ if (symlinkCheck !== null)
749
+ return symlinkCheck;
750
+ let ledgerBytes;
751
+ try {
752
+ ledgerBytes = expectedLedgerBytes(previousLedger, set, binding, planPackages);
753
+ }
754
+ catch (cause) {
755
+ if (cause instanceof TypeError)
756
+ return result(2, "indeterminate", "change-set-invalid");
757
+ throw cause;
758
+ }
759
+ if (git(root, ["checkout", "-b", set.branch, set.repository.baseCommit]).status !== 0) {
760
+ return result(2, "indeterminate", "branch-not-created");
761
+ }
762
+ for (const file of toWrite) {
763
+ if (file.after === null) {
764
+ const removed = removePath(root, file.path);
765
+ if (removed !== null)
766
+ return removed;
767
+ continue;
768
+ }
769
+ const text = resolvedTexts.get(file.path);
770
+ if (file.mode === "120000") {
771
+ const linked = writeSymlink(root, file.path, text, file.after);
772
+ if (linked !== null)
773
+ return linked;
774
+ }
775
+ else {
776
+ const written = writeRegularFile(root, file.path, text, file.after);
777
+ if (written !== null)
778
+ return written;
779
+ }
780
+ }
781
+ if (packageText !== undefined) {
782
+ const written = writeRegularFile(root, "package.json", packageText, contentDigest(packageText));
783
+ if (written !== null)
784
+ return written;
785
+ }
786
+ const lock = derivedLockfile(set);
787
+ let toolingVersion = null;
788
+ let toolingTool = null;
789
+ if (lock !== null) {
790
+ const lockpath = lockfilePath({ packageManager: set.observed.packageManager, lockfile: set.observed.lockfile });
791
+ if (lockpath === null || lockpath !== lock.path)
792
+ return result(2, "indeterminate", "lockfile-format-unsupported");
793
+ const packages = packagesForLockfile(set, lock.invariants);
794
+ if (packages === null)
795
+ return result(2, "indeterminate", "lockfile-invariants");
796
+ const baseLock = gitShowUtf8(root, set.repository.baseCommit, lock.path) ?? "";
797
+ const regen = await regenerateLockfile({
798
+ root,
799
+ packageManager: set.observed.packageManager,
800
+ lockfile: set.observed.lockfile,
801
+ toolVersion: input.toolVersion ?? null,
802
+ baseLockfile: baseLock,
803
+ packages,
804
+ now: input.now ?? (() => new Date()),
805
+ }, { spawn: input.spawn });
806
+ if (regen.verdict !== "satisfied") {
807
+ if (regen.verdict === "violated")
808
+ return result(1, "violated", regen.rule);
809
+ return result(2, "indeterminate", regen.reason);
810
+ }
811
+ // The runner already wrote the lockfile. `after` is the hex digest of those bytes.
812
+ let onDisk;
813
+ try {
814
+ onDisk = readFileSync(join(root, regen.path));
815
+ }
816
+ catch {
817
+ return result(1, "violated", "lockfile-invariants");
818
+ }
819
+ if (createHash("sha256").update(onDisk).digest("hex") !== regen.after)
820
+ return result(1, "violated", "lockfile-invariants");
821
+ toolingTool = regen.tooling.tool;
822
+ toolingVersion = regen.tooling.version;
823
+ }
824
+ const ledgerWritten = writeRegularFile(root, LEDGER_PATH, ledgerBytes.toString("utf8"), contentDigest(ledgerBytes.toString("utf8")));
825
+ if (ledgerWritten !== null)
826
+ return ledgerWritten;
827
+ const ledgerRead = readFileSync(join(root, LEDGER_PATH));
828
+ if (!ledgerRead.equals(ledgerBytes))
829
+ return result(1, "violated", "ledger-mismatch");
830
+ let toStore = set;
831
+ if (toolingVersion !== null && toolingTool !== null && TOOL_VERSION_SHAPE.test(toolingVersion)) {
832
+ const tooling = canonicalOrder([...(set.tooling ?? []).filter((row) => row.tool !== toolingTool), { tool: toolingTool, version: toolingVersion }], CANONICAL_KEYS.tool);
833
+ toStore = { ...set, tooling };
834
+ }
835
+ const stored = persistChangeSet(input.hub, toStore);
836
+ if (stored !== null)
837
+ return stored;
838
+ return result(0, "materialized");
839
+ }
840
+ //# sourceMappingURL=materialize.js.map