@intentius/chant 0.91.0 → 0.93.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 (344) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/graph.d.ts.map +1 -1
  4. package/dist/cli/handlers/operator.d.ts +6 -0
  5. package/dist/cli/handlers/operator.d.ts.map +1 -1
  6. package/dist/cli/handlers/run.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  10. package/dist/cli/plugins.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +12 -2
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  14. package/dist/config.d.ts +22 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/lexicon-module.d.ts +22 -0
  17. package/dist/lexicon-module.d.ts.map +1 -1
  18. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  19. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  20. package/dist/lifecycle/run-ledger.d.ts +2 -0
  21. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  22. package/dist/lifecycle/symptoms.d.ts +51 -0
  23. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  24. package/dist/lint/rules/index.d.ts +4 -2
  25. package/dist/lint/rules/index.d.ts.map +1 -1
  26. package/dist/lint/rules/op/index.d.ts +14 -3
  27. package/dist/lint/rules/op/index.d.ts.map +1 -1
  28. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  29. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  31. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  33. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  35. package/dist/op/activities/activity-contracts.d.ts +201 -0
  36. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  37. package/dist/op/activities/apply.d.ts.map +1 -1
  38. package/dist/op/activities/change-coverage.d.ts +44 -0
  39. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  40. package/dist/op/activities/converge.d.ts +24 -1
  41. package/dist/op/activities/converge.d.ts.map +1 -1
  42. package/dist/op/activities/decide.d.ts +107 -0
  43. package/dist/op/activities/decide.d.ts.map +1 -0
  44. package/dist/op/activities/index.d.ts +10 -0
  45. package/dist/op/activities/index.d.ts.map +1 -1
  46. package/dist/op/activities/shell.d.ts +32 -0
  47. package/dist/op/activities/shell.d.ts.map +1 -1
  48. package/dist/op/activities/source-release.d.ts +86 -0
  49. package/dist/op/activities/source-release.d.ts.map +1 -0
  50. package/dist/op/activities/source-rollback.d.ts +100 -0
  51. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  52. package/dist/op/activities/work-evidence.d.ts +42 -0
  53. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  54. package/dist/op/activity-contract.d.ts +1 -0
  55. package/dist/op/activity-contract.d.ts.map +1 -1
  56. package/dist/op/activity-profiles.d.ts +16 -0
  57. package/dist/op/activity-profiles.d.ts.map +1 -1
  58. package/dist/op/builders.d.ts +84 -2
  59. package/dist/op/builders.d.ts.map +1 -1
  60. package/dist/op/composites/converge-op.d.ts +31 -4
  61. package/dist/op/composites/converge-op.d.ts.map +1 -1
  62. package/dist/op/decide-backend.d.ts +116 -0
  63. package/dist/op/decide-backend.d.ts.map +1 -0
  64. package/dist/op/decide-config.d.ts +120 -0
  65. package/dist/op/decide-config.d.ts.map +1 -0
  66. package/dist/op/decide-read-inputs.d.ts +40 -0
  67. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  68. package/dist/op/gate-wait.d.ts +43 -0
  69. package/dist/op/gate-wait.d.ts.map +1 -0
  70. package/dist/op/gate.d.ts +5 -2
  71. package/dist/op/gate.d.ts.map +1 -1
  72. package/dist/op/index.d.ts +11 -4
  73. package/dist/op/index.d.ts.map +1 -1
  74. package/dist/op/local-executor.d.ts +29 -4
  75. package/dist/op/local-executor.d.ts.map +1 -1
  76. package/dist/op/local-output.d.ts +2 -0
  77. package/dist/op/local-output.d.ts.map +1 -1
  78. package/dist/op/op-ir.d.ts +2 -0
  79. package/dist/op/op-ir.d.ts.map +1 -1
  80. package/dist/op/op-verb-class.d.ts.map +1 -1
  81. package/dist/op/operator.d.ts +97 -1
  82. package/dist/op/operator.d.ts.map +1 -1
  83. package/dist/op/runtime.d.ts +37 -5
  84. package/dist/op/runtime.d.ts.map +1 -1
  85. package/dist/op/runtimes/local.d.ts.map +1 -1
  86. package/dist/op/source-archive.d.ts +63 -0
  87. package/dist/op/source-archive.d.ts.map +1 -0
  88. package/dist/op/steward-points.d.ts +136 -0
  89. package/dist/op/steward-points.d.ts.map +1 -0
  90. package/dist/op/steward-turn.d.ts +53 -0
  91. package/dist/op/steward-turn.d.ts.map +1 -0
  92. package/dist/op/steward.d.ts +22 -1
  93. package/dist/op/steward.d.ts.map +1 -1
  94. package/dist/op/types.d.ts +7 -0
  95. package/dist/op/types.d.ts.map +1 -1
  96. package/dist/op/work-lease-run.d.ts +1 -1
  97. package/dist/op/work-lease-run.d.ts.map +1 -1
  98. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  100. package/dist/workspace/answers-ledger.d.ts +76 -0
  101. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  102. package/dist/workspace/changes-cli.d.ts +16 -0
  103. package/dist/workspace/changes-cli.d.ts.map +1 -0
  104. package/dist/workspace/changes.d.ts +200 -0
  105. package/dist/workspace/changes.d.ts.map +1 -0
  106. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  107. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  108. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  118. package/dist/workspace/chant-migrations.d.ts +99 -0
  119. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  120. package/dist/workspace/checks/boxes.d.ts +34 -1
  121. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  122. package/dist/workspace/checks/diagrams.d.ts +31 -0
  123. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  124. package/dist/workspace/checks/records.d.ts +7 -0
  125. package/dist/workspace/checks/records.d.ts.map +1 -1
  126. package/dist/workspace/checks.d.ts +3 -0
  127. package/dist/workspace/checks.d.ts.map +1 -1
  128. package/dist/workspace/compose-graph.d.ts +2 -0
  129. package/dist/workspace/compose-graph.d.ts.map +1 -1
  130. package/dist/workspace/conformance/index.d.ts.map +1 -1
  131. package/dist/workspace/decide.d.ts +49 -4
  132. package/dist/workspace/decide.d.ts.map +1 -1
  133. package/dist/workspace/declaration.d.ts +52 -0
  134. package/dist/workspace/declaration.d.ts.map +1 -1
  135. package/dist/workspace/declaration.schema.json +114 -1
  136. package/dist/workspace/declared-kinds.d.ts +20 -0
  137. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  138. package/dist/workspace/generated-files.d.ts +7 -0
  139. package/dist/workspace/generated-files.d.ts.map +1 -1
  140. package/dist/workspace/intent.d.ts.map +1 -1
  141. package/dist/workspace/lineage-check.d.ts +3 -0
  142. package/dist/workspace/lineage-check.d.ts.map +1 -1
  143. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  144. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  145. package/dist/workspace/ls.d.ts +39 -1
  146. package/dist/workspace/ls.d.ts.map +1 -1
  147. package/dist/workspace/point-answer.schema.json +95 -0
  148. package/dist/workspace/points-cli.d.ts.map +1 -1
  149. package/dist/workspace/points.d.ts +1 -1
  150. package/dist/workspace/reason-codes.d.ts +14 -2
  151. package/dist/workspace/reason-codes.d.ts.map +1 -1
  152. package/dist/workspace/records-cli.d.ts +11 -1
  153. package/dist/workspace/records-cli.d.ts.map +1 -1
  154. package/dist/workspace/records-close.d.ts +1 -1
  155. package/dist/workspace/records-close.d.ts.map +1 -1
  156. package/dist/workspace/records-write.d.ts +11 -5
  157. package/dist/workspace/records-write.d.ts.map +1 -1
  158. package/dist/workspace/records.d.ts +34 -4
  159. package/dist/workspace/records.d.ts.map +1 -1
  160. package/dist/workspace/status-gates.d.ts +7 -2
  161. package/dist/workspace/status-gates.d.ts.map +1 -1
  162. package/dist/workspace/status-stewards.d.ts +59 -1
  163. package/dist/workspace/status-stewards.d.ts.map +1 -1
  164. package/dist/workspace/status.d.ts +16 -0
  165. package/dist/workspace/status.d.ts.map +1 -1
  166. package/dist/workspace/work-cli.d.ts +113 -2
  167. package/dist/workspace/work-cli.d.ts.map +1 -1
  168. package/dist/workspace/work-evidence.d.ts +74 -0
  169. package/dist/workspace/work-evidence.d.ts.map +1 -0
  170. package/dist/workspace/work.d.ts +43 -1
  171. package/dist/workspace/work.d.ts.map +1 -1
  172. package/package.json +1 -1
  173. package/src/cli/commands/audit.test.ts +9 -1
  174. package/src/cli/commands/build.ts +3 -2
  175. package/src/cli/commands/lint.ts +7 -1
  176. package/src/cli/handlers/graph.test.ts +20 -0
  177. package/src/cli/handlers/graph.ts +16 -2
  178. package/src/cli/handlers/operator.ts +34 -2
  179. package/src/cli/handlers/run.test.ts +154 -2
  180. package/src/cli/handlers/run.ts +121 -2
  181. package/src/cli/main.test.ts +10 -0
  182. package/src/cli/main.ts +19 -0
  183. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  184. package/src/cli/mcp/workspace-tools.ts +62 -4
  185. package/src/cli/plugins.ts +11 -3
  186. package/src/cli/registry.ts +12 -2
  187. package/src/codegen/pinned-upgrade.ts +2 -1
  188. package/src/codegen/release-wiring.test.ts +49 -2
  189. package/src/components/capability-plugin-loader.ts +3 -3
  190. package/src/components/cli-support.ts +2 -2
  191. package/src/config.ts +11 -0
  192. package/src/lexicon-module.ts +56 -1
  193. package/src/lexicon-package.test.ts +96 -0
  194. package/src/lifecycle/converge-ledger.ts +20 -0
  195. package/src/lifecycle/run-ledger.ts +6 -0
  196. package/src/lifecycle/symptoms.ts +95 -0
  197. package/src/lint/rules/index.ts +6 -2
  198. package/src/lint/rules/op/index.ts +17 -4
  199. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  200. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  201. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  202. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  203. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  204. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  205. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  206. package/src/op/activities/activity-contracts.ts +190 -1
  207. package/src/op/activities/apply.ts +7 -6
  208. package/src/op/activities/change-coverage.ts +60 -0
  209. package/src/op/activities/converge.test.ts +55 -1
  210. package/src/op/activities/converge.ts +208 -29
  211. package/src/op/activities/decide.live.test.ts +78 -0
  212. package/src/op/activities/decide.test.ts +300 -0
  213. package/src/op/activities/decide.ts +267 -0
  214. package/src/op/activities/index.ts +21 -0
  215. package/src/op/activities/shell.test.ts +91 -1
  216. package/src/op/activities/shell.ts +91 -2
  217. package/src/op/activities/source-release.test.ts +118 -0
  218. package/src/op/activities/source-release.ts +168 -0
  219. package/src/op/activities/source-rollback.test.ts +131 -0
  220. package/src/op/activities/source-rollback.ts +239 -0
  221. package/src/op/activities/work-evidence.ts +62 -0
  222. package/src/op/activity-contract-registry.test.ts +2 -0
  223. package/src/op/activity-contract-registry.ts +2 -2
  224. package/src/op/activity-contract.test.ts +12 -0
  225. package/src/op/activity-contract.ts +6 -1
  226. package/src/op/activity-profiles.ts +29 -0
  227. package/src/op/activity-registry.test.ts +1 -1
  228. package/src/op/activity-registry.ts +2 -2
  229. package/src/op/builders.ts +119 -5
  230. package/src/op/command-gate.test.ts +192 -0
  231. package/src/op/composites/converge-op.test.ts +27 -1
  232. package/src/op/composites/converge-op.ts +69 -15
  233. package/src/op/decide-backend.test.ts +44 -0
  234. package/src/op/decide-backend.ts +212 -0
  235. package/src/op/decide-config.test.ts +52 -0
  236. package/src/op/decide-config.ts +80 -0
  237. package/src/op/decide-read-inputs.ts +117 -0
  238. package/src/op/gate-approval.ts +2 -2
  239. package/src/op/gate-wait.ts +57 -0
  240. package/src/op/gate.ts +8 -3
  241. package/src/op/generate-pipeline.ts +2 -2
  242. package/src/op/index.ts +18 -3
  243. package/src/op/local-executor.ts +151 -15
  244. package/src/op/local-output.test.ts +33 -0
  245. package/src/op/local-output.ts +20 -2
  246. package/src/op/op-ir.test.ts +10 -0
  247. package/src/op/op-ir.ts +4 -0
  248. package/src/op/op-verb-class.ts +1 -0
  249. package/src/op/operator.ts +290 -11
  250. package/src/op/runtime.ts +31 -8
  251. package/src/op/runtimes/local.ts +3 -1
  252. package/src/op/source-archive.ts +161 -0
  253. package/src/op/steward-points.test.ts +313 -0
  254. package/src/op/steward-points.ts +194 -0
  255. package/src/op/steward-turn.ts +79 -0
  256. package/src/op/steward.test.ts +87 -3
  257. package/src/op/steward.ts +25 -1
  258. package/src/op/types.ts +7 -0
  259. package/src/op/work-lease-run.test.ts +20 -1
  260. package/src/op/work-lease-run.ts +2 -2
  261. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  262. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  263. package/src/workspace/answers-ledger.ts +118 -0
  264. package/src/workspace/changes-cli.ts +56 -0
  265. package/src/workspace/changes.schema.json +472 -0
  266. package/src/workspace/changes.test.ts +315 -0
  267. package/src/workspace/changes.ts +512 -0
  268. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  269. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  270. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  271. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  272. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  273. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  274. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  275. package/src/workspace/chant-migrations.ts +164 -0
  276. package/src/workspace/check-contract.test.ts +4 -2
  277. package/src/workspace/check.schema.json +15 -5
  278. package/src/workspace/checks/boxes.test.ts +80 -1
  279. package/src/workspace/checks/boxes.ts +136 -1
  280. package/src/workspace/checks/diagrams.test.ts +134 -0
  281. package/src/workspace/checks/diagrams.ts +108 -0
  282. package/src/workspace/checks/records.ts +47 -0
  283. package/src/workspace/checks.ts +8 -1
  284. package/src/workspace/compose-graph.ts +2 -0
  285. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  286. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  287. package/src/workspace/conformance/index.ts +5 -0
  288. package/src/workspace/decide.test.ts +1 -1
  289. package/src/workspace/decide.ts +156 -23
  290. package/src/workspace/declaration.schema.json +114 -1
  291. package/src/workspace/declaration.test.ts +58 -0
  292. package/src/workspace/declaration.ts +104 -0
  293. package/src/workspace/declared-kinds.test.ts +5 -5
  294. package/src/workspace/declared-kinds.ts +46 -1
  295. package/src/workspace/generated-files.ts +14 -0
  296. package/src/workspace/graph-cli.ts +1 -1
  297. package/src/workspace/graph.schema.json +9 -1
  298. package/src/workspace/intent.schema.json +8 -2
  299. package/src/workspace/intent.ts +3 -10
  300. package/src/workspace/lineage-check.ts +16 -1
  301. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  302. package/src/workspace/lineage-upgrade.test.ts +70 -0
  303. package/src/workspace/lineage-upgrade.ts +81 -10
  304. package/src/workspace/ls-contract.test.ts +57 -0
  305. package/src/workspace/ls.schema.json +46 -1
  306. package/src/workspace/ls.ts +62 -3
  307. package/src/workspace/points-cli.ts +14 -3
  308. package/src/workspace/points-write.schema.json +11 -1
  309. package/src/workspace/points.schema.json +33 -0
  310. package/src/workspace/points.test.ts +1 -1
  311. package/src/workspace/points.ts +1 -1
  312. package/src/workspace/read-contract.test.ts +1 -1
  313. package/src/workspace/reason-codes.test.ts +11 -1
  314. package/src/workspace/reason-codes.ts +18 -2
  315. package/src/workspace/records-amend.schema.json +2 -0
  316. package/src/workspace/records-cli.ts +13 -3
  317. package/src/workspace/records-close.schema.json +2 -0
  318. package/src/workspace/records-new.schema.json +2 -0
  319. package/src/workspace/records-review.schema.json +2 -0
  320. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  321. package/src/workspace/records-sessions-write.test.ts +2 -34
  322. package/src/workspace/records-write.e2e.test.ts +108 -0
  323. package/src/workspace/records-write.test.ts +32 -41
  324. package/src/workspace/records-write.ts +33 -15
  325. package/src/workspace/records.schema.json +36 -2
  326. package/src/workspace/records.test.ts +48 -3
  327. package/src/workspace/records.ts +109 -3
  328. package/src/workspace/status-contract.test.ts +13 -8
  329. package/src/workspace/status-gates.ts +9 -4
  330. package/src/workspace/status-stewards.ts +108 -4
  331. package/src/workspace/status.schema.json +111 -7
  332. package/src/workspace/status.ts +36 -1
  333. package/src/workspace/work-acceptance.test.ts +274 -0
  334. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  335. package/src/workspace/work-cli.ts +193 -10
  336. package/src/workspace/work-evidence.schema.json +212 -0
  337. package/src/workspace/work-evidence.ts +169 -0
  338. package/src/workspace/work-history.schema.json +349 -0
  339. package/src/workspace/work-history.test.ts +156 -0
  340. package/src/workspace/work.test.ts +3 -2
  341. package/src/workspace/work.ts +100 -1
  342. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  343. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  344. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -0,0 +1,234 @@
1
+ /**
2
+ * `chud-lexicon-exit-fly-site` (#2809): declare a migrated repo's Fly site
3
+ * with the fly lexicon's `FlySite` composite.
4
+ *
5
+ * `chud-lexicon-exit` (0.92.0) deleted deploy/site.ts, chud's `ChudLocalSite`
6
+ * composite, and removed chud's `FlySite` marker from deploy/fly.ts, so the
7
+ * repo declared no composite instance: `chant workspace graph --composites`
8
+ * listed none, and nothing told a reader the app component deploys the site.
9
+ * Per ws-056 the Fly resources stay on the fly lexicon. This migration:
10
+ *
11
+ * - rewrites deploy/fly.ts as one `FlySite` instance (`flySite`) with the
12
+ * values deploy/fly.ts and deploy/fly-machine.ts declared, and deletes
13
+ * deploy/fly-machine.ts, when the two declare exactly the template's
14
+ * resources (comments aside). The build's plan has the same App, Volume,
15
+ * IP, Secret and one Machine, which the release Op's `flyRelease` finds;
16
+ * - names `FlySite` in the app component's `composites`, so the listing joins
17
+ * the instance to the component.
18
+ *
19
+ * A site the project changed keeps its files, and the plan lists the
20
+ * composite as not moved. The plan is empty unless the app component is the
21
+ * one `chud-lexicon-exit` wrote and names no composites yet.
22
+ */
23
+
24
+ import { existsSync, readFileSync } from "node:fs";
25
+ import { join, posix } from "node:path";
26
+ import type { ChantMigration, ChantMigrationContext, ChantMigrationPlan, NotMoved, PlannedChange } from "../chant-migrations";
27
+
28
+ export const CHUD_LEXICON_EXIT_FLY_SITE = "chud-lexicon-exit-fly-site";
29
+
30
+ const DESCRIPTION = "declare the Fly site with the fly lexicon's FlySite composite, the instance the app component deploys (#2809)";
31
+
32
+ function readText(dir: string, path: string): string | undefined {
33
+ const abs = join(dir, path);
34
+ return existsSync(abs) ? readFileSync(abs, "utf-8") : undefined;
35
+ }
36
+
37
+ /** The template's Fly site, as deploy/fly.ts and deploy/fly-machine.ts declare it. */
38
+ export interface FlySiteValues {
39
+ /** The import that gives the app's name, kept as it is: `import { appSlug } from "../app-name.ts";`. */
40
+ nameImport: string | null;
41
+ /** The expression the App's name is, as written. */
42
+ app: string;
43
+ region: string;
44
+ machine: string;
45
+ image: string;
46
+ port: string;
47
+ env: string;
48
+ cpuKind: string;
49
+ cpus: string;
50
+ memoryMb: string;
51
+ volume: { name: string; sizeGb: string; path: string };
52
+ ip: string;
53
+ secret: { name: string; value: string };
54
+ }
55
+
56
+ /** A file's code without its comments and blank lines. */
57
+ function codeOf(text: string): string {
58
+ return text
59
+ .replace(/\/\*[\s\S]*?\*\//g, "")
60
+ .split("\n")
61
+ .map((l) => l.replace(/\s+$/, ""))
62
+ .filter((l) => l !== "" && !/^\s*\/\//.test(l))
63
+ .join("\n");
64
+ }
65
+
66
+ const STR = `("[^"\\n]*")`;
67
+ const FLY_CODE = new RegExp(
68
+ "^" +
69
+ `import \\{ App, Fly, IPAddress, Secret, Volume \\} from "@intentius/chant-lexicon-fly";\\n` +
70
+ `(?:import \\{ FlySite \\} from "@intentius/chud-runtime/lexicon";\\n)?` +
71
+ `(?:(import \\{ \\w+ \\} from "\\.\\./app-name\\.ts";)\\n)?` +
72
+ `const name = ([^;\\n]+);\\n` +
73
+ `const region = ${STR};\\n` +
74
+ `export const flyApp = new App\\(\\{ name, org_slug: Fly\\.OrgSlug \\}\\);\\n` +
75
+ `export const data = new Volume\\(\\{ name: ${STR}, region, size_gb: (\\d+) \\}\\);\\n` +
76
+ `export const publicIp = new IPAddress\\(\\{ type: ${STR} \\}\\);\\n` +
77
+ `export const appSecret = new Secret\\(\\{ name: ${STR}, value: ([^\\n]+) \\}\\);` +
78
+ `(?:\\nexport const flySite = new FlySite\\([^\\n]*\\);)?$`,
79
+ );
80
+ const MACHINE_CODE = new RegExp(
81
+ [
82
+ `^import \\{ Machine, MachineConfig, MachineGuest, MachineMount, MachinePort, MachineService \\} from "@intentius/chant-lexicon-fly";`,
83
+ `export const guest = new MachineGuest\\(\\{ cpu_kind: ${STR}, cpus: (\\d+), memory_mb: (\\d+) \\}\\);`,
84
+ `export const dataMount = new MachineMount\\(\\{ volume: ${STR}, path: ${STR} \\}\\);`,
85
+ `export const https = new MachinePort\\(\\{ port: 443, handlers: \\["tls", "http"\\] \\}\\);`,
86
+ `export const http = new MachinePort\\(\\{ port: 80, handlers: \\["http"\\] \\}\\);`,
87
+ `export const web = new MachineService\\(\\{ protocol: "tcp", internal_port: (\\d+), ports: \\[https, http\\] \\}\\);`,
88
+ `export const server = new Machine\\(\\{`,
89
+ ` {2}name: ${STR},`,
90
+ ` {2}region: ${STR},`,
91
+ ` {2}config: new MachineConfig\\(\\{`,
92
+ ` {4}image: ${STR},`,
93
+ ` {4}guest,`,
94
+ ` {4}mounts: \\[dataMount\\],`,
95
+ ` {4}services: \\[web\\],`,
96
+ ` {4}env: (\\{[^\\n]*\\}),`,
97
+ ` {2}\\}\\),`,
98
+ `\\}\\);$`,
99
+ ].join("\\n"),
100
+ );
101
+
102
+ /**
103
+ * The Fly site's values when deploy/fly.ts and deploy/fly-machine.ts declare
104
+ * exactly the template's resources (their comments aside), or null. A site
105
+ * the project changed is left in its files, and the plan says so.
106
+ */
107
+ export function readFlySite(fly: string, machine: string): FlySiteValues | null {
108
+ const f = FLY_CODE.exec(codeOf(fly));
109
+ const m = MACHINE_CODE.exec(codeOf(machine));
110
+ if (!f || !m) return null;
111
+ const [, nameImport, app, region, volumeName, sizeGb, ip, secretName, secretValue] = f;
112
+ const [, cpuKind, cpus, memoryMb, mountVolume, mountPath, port, machineName, machineRegion, image, env] = m;
113
+ // One region and one Volume: what the composite declares.
114
+ if (machineRegion !== region || mountVolume !== volumeName) return null;
115
+ return {
116
+ nameImport: nameImport ?? null,
117
+ app,
118
+ region,
119
+ machine: machineName,
120
+ image,
121
+ port,
122
+ env,
123
+ cpuKind,
124
+ cpus,
125
+ memoryMb,
126
+ volume: { name: volumeName, sizeGb, path: mountPath },
127
+ ip,
128
+ secret: { name: secretName, value: secretValue },
129
+ };
130
+ }
131
+
132
+ /** deploy/fly.ts declaring the site with the fly lexicon's FlySite composite. */
133
+ export function flySiteFile(v: FlySiteValues): string {
134
+ return `/**
135
+ * The Fly site, \`fly\` in chant.config.ts's chud.sites, declared with the fly
136
+ * lexicon's FlySite composite: the App, the Machine that serves the app (Fly's
137
+ * proxy sends 443 and 80 to its port ${v.port}), the Volume the app's data
138
+ * lives on, mounted at ${JSON.parse(v.volume.path)}, a public IP and the ${JSON.parse(v.secret.name)}
139
+ * Secret. \`chant build deploy --lexicon fly\` serializes it to Machines API
140
+ * requests, with the one Machine a release ships to. \`chant workspace graph
141
+ * --composites\` lists it as the instance \`flySite\`, which the app component
142
+ * (app.component.ts) deploys: it names FlySite in its \`composites\`.
143
+ *
144
+ * The same declarations apply to a real Fly org (FLY_API_TOKEN, and FLY_ORG
145
+ * for the org) or to mudflaps, the Machines API emulator, when
146
+ * FLY_FLAPS_BASE_URL points at it. Fly app names are global; change the name
147
+ * before the first release to a real org if it is taken.
148
+ *
149
+ * No secret value is in the repo. ${JSON.parse(v.secret.name)}'s value comes from the
150
+ * environment at release time; without it, the release checks the app
151
+ * already has it (\`fly secrets set ${JSON.parse(v.secret.name)}=...\`) and refuses otherwise.
152
+ *
153
+ * \`chant workspace upgrade\` wrote this from deploy/fly.ts and
154
+ * deploy/fly-machine.ts (its migration ${CHUD_LEXICON_EXIT_FLY_SITE}), with the
155
+ * same resources, in place of chud's FlySite marker and ChudLocalSite
156
+ * composite (ws-056). The local site is the studio kit's box service
157
+ * (arugula-salad/studio, template/).
158
+ */
159
+ import { Fly, FlySite } from "@intentius/chant-lexicon-fly";
160
+ ${v.nameImport ? `${v.nameImport}\n` : ""}
161
+ export const flySite = FlySite({
162
+ app: ${v.app},
163
+ org: Fly.OrgSlug,
164
+ region: ${v.region},
165
+ machine: ${v.machine},
166
+ image: ${v.image},
167
+ port: ${v.port},
168
+ env: ${v.env},
169
+ cpuKind: ${v.cpuKind},
170
+ cpus: ${v.cpus},
171
+ memoryMb: ${v.memoryMb},
172
+ volume: { name: ${v.volume.name}, sizeGb: ${v.volume.sizeGb}, path: ${v.volume.path} },
173
+ ip: ${v.ip},
174
+ secrets: { ${/^[A-Za-z_$][\w$]*$/.test(JSON.parse(v.secret.name)) ? JSON.parse(v.secret.name) : v.secret.name}: ${v.secret.value} },
175
+ });
176
+ `;
177
+ }
178
+
179
+
180
+ const COMPONENT_BARE = ' archetype: "service",\n dependsOn: [],\n';
181
+ const COMPONENT_NAMED = ' archetype: "service",\n composites: ["FlySite"],\n dependsOn: [],\n';
182
+ const COMPONENT_SAID = " * (arugula-salad/studio, template/), and the Fly site's resources stay in\n * fly.ts and fly-machine.ts.\n */";
183
+ const COMPONENT_SAYS =
184
+ " * (arugula-salad/studio, template/).\n *\n * It deploys the Fly site, the fly lexicon's FlySite composite in fly.ts, and\n * says so in `composites`, so `chant workspace graph --composites` lists the\n * site's instance with this component.\n */";
185
+ /** The release Op's comment on where the Fly requests come from, as chud-lexicon-exit wrote it, and once the site is a FlySite. */
186
+ const RELEASE_SAID = " * deploy/fly.ts and deploy/fly-machine.ts, into dist/fly.json).";
187
+ const RELEASE_SAYS = " * the FlySite composite in deploy/fly.ts, into dist/fly.json).";
188
+
189
+ interface Declaration {
190
+ members?: Array<{ dir?: string; kind?: string }>;
191
+ }
192
+
193
+ function planFlySite(ctx: ChantMigrationContext): ChantMigrationPlan | null {
194
+ const { dir } = ctx;
195
+ let decl: Declaration | null = null;
196
+ try {
197
+ decl = JSON.parse(readText(dir, "chant.workspace.json") ?? "null") as Declaration | null;
198
+ } catch {
199
+ decl = null;
200
+ }
201
+ const candidates = (decl?.members ?? []).filter((m) => m.kind === "chant" && m.dir).map((m) => posix.normalize(m.dir!));
202
+ if (!candidates.includes("delivery")) candidates.push("delivery");
203
+
204
+ const changes: PlannedChange[] = [];
205
+ const notMoved: NotMoved[] = [];
206
+ for (const d of candidates) {
207
+ const at = (p: string) => posix.join(d, p);
208
+ const component = readText(dir, at("deploy/app.component.ts"));
209
+ if (component === undefined || !/its\s+\*?\s*migration chud-lexicon-exit\)/.test(component) || !component.includes(COMPONENT_BARE)) continue;
210
+
211
+ const fly = readText(dir, at("deploy/fly.ts"));
212
+ const machine = readText(dir, at("deploy/fly-machine.ts"));
213
+ const site = fly !== undefined && machine !== undefined ? readFlySite(fly, machine) : null;
214
+ if (site) {
215
+ changes.push({ path: at("deploy/fly.ts"), action: "write", why: "the Fly site as the fly lexicon's FlySite composite, the instance the app component deploys", data: Buffer.from(flySiteFile(site)) });
216
+ changes.push({ path: at("deploy/fly-machine.ts"), action: "delete", why: "its Machine is the FlySite composite's, in deploy/fly.ts" });
217
+ const release = readText(dir, at("ops/release.op.ts"));
218
+ if (release?.includes(RELEASE_SAID)) {
219
+ changes.push({ path: at("ops/release.op.ts"), action: "write", why: "its comment names the FlySite composite", data: Buffer.from(release.replace(RELEASE_SAID, RELEASE_SAYS)) });
220
+ }
221
+ } else if (fly !== undefined) {
222
+ notMoved.push({ what: `the Fly site as a composite instance (${at("deploy/fly.ts")} is not the template's, so its resources are kept as they are)`, where: "the fly lexicon's FlySite composite: declare the site with it, and the app component, which names FlySite in `composites`, deploys it" });
223
+ }
224
+ changes.push({ path: at("deploy/app.component.ts"), action: "write", why: "names FlySite in `composites`", data: Buffer.from(component.replace(COMPONENT_BARE, COMPONENT_NAMED).replace(COMPONENT_SAID, COMPONENT_SAYS)) });
225
+ }
226
+ if (changes.length === 0) return null;
227
+ return { id: CHUD_LEXICON_EXIT_FLY_SITE, description: DESCRIPTION, changes, notMoved, conflicts: [] };
228
+ }
229
+
230
+ export const chudLexiconExitFlySite: ChantMigration = {
231
+ id: CHUD_LEXICON_EXIT_FLY_SITE,
232
+ description: DESCRIPTION,
233
+ plan: planFlySite,
234
+ };
@@ -0,0 +1,164 @@
1
+ /**
2
+ * `chud-lexicon-exit-live-names` (#2833): join the migrated `app` component
3
+ * to the Machine its release Op ships to, so `chant components status --live`
4
+ * reads it reconciled instead of stale.
5
+ *
6
+ * `chud-lexicon-exit` (0.92.0) rewrote `deploy/app.component.ts` off chud's
7
+ * app component, and wrote no `liveNames` on it. `components status --live`
8
+ * joins a component to live entities by name (`Component.liveNames`, falling
9
+ * back to the component's own name), and the component is `app` while the
10
+ * Machine the release Op ships to is some other entity. With no `liveNames`,
11
+ * the identity fallback looks for an entity named `app`, finds none, and the
12
+ * status reads `stale` even right after a release.
13
+ *
14
+ * This migration adds `liveNames: [<name>]` to the app component chant
15
+ * wrote, deriving `<name>` from what the member's Fly resources actually
16
+ * declare (#2839) rather than assuming one: while `deploy/fly-machine.ts`
17
+ * still declares the Machine directly, it is the entity `server`, its own
18
+ * top-level export. Once chant#2809's `FlySite` migration has taken over
19
+ * (deploy/fly.ts declares the site with the fly lexicon's `FlySite`
20
+ * composite and `fly-machine.ts` is gone), the composite expands its
21
+ * `machine` member under the site's own instance name, so the Machine is
22
+ * `<instance>Machine` — `flySiteMachine` for the instance name the FlySite
23
+ * migration writes (`flySite`), but read from the file rather than assumed,
24
+ * since a person may rename the export. The two migrations run in registry
25
+ * order (`FlySite` first), so a fresh upgrade that takes both always sees the
26
+ * post-`FlySite` shape by the time this one plans.
27
+ *
28
+ * It looks for the component `chud-lexicon-exit` left (by its own comment and
29
+ * shape) and inserts the field after `dependsOn: [],` rather than next to
30
+ * `archetype` — chant#2809's `FlySite` migration recognises the component by
31
+ * the exact, contiguous text `archetype: "service",\n dependsOn: [],\n`, so
32
+ * anything inserted between those two lines (by either migration, in either
33
+ * order) would stop it from finding a component whose `composites` a person
34
+ * later removes. Anchoring after `dependsOn` instead keeps that pair intact
35
+ * whichever of the two migrations plans first.
36
+ *
37
+ * The plan is empty once `liveNames` already names the Machine this would
38
+ * derive. A component whose `liveNames` is exactly `["server"]` — this
39
+ * migration's own hardcoded output before #2839 — is corrected once the
40
+ * Machine it names is gone (`FlySite` has since taken over); any other
41
+ * hand-set value is left alone, on the same "by any hand" rule the empty
42
+ * case follows. It is a conflict when the component still reads as
43
+ * chud-lexicon-exit's but the `dependsOn: [],` line the edit anchors on is
44
+ * gone, or neither `deploy/fly.ts` nor `deploy/fly-machine.ts` says what the
45
+ * Machine's live name is — either way a person needs to add the mapping by
46
+ * hand.
47
+ */
48
+
49
+ import { posix } from "node:path";
50
+ import type { ChantMigration, ChantMigrationContext, ChantMigrationPlan, PlanConflict, PlannedChange } from "../chant-migrations";
51
+ import { applyEdits, readText, CHUD_LEXICON_EXIT, type Edit } from "./chud-lexicon-exit";
52
+
53
+ export const CHUD_LEXICON_EXIT_LIVE_NAMES = "chud-lexicon-exit-live-names";
54
+
55
+ const DESCRIPTION = "join the app component to the Machine its release Op ships to, so components status --live reads it (#2833)";
56
+
57
+ /** The app component `chud-lexicon-exit` writes, recognised by its own comment and shape (composites or not, ws-056's FlySite migration may have added one). */
58
+ function isExitAppComponent(text: string): boolean {
59
+ return text.includes("wrote this from chud's app component") && text.includes(`migration ${CHUD_LEXICON_EXIT}`) && /name:\s*"app"/.test(text);
60
+ }
61
+
62
+ /** This migration's own hardcoded output before #2839, worth correcting once the Machine it names no longer exists. */
63
+ const BAD_DEFAULT = 'liveNames: ["server"]';
64
+
65
+ /**
66
+ * The live name of the Machine the member's release Op ships to, derived
67
+ * from what `deploy/fly.ts` and `deploy/fly-machine.ts` actually declare
68
+ * (#2839): `<instance>Machine`, from the export name a `FlySite` composite
69
+ * instance is declared under, once chant#2809's migration has taken over; the
70
+ * entity `server`, `fly-machine.ts`'s own top-level export, while that file
71
+ * still declares the Machine directly. Neither shape: null, so the caller
72
+ * leaves it to a person rather than guessing.
73
+ */
74
+ function machineLiveName(dir: string, memberDir: string): string | null {
75
+ const fly = readText(dir, posix.join(memberDir, "deploy/fly.ts"));
76
+ const instance = fly !== undefined ? /^export const (\w+) = FlySite\(/m.exec(fly)?.[1] : undefined;
77
+ if (instance) return `${instance}Machine`;
78
+ if (readText(dir, posix.join(memberDir, "deploy/fly-machine.ts")) !== undefined) return "server";
79
+ return null;
80
+ }
81
+
82
+ function liveNamesEdits(name: string): Edit[] {
83
+ return [
84
+ {
85
+ find: /dependsOn: \[\],\n/,
86
+ replace: `dependsOn: [],\n liveNames: ["${name}"],\n`,
87
+ required: "the dependsOn: [] line",
88
+ },
89
+ {
90
+ // Best-effort: names the Machine in the component's own doc comment, when
91
+ // it still reads as chud-lexicon-exit left it. Not required — an edited
92
+ // comment is left alone. The caller only reaches this when the component
93
+ // has no liveNames yet, so this never double-fires.
94
+ find: " * records no release. The local site is the studio kit's box service\n * (arugula-salad/studio, template/), and the Fly site's resources stay in\n * fly.ts and fly-machine.ts.\n",
95
+ replace:
96
+ " * records no release. The local site is the studio kit's box service\n" +
97
+ " * (arugula-salad/studio, template/), and the Fly site's resources stay in\n" +
98
+ " * fly.ts and fly-machine.ts. Its live name is the Machine the release Op\n" +
99
+ ` * ships to, the entity ${name} (chant#2833), which\n` +
100
+ " * `chant components status --live` observes for it.\n",
101
+ },
102
+ ];
103
+ }
104
+
105
+ interface Declaration {
106
+ members?: Array<{ dir?: string; kind?: string }>;
107
+ }
108
+
109
+ function planLiveNames(ctx: ChantMigrationContext): ChantMigrationPlan | null {
110
+ const { dir } = ctx;
111
+ let decl: Declaration | null = null;
112
+ try {
113
+ decl = JSON.parse(readText(dir, "chant.workspace.json") ?? "null") as Declaration | null;
114
+ } catch {
115
+ decl = null;
116
+ }
117
+ const candidates = (decl?.members ?? []).filter((m) => m.kind === "chant" && m.dir).map((m) => posix.normalize(m.dir!));
118
+ if (!candidates.includes("delivery")) candidates.push("delivery");
119
+
120
+ const changes: PlannedChange[] = [];
121
+ const conflicts: PlanConflict[] = [];
122
+ for (const d of candidates) {
123
+ const path = posix.join(d, "deploy/app.component.ts");
124
+ const text = readText(dir, path);
125
+ if (text === undefined || !isExitAppComponent(text)) continue;
126
+
127
+ if (/liveNames:/.test(text)) {
128
+ // Already there, by any hand: nothing to add. Correct it only when it
129
+ // is exactly this migration's own bad default and the Machine it names
130
+ // is gone — any other value, including the right one, is left alone.
131
+ if (!text.includes(BAD_DEFAULT)) continue;
132
+ const name = machineLiveName(dir, d);
133
+ if (name === null || name === "server") continue;
134
+ changes.push({
135
+ path,
136
+ action: "write",
137
+ why: `liveNames: ["${name}"]: the app component joins the Machine the FlySite composite gives it now, in place of the entity this migration wrongly assumed (#2839)`,
138
+ data: Buffer.from(text.replace(BAD_DEFAULT, `liveNames: ["${name}"]`)),
139
+ });
140
+ continue;
141
+ }
142
+
143
+ const name = machineLiveName(dir, d);
144
+ if (name === null) {
145
+ conflicts.push({ path, reason: "cannot tell the Machine's live name from deploy/fly.ts or deploy/fly-machine.ts. Add liveNames to the app component by hand, then run the upgrade again" });
146
+ continue;
147
+ }
148
+ const edited = applyEdits(text, liveNamesEdits(name));
149
+ if (edited.missing.length > 0) {
150
+ conflicts.push({ path, reason: `cannot find ${edited.missing.join("; ")}. Add liveNames: ["${name}"] to the app component by hand, then run the upgrade again` });
151
+ continue;
152
+ }
153
+ if (edited.text === text) continue;
154
+ changes.push({ path, action: "write", why: `liveNames: ["${name}"]: the app component joins the Machine its release Op ships to`, data: Buffer.from(edited.text) });
155
+ }
156
+ if (changes.length === 0 && conflicts.length === 0) return null;
157
+ return { id: CHUD_LEXICON_EXIT_LIVE_NAMES, description: DESCRIPTION, changes, notMoved: [], conflicts };
158
+ }
159
+
160
+ export const chudLexiconExitLiveNames: ChantMigration = {
161
+ id: CHUD_LEXICON_EXIT_LIVE_NAMES,
162
+ description: DESCRIPTION,
163
+ plan: planLiveNames,
164
+ };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * `chud-lexicon-exit-rollback` (#2800): give a repo `chud-lexicon-exit`
3
+ * migrated the rollback Op chud had, on chant's own steps.
4
+ *
5
+ * `chud-lexicon-exit` shipped in 0.92.0 with a release Op that ships the app
6
+ * member to Fly as a source tree (#2782), and it deleted chud's rollback Op
7
+ * because nothing in chant could take that site back yet. This migration
8
+ * writes `ops/rollback.op.ts` beside the release Op, and a `rollback` script.
9
+ * A repo `chud-lexicon-exit` migrates from now on gets both, since this
10
+ * migration is planned after it in the same upgrade.
11
+ *
12
+ * The rollback is an Op rather than `chant components rollback` because the
13
+ * release is one: the release Op archives, plans, gates, ships and records,
14
+ * and the app component only runs the supply chain. Rolling back through the
15
+ * component would put publish and rollback in two places, which the boundary
16
+ * between Ops and components says not to do (components/backends-reference).
17
+ *
18
+ * The plan is empty unless the member's release Op is the one
19
+ * `chud-lexicon-exit` wrote, and once the rollback Op is there. A file at
20
+ * `ops/rollback.op.ts` that is not this one is a conflict.
21
+ */
22
+
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import { join, posix } from "node:path";
25
+ import type { ChantMigration, ChantMigrationContext, ChantMigrationPlan, PlanConflict, PlannedChange } from "../chant-migrations";
26
+ import { CHUD_LEXICON_EXIT } from "./chud-lexicon-exit";
27
+
28
+ export const CHUD_LEXICON_EXIT_ROLLBACK = "chud-lexicon-exit-rollback";
29
+
30
+ const DESCRIPTION = "add the rollback Op to a repo chud-lexicon-exit migrated: the previous source release back on the Fly site (#2800)";
31
+
32
+ /** The release Op `chud-lexicon-exit` writes, recognised by what it holds. */
33
+ function isExitReleaseOp(text: string): boolean {
34
+ return text.includes(`its migration ${CHUD_LEXICON_EXIT}`) && text.includes("flyRelease(") && text.includes("releasePlan(");
35
+ }
36
+
37
+ export function rollbackOp(pointsRel: string): string {
38
+ return `/**
39
+ * Rollback: put the release the Fly site served before the latest one back
40
+ * on it, once the rollback plan is approved, and record it.
41
+ *
42
+ * \`chant workspace upgrade\` wrote this (its migration ${CHUD_LEXICON_EXIT_ROLLBACK})
43
+ * in place of chud's rollback Op, beside the release Op (ops/release.op.ts):
44
+ *
45
+ * - Plan reads the \`fly\` release ledger. The release it goes back to is the
46
+ * one the site served before the latest release the release Op shipped
47
+ * (\`to: "sha256:..."\` on the plan step picks another). Its plan is read
48
+ * back from chant/lifecycle, its commit's app member is archived again, and
49
+ * the run stops unless the archive hashes to the digest that plan
50
+ * recorded. The rollback plan names both releases and the archive, and is
51
+ * named by its own sha256. Build writes the Fly app's requests.
52
+ * - The \`rollback\` gate approves that plan's digest: \`chant approve rollback
53
+ * rollback --plan <digest>\`. It takes the ship gate's approver count and
54
+ * Cedar policy (decisions/ship-skip.cedar.ts, \`log-only\`): a rollback has
55
+ * no ship-skip answer, so only people pass it.
56
+ * - Roll back reads the archive again, refused unless it still hashes to the
57
+ * planned digest, before any call to Fly. The Machine config the release
58
+ * Op recorded for that release must carry exactly that tree under
59
+ * /srv/app, or nothing changes. Then that config goes back on the Machine,
60
+ * files and start command included, and the Machine must be started with
61
+ * that release (and healthy at \`chud.sites.fly.url\` when set). Migrations
62
+ * are not undone: the data stays where the later release left it.
63
+ * - Record appends the restored release to the \`fly\` release ledger, with
64
+ * \`restores\` naming it, the actor who ran the rollback and the approver.
65
+ *
66
+ * Running it again changes nothing: the same ledger plans the same rollback,
67
+ * so its approval holds, the Machine already serves the release, and the
68
+ * ledger is written once. A release shipped after the rollback is the latest
69
+ * one again, so the next rollback goes back from it.
70
+ */
71
+ import { readFileSync } from "node:fs";
72
+ import { fileURLToPath } from "node:url";
73
+ import { Op, phase, gate, build, releaseRollbackPlan, releaseRollbackRecord } from "@intentius/chant/op";
74
+ import { parsePoints, quorumOf } from "@intentius/chant/workspace/points";
75
+ import { flyRollback } from "@intentius/chant-lexicon-fly";
76
+ import project from "../chant.config.ts";
77
+ import { shipSkipPolicy } from "../decisions/ship-skip.cedar.ts";
78
+
79
+ /** The environment the release Op ships to. */
80
+ const ENV = "fly";
81
+ /** Where the release Op put the files on the Machine. */
82
+ const INTO = "/srv/app";
83
+ const params = project.buildParams;
84
+ /** The Fly site's public URL, when chant.config.ts gives one: the restored release is verified at its health endpoint too. */
85
+ const siteUrl = (project as { chud?: { sites?: { fly?: { url?: string } } } }).chud?.sites?.fly?.url;
86
+
87
+ const pointsFile = fileURLToPath(new URL(${JSON.stringify(pointsRel)}, import.meta.url));
88
+ const shipSkipPoint = parsePoints(readFileSync(pointsFile, "utf-8"), pointsFile)["ship-skip"];
89
+
90
+ const plan = releaseRollbackPlan({ id: "plan", component: "app", env: ENV });
91
+
92
+ export default Op({
93
+ name: "rollback",
94
+ overview: \`Put the previous release of \${params.name.default} back on Fly once the rollback plan is approved.\`,
95
+ phases: [
96
+ phase("Plan", [plan, build(".", { script: "build:fly" })]),
97
+ phase("Gate", [
98
+ gate("rollback", {
99
+ plan: plan.out.digest,
100
+ description: \`Roll \${params.name.default} on \${ENV} back to its previous release\`,
101
+ approval: { quorum: quorumOf(shipSkipPoint), policy: shipSkipPolicy, mode: "log-only" },
102
+ }),
103
+ ]),
104
+ phase("Roll back", [
105
+ flyRollback({
106
+ environment: ENV,
107
+ component: "app",
108
+ plan: "dist/fly.json",
109
+ to: plan.out.to,
110
+ source: { archive: plan.out.archive, digest: plan.out.archiveDigest, dir: plan.out.dir, into: INTO },
111
+ verify: { url: siteUrl, healthPath: params.appHealth.default },
112
+ }),
113
+ ]),
114
+ phase("Record", [releaseRollbackRecord({ plan: plan.out.file, digest: plan.out.digest, approval: { op: "rollback", gate: "rollback" } })]),
115
+ ],
116
+ });
117
+ `;
118
+ }
119
+
120
+ /** The release Op's sentence about the rollback, as chud-lexicon-exit wrote it, and what it says once the rollback Op is there. */
121
+ const RELEASE_SAID = /\), and rolling the site back to the previous\n \* release \(INTENTIUS\/chant#2800\)\. /;
122
+ const RELEASE_SAYS = "). ops/rollback.op.ts rolls the site back to the\n * previous release. ";
123
+
124
+ /** The root docs' lines `chud-lexicon-exit` wrote without a rollback (#2805), and what they say once it is there. Each is left alone when not found. */
125
+ const DOC_EDITS: Array<{ path: string; find: string; replace: string }> = [
126
+ {
127
+ path: "README.md",
128
+ find: "the release Op (Check, the ship-skip decision point, then the ship gate), the app component's supply chain,",
129
+ replace: "the release Op (Check, the ship-skip decision point, then the ship gate) and the rollback Op, the app component's supply chain,",
130
+ },
131
+ {
132
+ path: "README.md",
133
+ find: "npm run release # ship it\n",
134
+ replace: "npm run release # ship it\nnpm run rollback # chant run rollback: the previous release back, through its gate\n",
135
+ },
136
+ { path: "CLAUDE.md", find: "`ops/` (the release Op),", replace: "`ops/` (the release and rollback Ops)," },
137
+ ];
138
+
139
+ interface Declaration {
140
+ members?: Array<{ name?: string; dir?: string; kind?: string }>;
141
+ }
142
+
143
+ function readText(dir: string, path: string): string | undefined {
144
+ const abs = join(dir, path);
145
+ return existsSync(abs) ? readFileSync(abs, "utf-8") : undefined;
146
+ }
147
+
148
+ function planRollback(ctx: ChantMigrationContext): ChantMigrationPlan | null {
149
+ const { dir } = ctx;
150
+ let decl: Declaration | null = null;
151
+ try {
152
+ decl = JSON.parse(readText(dir, "chant.workspace.json") ?? "null") as Declaration | null;
153
+ } catch {
154
+ decl = null;
155
+ }
156
+ const candidates = (decl?.members ?? []).filter((m) => m.kind === "chant" && m.dir).map((m) => posix.normalize(m.dir!));
157
+ if (!candidates.includes("delivery")) candidates.push("delivery");
158
+ const members = candidates.filter((d) => {
159
+ const release = readText(dir, posix.join(d, "ops/release.op.ts"));
160
+ return release !== undefined && isExitReleaseOp(release);
161
+ });
162
+ if (members.length === 0) return null;
163
+
164
+ const changes: PlannedChange[] = [];
165
+ const conflicts: PlanConflict[] = [];
166
+ for (const d of members) {
167
+ const at = (p: string) => posix.join(d, p);
168
+ const opPath = at("ops/rollback.op.ts");
169
+ const text = rollbackOp(posix.relative(at("ops"), "decisions/points.json"));
170
+ const current = readText(dir, opPath);
171
+ if (current === undefined) {
172
+ changes.push({ path: opPath, action: "write", why: "chant's rollback Op: Plan from the release ledger with the archive checked again, the rollback gate, Roll back on Fly, and Record", data: Buffer.from(text) });
173
+ } else if (current !== text && !current.includes(`its migration ${CHUD_LEXICON_EXIT_ROLLBACK}`)) {
174
+ conflicts.push({ path: opPath, reason: "exists and is not the rollback Op this migration writes. Move it aside or delete it, then run the upgrade again" });
175
+ continue;
176
+ }
177
+
178
+ // The release Op's comment named the rollback as not chant's yet.
179
+ const releasePath = at("ops/release.op.ts");
180
+ const release = readText(dir, releasePath)!;
181
+ const said = RELEASE_SAID.exec(release);
182
+ if (said) {
183
+ changes.push({ path: releasePath, action: "write", why: "its comment names ops/rollback.op.ts", data: Buffer.from(release.replace(RELEASE_SAID, RELEASE_SAYS)) });
184
+ }
185
+
186
+ const pkgPath = at("package.json");
187
+ const pkgText = readText(dir, pkgPath);
188
+ if (pkgText !== undefined) {
189
+ try {
190
+ const pkg = JSON.parse(pkgText) as { scripts?: Record<string, string> };
191
+ if (pkg.scripts?.release === "chant run release" && pkg.scripts.rollback === undefined) {
192
+ const scripts: Record<string, string> = {};
193
+ for (const [k, v] of Object.entries(pkg.scripts)) {
194
+ scripts[k] = v;
195
+ if (k === "release") scripts.rollback = "chant run rollback";
196
+ }
197
+ const next = JSON.stringify({ ...pkg, scripts }, null, 2) + "\n";
198
+ changes.push({ path: pkgPath, action: "write", why: "adds `rollback`: chant run rollback", data: Buffer.from(next) });
199
+ }
200
+ } catch {
201
+ conflicts.push({ path: pkgPath, reason: "is not JSON" });
202
+ }
203
+ }
204
+ }
205
+ if (conflicts.length === 0) {
206
+ for (const path of [...new Set(DOC_EDITS.map((e) => e.path))]) {
207
+ const text = readText(dir, path);
208
+ if (text === undefined) continue;
209
+ let next = text;
210
+ for (const e of DOC_EDITS.filter((x) => x.path === path)) {
211
+ if (next.includes(e.find) && !next.includes(e.replace)) next = next.replace(e.find, e.replace);
212
+ }
213
+ if (next !== text) changes.push({ path, action: "write", why: "names the rollback Op", data: Buffer.from(next) });
214
+ }
215
+ }
216
+ return { id: CHUD_LEXICON_EXIT_ROLLBACK, description: DESCRIPTION, changes, notMoved: [], conflicts };
217
+ }
218
+
219
+ export const chudLexiconExitRollback: ChantMigration = {
220
+ id: CHUD_LEXICON_EXIT_ROLLBACK,
221
+ description: DESCRIPTION,
222
+ plan: planRollback,
223
+ };