@intentius/chant 0.79.0 → 0.81.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 (462) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +30 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/report-html.d.ts +5 -0
  5. package/dist/audit/report-html.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +13 -0
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/terraform-state.d.ts +13 -0
  9. package/dist/audit/terraform-state.d.ts.map +1 -1
  10. package/dist/cli/command-group.d.ts +6 -0
  11. package/dist/cli/command-group.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts +44 -3
  13. package/dist/cli/commands/audit.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/doctor.d.ts.map +1 -1
  18. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  19. package/dist/cli/commands/init.d.ts +6 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/commands/update.d.ts +6 -0
  24. package/dist/cli/commands/update.d.ts.map +1 -1
  25. package/dist/cli/commands/vendor.d.ts +5 -0
  26. package/dist/cli/commands/vendor.d.ts.map +1 -1
  27. package/dist/cli/conflict-check.d.ts +6 -0
  28. package/dist/cli/conflict-check.d.ts.map +1 -1
  29. package/dist/cli/handlers/build.d.ts.map +1 -1
  30. package/dist/cli/handlers/components.d.ts.map +1 -1
  31. package/dist/cli/handlers/dev.d.ts.map +1 -1
  32. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  33. package/dist/cli/handlers/init.d.ts.map +1 -1
  34. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  35. package/dist/cli/handlers/misc.d.ts.map +1 -1
  36. package/dist/cli/handlers/operator.d.ts +7 -0
  37. package/dist/cli/handlers/operator.d.ts.map +1 -1
  38. package/dist/cli/handlers/promote.d.ts +20 -0
  39. package/dist/cli/handlers/promote.d.ts.map +1 -0
  40. package/dist/cli/handlers/run-generate.d.ts +39 -0
  41. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  42. package/dist/cli/handlers/run.d.ts +4 -1
  43. package/dist/cli/handlers/run.d.ts.map +1 -1
  44. package/dist/cli/handlers/vendor.d.ts +8 -3
  45. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  46. package/dist/cli/main.d.ts +12 -0
  47. package/dist/cli/main.d.ts.map +1 -1
  48. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  49. package/dist/cli/plugins.d.ts +14 -0
  50. package/dist/cli/plugins.d.ts.map +1 -1
  51. package/dist/cli/registry.d.ts +46 -1
  52. package/dist/cli/registry.d.ts.map +1 -1
  53. package/dist/codegen/fetch.d.ts +10 -0
  54. package/dist/codegen/fetch.d.ts.map +1 -1
  55. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  56. package/dist/components/cli-support.d.ts +3 -1
  57. package/dist/components/cli-support.d.ts.map +1 -1
  58. package/dist/components/discover.d.ts.map +1 -1
  59. package/dist/components/driver.d.ts +16 -2
  60. package/dist/components/driver.d.ts.map +1 -1
  61. package/dist/components/fan-out-support.d.ts.map +1 -1
  62. package/dist/components/gate-plan.d.ts +40 -0
  63. package/dist/components/gate-plan.d.ts.map +1 -0
  64. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  65. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  66. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  67. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  68. package/dist/components/promote.d.ts +264 -0
  69. package/dist/components/promote.d.ts.map +1 -0
  70. package/dist/components/sandbox/driver.d.ts.map +1 -1
  71. package/dist/components/sandbox/run.d.ts.map +1 -1
  72. package/dist/components/verbs/build-archive.d.ts +22 -11
  73. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  74. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  75. package/dist/config-static.d.ts +59 -0
  76. package/dist/config-static.d.ts.map +1 -0
  77. package/dist/config.d.ts +104 -5
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/content-digest.d.ts +17 -0
  80. package/dist/content-digest.d.ts.map +1 -0
  81. package/dist/discovery/files.d.ts +48 -4
  82. package/dist/discovery/files.d.ts.map +1 -1
  83. package/dist/discovery/fold-import.d.ts +32 -1
  84. package/dist/discovery/fold-import.d.ts.map +1 -1
  85. package/dist/discovery/index.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/driver.d.ts +20 -0
  87. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  88. package/dist/discovery/sandbox/fork.d.ts +20 -0
  89. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  90. package/dist/discovery/walk.d.ts +119 -0
  91. package/dist/discovery/walk.d.ts.map +1 -0
  92. package/dist/graph-ir.d.ts +17 -1
  93. package/dist/graph-ir.d.ts.map +1 -1
  94. package/dist/join-key.d.ts +31 -0
  95. package/dist/join-key.d.ts.map +1 -0
  96. package/dist/lexicon-module.d.ts +110 -0
  97. package/dist/lexicon-module.d.ts.map +1 -0
  98. package/dist/lexicon.d.ts +82 -1
  99. package/dist/lexicon.d.ts.map +1 -1
  100. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  101. package/dist/lifecycle/digest.d.ts +5 -1
  102. package/dist/lifecycle/digest.d.ts.map +1 -1
  103. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +64 -0
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/lease.d.ts +8 -1
  108. package/dist/lifecycle/lease.d.ts.map +1 -1
  109. package/dist/lifecycle/legacy-digest.d.ts +59 -0
  110. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  111. package/dist/lifecycle/member-ledger.d.ts +67 -0
  112. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/plan-digest.d.ts +4 -3
  114. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +42 -1
  116. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  117. package/dist/lifecycle/status.d.ts +90 -5
  118. package/dist/lifecycle/status.d.ts.map +1 -1
  119. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  120. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  121. package/dist/op/activities/activity-contracts.d.ts +32 -0
  122. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  123. package/dist/op/activities/index.d.ts +2 -0
  124. package/dist/op/activities/index.d.ts.map +1 -1
  125. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  126. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  127. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  128. package/dist/op/activity-registry.d.ts.map +1 -1
  129. package/dist/op/discover.d.ts.map +1 -1
  130. package/dist/op/gate-approval.d.ts.map +1 -1
  131. package/dist/op/gate-name.d.ts +7 -0
  132. package/dist/op/gate-name.d.ts.map +1 -1
  133. package/dist/op/gate-summary.d.ts +16 -0
  134. package/dist/op/gate-summary.d.ts.map +1 -1
  135. package/dist/op/gate.d.ts +16 -2
  136. package/dist/op/gate.d.ts.map +1 -1
  137. package/dist/op/generate-pipeline.d.ts.map +1 -1
  138. package/dist/op/runtimes/local.d.ts.map +1 -1
  139. package/dist/project-root.d.ts +26 -0
  140. package/dist/project-root.d.ts.map +1 -1
  141. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  142. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  143. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  144. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  145. package/dist/workspace/checks/generated.d.ts +156 -0
  146. package/dist/workspace/checks/generated.d.ts.map +1 -0
  147. package/dist/workspace/checks/ledgers.d.ts +78 -0
  148. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  149. package/dist/workspace/checks/links.d.ts +25 -0
  150. package/dist/workspace/checks/links.d.ts.map +1 -0
  151. package/dist/workspace/checks/pipelines.d.ts +76 -0
  152. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  153. package/dist/workspace/checks.d.ts +161 -0
  154. package/dist/workspace/checks.d.ts.map +1 -0
  155. package/dist/workspace/compose-graph.d.ts +115 -0
  156. package/dist/workspace/compose-graph.d.ts.map +1 -0
  157. package/dist/workspace/compose-reports.d.ts +68 -0
  158. package/dist/workspace/compose-reports.d.ts.map +1 -0
  159. package/dist/workspace/declaration.d.ts +220 -0
  160. package/dist/workspace/declaration.d.ts.map +1 -0
  161. package/dist/workspace/declaration.schema.json +434 -0
  162. package/dist/workspace/generated-files.d.ts +65 -0
  163. package/dist/workspace/generated-files.d.ts.map +1 -0
  164. package/dist/workspace/glob.d.ts +13 -0
  165. package/dist/workspace/glob.d.ts.map +1 -0
  166. package/dist/workspace/graph-cli.d.ts +75 -0
  167. package/dist/workspace/graph-cli.d.ts.map +1 -0
  168. package/dist/workspace/init.d.ts +96 -0
  169. package/dist/workspace/init.d.ts.map +1 -0
  170. package/dist/workspace/jsonc.d.ts +38 -0
  171. package/dist/workspace/jsonc.d.ts.map +1 -0
  172. package/dist/workspace/kinds.d.ts +192 -0
  173. package/dist/workspace/kinds.d.ts.map +1 -0
  174. package/dist/workspace/lineage-check.d.ts +84 -0
  175. package/dist/workspace/lineage-check.d.ts.map +1 -0
  176. package/dist/workspace/lineage-cli.d.ts +31 -0
  177. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  178. package/dist/workspace/lineage-init.d.ts +101 -0
  179. package/dist/workspace/lineage-init.d.ts.map +1 -0
  180. package/dist/workspace/lineage-lock.d.ts +258 -0
  181. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  182. package/dist/workspace/lineage-merge.d.ts +14 -0
  183. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  184. package/dist/workspace/lineage-migrations.d.ts +178 -0
  185. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  186. package/dist/workspace/lineage-update.d.ts +59 -0
  187. package/dist/workspace/lineage-update.d.ts.map +1 -0
  188. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  189. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  190. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  191. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  192. package/dist/workspace/lineage-vendor.d.ts +63 -0
  193. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  194. package/dist/workspace/lineage-version.d.ts +32 -0
  195. package/dist/workspace/lineage-version.d.ts.map +1 -0
  196. package/dist/workspace/links.d.ts +150 -0
  197. package/dist/workspace/links.d.ts.map +1 -0
  198. package/dist/workspace/ls.d.ts +99 -0
  199. package/dist/workspace/ls.d.ts.map +1 -0
  200. package/dist/workspace/member-commands.d.ts +139 -0
  201. package/dist/workspace/member-commands.d.ts.map +1 -0
  202. package/dist/workspace/member-handles.d.ts +54 -0
  203. package/dist/workspace/member-handles.d.ts.map +1 -0
  204. package/dist/workspace/member-pipeline.d.ts +143 -0
  205. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  206. package/dist/workspace/member-run.d.ts +67 -0
  207. package/dist/workspace/member-run.d.ts.map +1 -0
  208. package/dist/workspace/reason-codes.d.ts +65 -0
  209. package/dist/workspace/reason-codes.d.ts.map +1 -0
  210. package/dist/workspace/record-source.d.ts +22 -0
  211. package/dist/workspace/record-source.d.ts.map +1 -0
  212. package/dist/workspace/records-cli.d.ts +81 -0
  213. package/dist/workspace/records-cli.d.ts.map +1 -0
  214. package/dist/workspace/records.d.ts +118 -0
  215. package/dist/workspace/records.d.ts.map +1 -0
  216. package/dist/workspace/root-refusal.d.ts +53 -0
  217. package/dist/workspace/root-refusal.d.ts.map +1 -0
  218. package/dist/workspace/status.d.ts +150 -0
  219. package/dist/workspace/status.d.ts.map +1 -0
  220. package/dist/workspace/template-manifest.d.ts +90 -0
  221. package/dist/workspace/template-manifest.d.ts.map +1 -0
  222. package/dist/workspace/tree.d.ts +36 -0
  223. package/dist/workspace/tree.d.ts.map +1 -0
  224. package/dist/workspace/trust/attestor.d.ts +67 -0
  225. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  226. package/dist/workspace/trust/policy.d.ts +111 -0
  227. package/dist/workspace/trust/policy.d.ts.map +1 -0
  228. package/dist/workspace/trust/provenance.d.ts +67 -0
  229. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  230. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  231. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  232. package/dist/workspace/trust/test-repo.d.ts +42 -0
  233. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  234. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  235. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  236. package/dist/workspace/trust/verify.d.ts +66 -0
  237. package/dist/workspace/trust/verify.d.ts.map +1 -0
  238. package/dist/workspace/which-chant.d.ts +58 -0
  239. package/dist/workspace/which-chant.d.ts.map +1 -0
  240. package/dist/workspace/workspace-kinds.schema.json +133 -0
  241. package/package.json +4 -3
  242. package/src/audit/core.ts +13 -2
  243. package/src/audit/discover.ts +102 -36
  244. package/src/audit/report-html.ts +3 -1
  245. package/src/audit/report-model.ts +14 -1
  246. package/src/audit/terraform-state.test.ts +81 -1
  247. package/src/audit/terraform-state.ts +25 -0
  248. package/src/cli/command-group.ts +7 -0
  249. package/src/cli/commands/audit-truncation.test.ts +167 -0
  250. package/src/cli/commands/audit.ts +132 -29
  251. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  252. package/src/cli/commands/check-lexicon.ts +24 -0
  253. package/src/cli/commands/doctor.test.ts +30 -0
  254. package/src/cli/commands/doctor.ts +21 -0
  255. package/src/cli/commands/import-agents.ts +15 -4
  256. package/src/cli/commands/init.ts +76 -25
  257. package/src/cli/commands/lint.test.ts +52 -1
  258. package/src/cli/commands/lint.ts +41 -71
  259. package/src/cli/commands/onboard.ts +15 -0
  260. package/src/cli/commands/update.ts +18 -5
  261. package/src/cli/commands/vendor.ts +11 -6
  262. package/src/cli/conflict-check.test.ts +18 -1
  263. package/src/cli/conflict-check.ts +13 -0
  264. package/src/cli/discovery-skip.test.ts +168 -0
  265. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  266. package/src/cli/handlers/build-member.test.ts +127 -0
  267. package/src/cli/handlers/build.test.ts +11 -0
  268. package/src/cli/handlers/build.ts +35 -1
  269. package/src/cli/handlers/components.test.ts +127 -0
  270. package/src/cli/handlers/components.ts +70 -8
  271. package/src/cli/handlers/dev.ts +6 -1
  272. package/src/cli/handlers/fan-out.ts +9 -2
  273. package/src/cli/handlers/graph.test.ts +20 -0
  274. package/src/cli/handlers/graph.ts +4 -2
  275. package/src/cli/handlers/init.ts +54 -1
  276. package/src/cli/handlers/lifecycle.ts +2 -1
  277. package/src/cli/handlers/misc.ts +16 -0
  278. package/src/cli/handlers/operator.test.ts +68 -0
  279. package/src/cli/handlers/operator.ts +39 -10
  280. package/src/cli/handlers/promote.test.ts +377 -0
  281. package/src/cli/handlers/promote.ts +391 -0
  282. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  283. package/src/cli/handlers/run-generate.test.ts +158 -0
  284. package/src/cli/handlers/run-generate.ts +168 -0
  285. package/src/cli/handlers/run.test.ts +47 -0
  286. package/src/cli/handlers/run.ts +41 -6
  287. package/src/cli/handlers/vendor.ts +147 -43
  288. package/src/cli/main.test.ts +137 -1
  289. package/src/cli/main.ts +290 -24
  290. package/src/cli/mcp/op-tools.ts +2 -1
  291. package/src/cli/path-lexicon-messages.test.ts +152 -0
  292. package/src/cli/plugins.ts +43 -5
  293. package/src/cli/registry.ts +46 -1
  294. package/src/cli/static-config-read.test.ts +208 -0
  295. package/src/codegen/fetch.test.ts +63 -1
  296. package/src/codegen/fetch.ts +30 -1
  297. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  298. package/src/components/capability-plugin-loader.ts +3 -1
  299. package/src/components/cli-support.ts +9 -3
  300. package/src/components/discover.test.ts +27 -57
  301. package/src/components/discover.ts +20 -51
  302. package/src/components/driver.test.ts +90 -12
  303. package/src/components/driver.ts +43 -5
  304. package/src/components/fan-out-support.ts +2 -1
  305. package/src/components/gate-plan.ts +53 -0
  306. package/src/components/pilots/README.md +1 -1
  307. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  308. package/src/components/presets/ecs-fargate.ts +10 -5
  309. package/src/components/promote.test.ts +645 -0
  310. package/src/components/promote.ts +641 -0
  311. package/src/components/sandbox/driver.ts +8 -8
  312. package/src/components/sandbox/run.ts +4 -1
  313. package/src/components/verbs/build-archive.ts +27 -21
  314. package/src/components/verbs/run-agent.ts +3 -5
  315. package/src/config-static.test.ts +136 -0
  316. package/src/config-static.ts +232 -0
  317. package/src/config.test.ts +40 -0
  318. package/src/config.ts +161 -11
  319. package/src/content-digest.test.ts +120 -0
  320. package/src/content-digest.ts +21 -0
  321. package/src/discovery/files.test.ts +127 -1
  322. package/src/discovery/files.ts +91 -59
  323. package/src/discovery/fold-import.ts +127 -9
  324. package/src/discovery/index.ts +1 -0
  325. package/src/discovery/sandbox/driver.ts +73 -35
  326. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  327. package/src/discovery/sandbox/fork.ts +83 -43
  328. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  329. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  330. package/src/discovery/walk.test.ts +263 -0
  331. package/src/discovery/walk.ts +334 -0
  332. package/src/effect-receipt.ts +2 -2
  333. package/src/graph-ir.ts +18 -1
  334. package/src/join-key.test.ts +25 -0
  335. package/src/join-key.ts +39 -0
  336. package/src/lexicon-module.test.ts +203 -0
  337. package/src/lexicon-module.ts +215 -0
  338. package/src/lexicon.ts +105 -1
  339. package/src/lifecycle/build-ledger-store.ts +13 -2
  340. package/src/lifecycle/digest.ts +9 -5
  341. package/src/lifecycle/gate-ledger.test.ts +20 -0
  342. package/src/lifecycle/gate-ledger.ts +28 -6
  343. package/src/lifecycle/git.test.ts +13 -1
  344. package/src/lifecycle/git.ts +221 -176
  345. package/src/lifecycle/lease.ts +25 -10
  346. package/src/lifecycle/legacy-digest.test.ts +206 -0
  347. package/src/lifecycle/legacy-digest.ts +103 -0
  348. package/src/lifecycle/member-ledger.test.ts +319 -0
  349. package/src/lifecycle/member-ledger.ts +145 -0
  350. package/src/lifecycle/plan-digest.ts +7 -7
  351. package/src/lifecycle/release-ledger.test.ts +20 -0
  352. package/src/lifecycle/release-ledger.ts +53 -3
  353. package/src/lifecycle/status.test.ts +151 -1
  354. package/src/lifecycle/status.ts +228 -7
  355. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  356. package/src/op/activities/activity-contracts.ts +37 -0
  357. package/src/op/activities/index.ts +10 -0
  358. package/src/op/activities/propose-upgrade.ts +206 -0
  359. package/src/op/activity-contract-registry.ts +18 -4
  360. package/src/op/activity-registry.ts +26 -0
  361. package/src/op/discover.test.ts +14 -4
  362. package/src/op/discover.ts +19 -23
  363. package/src/op/gate-approval.ts +3 -1
  364. package/src/op/gate-name.ts +8 -0
  365. package/src/op/gate-summary.test.ts +14 -0
  366. package/src/op/gate-summary.ts +25 -2
  367. package/src/op/gate.ts +36 -7
  368. package/src/op/generate-pipeline.test.ts +14 -5
  369. package/src/op/generate-pipeline.ts +2 -1
  370. package/src/op/runtimes/local.ts +2 -1
  371. package/src/project-root.test.ts +66 -1
  372. package/src/project-root.ts +48 -0
  373. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  374. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  375. package/src/workspace/behold-kinds.test.ts +162 -0
  376. package/src/workspace/check-contract.test.ts +127 -0
  377. package/src/workspace/check.schema.json +347 -0
  378. package/src/workspace/checks/generated.test.ts +287 -0
  379. package/src/workspace/checks/generated.ts +475 -0
  380. package/src/workspace/checks/ledgers.test.ts +139 -0
  381. package/src/workspace/checks/ledgers.ts +246 -0
  382. package/src/workspace/checks/links.ts +183 -0
  383. package/src/workspace/checks/pipelines.test.ts +133 -0
  384. package/src/workspace/checks/pipelines.ts +225 -0
  385. package/src/workspace/checks-repo.test.ts +71 -0
  386. package/src/workspace/checks.test.ts +450 -0
  387. package/src/workspace/checks.ts +461 -0
  388. package/src/workspace/compose-graph.test.ts +146 -0
  389. package/src/workspace/compose-graph.ts +228 -0
  390. package/src/workspace/compose-reports.test.ts +109 -0
  391. package/src/workspace/compose-reports.ts +186 -0
  392. package/src/workspace/declaration.schema.json +434 -0
  393. package/src/workspace/declaration.test.ts +347 -0
  394. package/src/workspace/declaration.ts +623 -0
  395. package/src/workspace/generated-files.test.ts +114 -0
  396. package/src/workspace/generated-files.ts +94 -0
  397. package/src/workspace/glob.ts +49 -0
  398. package/src/workspace/graph-cli.ts +220 -0
  399. package/src/workspace/graph-contract.test.ts +204 -0
  400. package/src/workspace/graph.schema.json +662 -0
  401. package/src/workspace/init.test.ts +223 -0
  402. package/src/workspace/init.ts +585 -0
  403. package/src/workspace/jsonc.test.ts +65 -0
  404. package/src/workspace/jsonc.ts +231 -0
  405. package/src/workspace/kinds.test.ts +288 -0
  406. package/src/workspace/kinds.ts +506 -0
  407. package/src/workspace/lineage-check.ts +264 -0
  408. package/src/workspace/lineage-cli.ts +114 -0
  409. package/src/workspace/lineage-init.test.ts +231 -0
  410. package/src/workspace/lineage-init.ts +365 -0
  411. package/src/workspace/lineage-lock.test.ts +224 -0
  412. package/src/workspace/lineage-lock.ts +357 -0
  413. package/src/workspace/lineage-merge.ts +44 -0
  414. package/src/workspace/lineage-migrations.test.ts +178 -0
  415. package/src/workspace/lineage-migrations.ts +343 -0
  416. package/src/workspace/lineage-update.ts +160 -0
  417. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  418. package/src/workspace/lineage-upgrade.test.ts +474 -0
  419. package/src/workspace/lineage-upgrade.ts +624 -0
  420. package/src/workspace/lineage-vendor.test.ts +126 -0
  421. package/src/workspace/lineage-vendor.ts +215 -0
  422. package/src/workspace/lineage-version.ts +127 -0
  423. package/src/workspace/links.test.ts +344 -0
  424. package/src/workspace/links.ts +331 -0
  425. package/src/workspace/ls-contract.test.ts +200 -0
  426. package/src/workspace/ls.schema.json +190 -0
  427. package/src/workspace/ls.ts +254 -0
  428. package/src/workspace/member-commands.e2e.test.ts +244 -0
  429. package/src/workspace/member-commands.test.ts +217 -0
  430. package/src/workspace/member-commands.ts +540 -0
  431. package/src/workspace/member-handles.test.ts +76 -0
  432. package/src/workspace/member-handles.ts +228 -0
  433. package/src/workspace/member-pipeline.test.ts +135 -0
  434. package/src/workspace/member-pipeline.ts +317 -0
  435. package/src/workspace/member-run.ts +146 -0
  436. package/src/workspace/read-contract.test.ts +129 -0
  437. package/src/workspace/reason-codes.test.ts +113 -0
  438. package/src/workspace/reason-codes.ts +81 -0
  439. package/src/workspace/record-source.ts +117 -0
  440. package/src/workspace/records-cli.ts +198 -0
  441. package/src/workspace/records-contract.test.ts +93 -0
  442. package/src/workspace/records.schema.json +150 -0
  443. package/src/workspace/records.test.ts +243 -0
  444. package/src/workspace/records.ts +384 -0
  445. package/src/workspace/root-refusal.ts +97 -0
  446. package/src/workspace/status-contract.test.ts +308 -0
  447. package/src/workspace/status.schema.json +210 -0
  448. package/src/workspace/status.ts +412 -0
  449. package/src/workspace/template-manifest.test.ts +79 -0
  450. package/src/workspace/template-manifest.ts +221 -0
  451. package/src/workspace/tree.ts +132 -0
  452. package/src/workspace/trust/attestor.ts +93 -0
  453. package/src/workspace/trust/policy.ts +306 -0
  454. package/src/workspace/trust/provenance.ts +181 -0
  455. package/src/workspace/trust/ssh-commit.ts +138 -0
  456. package/src/workspace/trust/test-repo.ts +138 -0
  457. package/src/workspace/trust/trust.test.ts +397 -0
  458. package/src/workspace/trust/verify-cli.ts +56 -0
  459. package/src/workspace/trust/verify.ts +194 -0
  460. package/src/workspace/which-chant.test.ts +117 -0
  461. package/src/workspace/which-chant.ts +151 -0
  462. package/src/workspace/workspace-kinds.schema.json +133 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * A chant member's outputs and parameters, read from its source without
3
+ * running it (#2539, #2524 D6).
4
+ *
5
+ * `chant workspace check` resolves member links in source and never runs
6
+ * member code, so it can't build a member to learn its outputs. This reader
7
+ * parses the member's TypeScript files and picks out the two handles a join
8
+ * uses, the way the graph IR names them (`exports` and `imports`):
9
+ *
10
+ * - an output is the name passed to `output(ref, "Name")`, or the binding of
11
+ * `export const name = stackOutput(ref)`;
12
+ * - a parameter is the binding of `export const name = new Parameter(...)`.
13
+ *
14
+ * Only calls whose function is imported from `@intentius/chant` or a chant
15
+ * lexicon count. An output name must be a string literal, or a `const` in the
16
+ * same file that is one. Any other name can't be read without running the
17
+ * code, so it is a gap: the member's list of outputs is then incomplete, and a
18
+ * link to a name the reader didn't find is unresolved rather than missing.
19
+ *
20
+ * The reader loads the TypeScript parser and nothing else from the project.
21
+ */
22
+
23
+ import * as ts from "typescript";
24
+ import { isInside, type Declaration, type ResolvedGroup } from "./declaration";
25
+ import type { KindRegistry } from "./kinds";
26
+ import { kindHandles, type MemberHandles } from "./links";
27
+ import { joinPath, skippedDir, type WorkspaceTree } from "./tree";
28
+
29
+ const memberDir = (dir: string) => (dir === "." ? "" : dir);
30
+
31
+ /** One output or parameter found in source. */
32
+ export interface SourceHandle {
33
+ name: string;
34
+ /** Tree-relative file and 1-based line, for messages. */
35
+ file: string;
36
+ line: number;
37
+ }
38
+
39
+ export interface SourceHandles {
40
+ outputs: SourceHandle[];
41
+ /** Parameters: the names a consumer reads from outside. */
42
+ inputs: SourceHandle[];
43
+ /** Why the list of outputs may be incomplete, one line each. Empty when it is complete. */
44
+ gaps: string[];
45
+ }
46
+
47
+ /** Directories inside a member that hold no project source. */
48
+ const NOT_SOURCE = new Set(["dist", "coverage", "generated"]);
49
+
50
+ const CHANT_MODULE = /^@intentius\/chant(-lexicon-[a-z0-9-]+)?(\/.*)?$/;
51
+
52
+ function isSourceFile(name: string): boolean {
53
+ if (!/\.(m?ts|tsx)$/.test(name) || name.endsWith(".d.ts")) return false;
54
+ if (/\.(test|spec)\.m?tsx?$/.test(name)) return false;
55
+ return !name.startsWith("chant.config.");
56
+ }
57
+
58
+ /**
59
+ * Every source file under `dir` (tree-relative), leaving out `exclude`d
60
+ * directories (other members, group matches), `node_modules`, dot
61
+ * directories and build output.
62
+ */
63
+ function sourceFiles(tree: WorkspaceTree, dir: string, exclude: ReadonlySet<string>): string[] {
64
+ const out: string[] = [];
65
+ const walk = (at: string) => {
66
+ for (const e of tree.list(at) ?? []) {
67
+ const path = joinPath(at, e.name);
68
+ if (e.type === "dir") {
69
+ if (skippedDir(e.name) || NOT_SOURCE.has(e.name) || exclude.has(path)) continue;
70
+ walk(path);
71
+ } else if (isSourceFile(e.name)) {
72
+ out.push(path);
73
+ }
74
+ }
75
+ };
76
+ walk(dir);
77
+ return out.sort();
78
+ }
79
+
80
+ /** The chant functions a file imports, by local name, and the namespaces it imports chant modules as. */
81
+ interface ChantImports {
82
+ names: Map<string, string>;
83
+ namespaces: Set<string>;
84
+ }
85
+
86
+ function chantImports(source: ts.SourceFile): ChantImports {
87
+ const names = new Map<string, string>();
88
+ const namespaces = new Set<string>();
89
+ for (const s of source.statements) {
90
+ if (!ts.isImportDeclaration(s) || !ts.isStringLiteral(s.moduleSpecifier)) continue;
91
+ const from = s.moduleSpecifier.text;
92
+ // The components API has its own stackOutput(stack, name), which reads an output rather than declaring one.
93
+ if (!CHANT_MODULE.test(from) || from.includes("/components")) continue;
94
+ const bindings = s.importClause?.namedBindings;
95
+ if (!bindings) continue;
96
+ if (ts.isNamespaceImport(bindings)) namespaces.add(bindings.name.text);
97
+ else for (const el of bindings.elements) names.set(el.name.text, (el.propertyName ?? el.name).text);
98
+ }
99
+ return { names, namespaces };
100
+ }
101
+
102
+ /** The chant function a callee names (`output`, `ns.Parameter`), or undefined. */
103
+ function chantCallee(expr: ts.Expression, imports: ChantImports): string | undefined {
104
+ if (ts.isIdentifier(expr)) return imports.names.get(expr.text);
105
+ if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && imports.namespaces.has(expr.expression.text)) {
106
+ return expr.name.text;
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ /** The top-level `const` initializers of a file, for reading a name held in one. */
112
+ function fileConsts(source: ts.SourceFile): Map<string, ts.Expression> {
113
+ const consts = new Map<string, ts.Expression>();
114
+ for (const s of source.statements) {
115
+ if (!ts.isVariableStatement(s) || (s.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
116
+ for (const d of s.declarationList.declarations) if (ts.isIdentifier(d.name) && d.initializer) consts.set(d.name.text, d.initializer);
117
+ }
118
+ return consts;
119
+ }
120
+
121
+ /** A string the expression is without running anything, or undefined. */
122
+ function literalString(expr: ts.Expression, consts: Map<string, ts.Expression>, seen = new Set<string>()): string | undefined {
123
+ if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return expr.text;
124
+ if (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isSatisfiesExpression(expr)) return literalString(expr.expression, consts, seen);
125
+ if (ts.isIdentifier(expr) && !seen.has(expr.text)) {
126
+ const init = consts.get(expr.text);
127
+ if (init) return literalString(init, consts, new Set([...seen, expr.text]));
128
+ }
129
+ return undefined;
130
+ }
131
+
132
+ function isExported(s: ts.Statement): boolean {
133
+ return (ts.canHaveModifiers(s) ? ts.getModifiers(s) : undefined)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
134
+ }
135
+
136
+ /** Read one file's outputs and parameters into `out`. */
137
+ function readFile(path: string, text: string, out: SourceHandles): void {
138
+ const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, false);
139
+ const imports = chantImports(source);
140
+ if (imports.names.size === 0 && imports.namespaces.size === 0) return;
141
+ const consts = fileConsts(source);
142
+ const lineOf = (node: ts.Node) => source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
143
+
144
+ // Exported bindings: stackOutput outputs and Parameter inputs are named by them.
145
+ for (const s of source.statements) {
146
+ if (!ts.isVariableStatement(s) || !isExported(s)) continue;
147
+ for (const d of s.declarationList.declarations) {
148
+ if (!ts.isIdentifier(d.name) || !d.initializer) continue;
149
+ const init = d.initializer;
150
+ if (ts.isNewExpression(init) && chantCallee(init.expression, imports) === "Parameter") {
151
+ out.inputs.push({ name: d.name.text, file: path, line: lineOf(d) });
152
+ } else if (ts.isCallExpression(init) && chantCallee(init.expression, imports) === "stackOutput") {
153
+ const first = init.arguments[0];
154
+ if (first && !ts.isStringLiteralLike(first)) out.outputs.push({ name: d.name.text, file: path, line: lineOf(d) });
155
+ }
156
+ }
157
+ }
158
+
159
+ // output(ref, "Name") anywhere in the file.
160
+ const visit = (node: ts.Node) => {
161
+ if (ts.isCallExpression(node) && chantCallee(node.expression, imports) === "output" && node.arguments.length >= 2) {
162
+ const name = literalString(node.arguments[1], consts);
163
+ if (name !== undefined) out.outputs.push({ name, file: path, line: lineOf(node) });
164
+ else out.gaps.push(`${path}:${lineOf(node)}: an output whose name is not a string literal`);
165
+ }
166
+ ts.forEachChild(node, visit);
167
+ };
168
+ visit(source);
169
+ }
170
+
171
+ /**
172
+ * Read a chant member's outputs and parameters from the source under `dir`
173
+ * (tree-relative, `""` for the root), leaving out the `exclude`d directories.
174
+ * Runs no member code.
175
+ */
176
+ export function readSourceHandles(tree: WorkspaceTree, dir: string, exclude: ReadonlySet<string> = new Set()): SourceHandles {
177
+ const out: SourceHandles = { outputs: [], inputs: [], gaps: [] };
178
+ for (const file of sourceFiles(tree, dir, exclude)) {
179
+ let text: string;
180
+ try {
181
+ text = tree.read(file);
182
+ } catch {
183
+ out.gaps.push(`${file}: the file could not be read`);
184
+ continue;
185
+ }
186
+ readFile(file, text, out);
187
+ }
188
+ return out;
189
+ }
190
+
191
+ /**
192
+ * Every member's handles, read in source for `chant workspace check`. A
193
+ * `chant` member's outputs and parameters are parsed from its TypeScript
194
+ * without running it; other kinds expose what the kind and the entry list.
195
+ * A member whose directory is missing or whose kind is unknown is left out,
196
+ * so links to it are unresolved.
197
+ */
198
+ export function sourceMemberHandles(
199
+ declaration: Declaration,
200
+ tree: WorkspaceTree,
201
+ groups: readonly ResolvedGroup[],
202
+ kinds: KindRegistry,
203
+ ): Map<string, MemberHandles> {
204
+ const out = new Map<string, MemberHandles>();
205
+ for (const m of declaration.members) {
206
+ const kind = kinds.get(m.kind);
207
+ if (!kind || tree.stat(memberDir(m.dir)) !== "dir") continue;
208
+ const listed = kindHandles(m, kind);
209
+ if (listed) {
210
+ out.set(m.name, listed);
211
+ } else {
212
+ // Other members' directories and group matches inside this one are not its source.
213
+ const exclude = new Set<string>();
214
+ for (const o of declaration.members) if (o !== m && o.dir !== "." && isInside(o.dir, m.dir)) exclude.add(o.dir);
215
+ for (const g of groups) for (const dir of g.matches) if (isInside(dir, m.dir)) exclude.add(dir);
216
+ const read = readSourceHandles(tree, memberDir(m.dir), exclude);
217
+ out.set(m.name, {
218
+ member: m.name,
219
+ outputs: read.outputs.map((o) => ({ name: o.name })),
220
+ inputs: read.inputs.map((i) => ({ name: i.name })),
221
+ complete: read.gaps.length === 0,
222
+ why: read.gaps.length === 0 ? null : read.gaps.length === 1 ? read.gaps[0] : `${read.gaps[0]}, and ${read.gaps.length - 1} more`,
223
+ exposesNone: false,
224
+ });
225
+ }
226
+ }
227
+ return out;
228
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Per-member pipelines (#2542): which member a project belongs to, where its
3
+ * pipeline goes, the command that regenerates it, and the interim record of
4
+ * generated files (`.chant/generated.json`, until #2541).
5
+ */
6
+
7
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
8
+ import { existsSync, readFileSync, rmSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { twoMemberWorkspace } from "./__fixtures__/two-member-workspace";
11
+ import { findWorkspaceRoot } from "../project-root";
12
+ import {
13
+ GENERATED_RECORD_FILE,
14
+ memberPipelineFile,
15
+ planMemberComponentPipeline,
16
+ planMemberOpPipelines,
17
+ readGeneratedRecord,
18
+ recordGeneratedFiles,
19
+ resolveMemberContext,
20
+ writeMemberComponentPipeline,
21
+ } from "./member-pipeline";
22
+
23
+ let repo: string;
24
+ beforeEach(() => {
25
+ repo = twoMemberWorkspace();
26
+ });
27
+ afterEach(() => {
28
+ rmSync(repo, { recursive: true, force: true });
29
+ });
30
+
31
+ function context(dir: string) {
32
+ const found = findWorkspaceRoot(dir);
33
+ expect(found).toBeDefined();
34
+ return resolveMemberContext(dir, found!);
35
+ }
36
+
37
+ describe("which member a project belongs to", () => {
38
+ test("the member's directory and anything under it", () => {
39
+ expect(context(join(repo, "services/api"))?.pipeline).toEqual({ name: "api", dir: "services/api" });
40
+ expect(context(join(repo, "services/api/src"))?.member.name).toBe("api");
41
+ expect(context(join(repo, "apps/web"))?.pipeline).toEqual({ name: "web", dir: "apps/web" });
42
+ });
43
+
44
+ test("an example group's match and a directory no member holds are no member", () => {
45
+ expect(context(join(repo, "examples/demo"))).toBeUndefined();
46
+ expect(context(repo)).toBeUndefined();
47
+ });
48
+
49
+ test("a root member excludes the other members and the group matches", () => {
50
+ rmSync(repo, { recursive: true, force: true });
51
+ repo = twoMemberWorkspace({ root: true });
52
+ expect(context(repo)?.pipeline).toEqual({ name: "platform", dir: ".", exclude: ["apps/web", "examples/demo", "services/api"] });
53
+ expect(context(join(repo, "services/api"))?.member.name).toBe("api");
54
+ });
55
+
56
+ test("a workspace below the repository root gives repository-relative directories", () => {
57
+ rmSync(repo, { recursive: true, force: true });
58
+ repo = twoMemberWorkspace({ prefix: "estate" });
59
+ const ctx = context(join(repo, "estate/services/api"));
60
+ expect(ctx?.pipeline).toEqual({ name: "api", dir: "estate/services/api" });
61
+ expect(ctx?.repoRoot).toBe(repo);
62
+ });
63
+ });
64
+
65
+ describe("the component pipeline's file and command", () => {
66
+ test("default files are named after the member and the environment, at the forge's path", () => {
67
+ expect(memberPipelineFile("github", "api", "staging")).toBe(".github/workflows/chant-api-staging.yml");
68
+ expect(memberPipelineFile("forgejo", "api", "staging")).toBe(".forgejo/workflows/chant-api-staging.yml");
69
+ expect(memberPipelineFile("gitlab", "api", "staging")).toBe(".gitlab/ci/chant-api-staging.gitlab-ci.yml");
70
+ expect(memberPipelineFile("jenkins", "api", "staging")).toBeUndefined();
71
+ });
72
+
73
+ test("the plan names the file, and the command runs in the member's directory", () => {
74
+ const dir = join(repo, "services/api");
75
+ const plan = planMemberComponentPipeline({ projectDir: dir, found: findWorkspaceRoot(dir)!, lexicon: "github", env: "staging", promoteTo: "prod" })!;
76
+ expect(plan.file).toBe(".github/workflows/chant-api-staging.yml");
77
+ expect(plan.target).toBe(join(repo, ".github/workflows/chant-api-staging.yml"));
78
+ expect(plan.member).toEqual({ name: "api", dir: "services/api", file: ".github/workflows/chant-api-staging.yml" });
79
+ expect(plan.command).toBe("chant build --components --generate github --env staging --promote-to prod");
80
+ });
81
+
82
+ test("--output keeps the path, relative to the member in the command", () => {
83
+ const dir = join(repo, "services/api");
84
+ const plan = planMemberComponentPipeline({
85
+ projectDir: dir,
86
+ found: findWorkspaceRoot(dir)!,
87
+ lexicon: "gitlab",
88
+ output: join(repo, "ci/api.yml"),
89
+ })!;
90
+ expect(plan.file).toBe("ci/api.yml");
91
+ expect(plan.command).toBe("chant build --components --generate gitlab --env production --output ../../ci/api.yml");
92
+ });
93
+
94
+ test("a forge with no default path needs --output", () => {
95
+ const dir = join(repo, "services/api");
96
+ expect(() => planMemberComponentPipeline({ projectDir: dir, found: findWorkspaceRoot(dir)!, lexicon: "jenkins" })).toThrow(/pass --output/);
97
+ });
98
+
99
+ test("writing puts the header on the file and records it under the member", () => {
100
+ const dir = join(repo, "services/api");
101
+ const plan = planMemberComponentPipeline({ projectDir: dir, found: findWorkspaceRoot(dir)!, lexicon: "github", env: "staging" })!;
102
+ writeMemberComponentPipeline(plan, "name: x\n");
103
+ const written = readFileSync(plan.target, "utf-8");
104
+ expect(written.split("\n")[0]).toBe(
105
+ "# Generated by chant — do not edit directly. Regenerate with: chant build --components --generate github --env staging (in the member's directory)",
106
+ );
107
+ expect(written.endsWith("name: x\n")).toBe(true);
108
+ expect(readGeneratedRecord(dir)).toEqual({
109
+ schema: 1,
110
+ files: [{ path: ".github/workflows/chant-api-staging.yml", command: "chant build --components --generate github --env staging", env: "staging" }],
111
+ });
112
+ expect(existsSync(join(dir, GENERATED_RECORD_FILE))).toBe(true);
113
+ });
114
+ });
115
+
116
+ describe("the Op pipelines' directory and command", () => {
117
+ test("files go to the forge's directory at the repository root", () => {
118
+ const dir = join(repo, "apps/web");
119
+ const plan = planMemberOpPipelines({ projectDir: dir, found: findWorkspaceRoot(dir)!, provider: "gitlab", specFile: join(dir, "ops.json") })!;
120
+ expect(plan.outDir).toBe(join(repo, ".gitlab/ci"));
121
+ expect(plan.member).toEqual({ name: "web", dir: "apps/web", fileDir: ".gitlab/ci" });
122
+ expect(plan.command).toBe("chant run --generate gitlab --spec ops.json");
123
+ });
124
+ });
125
+
126
+ describe("the generated-file record", () => {
127
+ test("entries are keyed by path and sorted, so regenerating leaves the file unchanged", () => {
128
+ const dir = join(repo, "apps/web");
129
+ recordGeneratedFiles(dir, [{ path: "b.yml", command: "x" }, { path: "a.yml", command: "y" }]);
130
+ const first = readFileSync(join(dir, GENERATED_RECORD_FILE), "utf-8");
131
+ recordGeneratedFiles(dir, [{ path: "a.yml", command: "y" }]);
132
+ expect(readFileSync(join(dir, GENERATED_RECORD_FILE), "utf-8")).toBe(first);
133
+ expect(readGeneratedRecord(dir).files.map((f) => f.path)).toEqual(["a.yml", "b.yml"]);
134
+ });
135
+ });
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Per-member CI pipelines (#2542; #2524 D19, ws-041 and ws-042).
3
+ *
4
+ * When `chant build --components --generate` or `chant run --generate` runs
5
+ * for a project inside a workspace member, the generator is told which
6
+ * member it is generating for ({@link PipelineMember}). The generated
7
+ * pipeline is then filtered to the member's paths, its jobs run in the
8
+ * member's directory, and its file is named after the member so two members
9
+ * never write the same file.
10
+ *
11
+ * Forges read CI files from fixed paths at the repository root, outside any
12
+ * member's directory. The member whose command generates such a file owns
13
+ * it (ws-042), and records it in its list of generated files. That list
14
+ * belongs in the member's `generated` entry in `chant.workspace.json` once
15
+ * #2541 lands. Until then it is `.chant/generated.json` in the member's
16
+ * directory ({@link GENERATED_RECORD_FILE}), which `WSP081` reads to allow
17
+ * one declarer per file (./checks/pipelines.ts).
18
+ *
19
+ * The command handlers import this module only after `findWorkspaceRoot`
20
+ * has found a declaration, so a project outside any workspace never loads it.
21
+ */
22
+
23
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
24
+ import { dirname, join, relative, resolve, sep } from "node:path";
25
+ import { GENERATED_MARKER } from "../discovery/files";
26
+ import type { PipelineMember } from "../lexicon";
27
+ import type { WorkspaceRootSearch } from "../project-root";
28
+ import { ownerOf, readDeclaration, resolveGroups, rootExclusions, WorkspaceReadError, type Declaration, type Member } from "./declaration";
29
+ import { gitTop, workingTree } from "./tree";
30
+
31
+ /**
32
+ * TODO(#2541): the interim home of a member's generated-file list, relative
33
+ * to the member's directory. #2541 moves the list into the member's entry in
34
+ * the declaration, and this file goes away.
35
+ */
36
+ export const GENERATED_RECORD_FILE = ".chant/generated.json";
37
+
38
+ /** One generated file a member owns. */
39
+ export interface GeneratedFileEntry {
40
+ /** The file, relative to the repository root, with `/` separators. */
41
+ path: string;
42
+ /** The command that regenerates it, run in the member's directory. */
43
+ command: string;
44
+ /** The environment the pipeline deploys, for a component pipeline. */
45
+ env?: string;
46
+ }
47
+
48
+ export interface GeneratedRecord {
49
+ schema: 1;
50
+ files: GeneratedFileEntry[];
51
+ }
52
+
53
+ /** The member a project belongs to, and where it sits. */
54
+ export interface MemberContext {
55
+ declaration: Declaration;
56
+ member: Member;
57
+ /** Absolute path of the workspace root. */
58
+ workspaceRoot: string;
59
+ /** Absolute path of the repository root: the git top, or the workspace root outside git. */
60
+ repoRoot: string;
61
+ /** Absolute path of the member's directory. */
62
+ memberRoot: string;
63
+ /** What the generators read: name, repository-relative directory and exclusions. */
64
+ pipeline: PipelineMember;
65
+ }
66
+
67
+ function real(path: string): string {
68
+ try {
69
+ return realpathSync(path);
70
+ } catch {
71
+ return resolve(path);
72
+ }
73
+ }
74
+
75
+ /** `path` relative to `from`, `/`-separated, `"."` for `from` itself. */
76
+ function relPath(from: string, path: string): string {
77
+ const rel = relative(from, path).split(sep).join("/");
78
+ return rel === "" ? "." : rel;
79
+ }
80
+
81
+ function joinRel(prefix: string, dir: string): string {
82
+ if (prefix === ".") return dir;
83
+ return dir === "." ? prefix : `${prefix}/${dir}`;
84
+ }
85
+
86
+ /**
87
+ * The member that owns `projectDir`, given the declaration `findWorkspaceRoot`
88
+ * found. Undefined when no member owns it: the directory sits in an example
89
+ * group's match, or outside every member of a workspace with no root member.
90
+ * Throws `WorkspaceReadError` when the declaration can't be read.
91
+ */
92
+ export function resolveMemberContext(projectDir: string, found: WorkspaceRootSearch): MemberContext | undefined {
93
+ const workspaceRoot = real(found.dir);
94
+ const tree = workingTree(workspaceRoot);
95
+ const declaration = readDeclaration(tree);
96
+ const groups = resolveGroups(declaration, tree);
97
+ const at = relPath(workspaceRoot, real(projectDir));
98
+ if (at.startsWith("..")) return undefined;
99
+ const owner = ownerOf(declaration, groups, at);
100
+ if (!owner || !("member" in owner)) return undefined;
101
+ const member = owner.member;
102
+
103
+ const repoRoot = real(gitTop(workspaceRoot) ?? workspaceRoot);
104
+ const prefix = relPath(repoRoot, workspaceRoot);
105
+ const pipeline: PipelineMember = { name: member.name, dir: joinRel(prefix, member.dir) };
106
+ if (member.dir === ".") {
107
+ const exclude = rootExclusions(declaration, groups).map((e) => joinRel(prefix, e.dir));
108
+ if (exclude.length > 0) pipeline.exclude = exclude;
109
+ }
110
+ return {
111
+ declaration,
112
+ member,
113
+ workspaceRoot,
114
+ repoRoot,
115
+ memberRoot: member.dir === "." ? workspaceRoot : join(workspaceRoot, member.dir),
116
+ pipeline,
117
+ };
118
+ }
119
+
120
+ /** Where each forge reads a member's component pipeline from, relative to the repository root. */
121
+ const PIPELINE_DIRS: Record<string, string> = {
122
+ github: ".github/workflows",
123
+ forgejo: ".forgejo/workflows",
124
+ // GitLab reads only .gitlab-ci.yml, which includes these (`include: local: .gitlab/ci/*.gitlab-ci.yml`).
125
+ gitlab: ".gitlab/ci",
126
+ };
127
+
128
+ /** The directory a forge's generated files go in for a member, relative to the repository root, or undefined for a forge chant doesn't know. */
129
+ export function memberPipelineDir(provider: string): string | undefined {
130
+ return PIPELINE_DIRS[provider];
131
+ }
132
+
133
+ /**
134
+ * The default path of a member's component pipeline for one environment,
135
+ * relative to the repository root: `chant-<member>-<env>.yml` in the forge's
136
+ * workflow directory, or `.gitlab/ci/chant-<member>-<env>.gitlab-ci.yml`.
137
+ */
138
+ export function memberPipelineFile(provider: string, member: string, env: string): string | undefined {
139
+ const dir = PIPELINE_DIRS[provider];
140
+ if (dir === undefined) return undefined;
141
+ return `${dir}/chant-${member}-${env}${provider === "gitlab" ? ".gitlab-ci.yml" : ".yml"}`;
142
+ }
143
+
144
+ /** A repository-relative path for an absolute one. */
145
+ export function repoRelative(ctx: MemberContext, absolute: string): string {
146
+ return relPath(ctx.repoRoot, resolve(absolute));
147
+ }
148
+
149
+ /** Read a member's generated-file record, or an empty one. */
150
+ export function readGeneratedRecord(memberRoot: string): GeneratedRecord {
151
+ const file = join(memberRoot, GENERATED_RECORD_FILE);
152
+ if (!existsSync(file)) return { schema: 1, files: [] };
153
+ const parsed = JSON.parse(readFileSync(file, "utf-8")) as Partial<GeneratedRecord>;
154
+ return { schema: 1, files: Array.isArray(parsed.files) ? parsed.files : [] };
155
+ }
156
+
157
+ /**
158
+ * Add or replace entries in a member's generated-file record, keyed by path,
159
+ * and write it back sorted, so regenerating the same files leaves it
160
+ * byte-identical.
161
+ */
162
+ export function recordGeneratedFiles(memberRoot: string, entries: GeneratedFileEntry[]): string {
163
+ const record = readGeneratedRecord(memberRoot);
164
+ const byPath = new Map(record.files.map((f) => [f.path, f]));
165
+ for (const e of entries) byPath.set(e.path, e);
166
+ const files = [...byPath.values()].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
167
+ const file = join(memberRoot, GENERATED_RECORD_FILE);
168
+ mkdirSync(dirname(file), { recursive: true });
169
+ writeFileSync(file, JSON.stringify({ schema: 1, files }, null, 2) + "\n");
170
+ return file;
171
+ }
172
+
173
+ /** Quote one argument for a recorded command line when it needs it. */
174
+ export function shellArg(arg: string): string {
175
+ return /^[A-Za-z0-9._\/=:@%+,-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`;
176
+ }
177
+
178
+ /** The first line of a member's generated pipeline, naming the command that regenerates it. */
179
+ export function generatedHeader(command: string): string {
180
+ return `# ${GENERATED_MARKER}. Regenerate with: ${command} (in the member's directory)\n`;
181
+ }
182
+
183
+ /** What `chant build --components --generate` needs to write a member's pipeline. */
184
+ export interface MemberComponentPlan {
185
+ ctx: MemberContext;
186
+ /** Passed to the generator as `options.member`. */
187
+ member: PipelineMember;
188
+ /** The pipeline file, relative to the repository root. */
189
+ file: string;
190
+ /** The pipeline file's absolute path. */
191
+ target: string;
192
+ /** The command that regenerates it, run in the member's directory. */
193
+ command: string;
194
+ env: string;
195
+ }
196
+
197
+ export interface ComponentPlanInput {
198
+ projectDir: string;
199
+ found: WorkspaceRootSearch;
200
+ lexicon: string;
201
+ /** `--env`, or undefined for the generator's default. */
202
+ env?: string;
203
+ promoteTo?: string;
204
+ /** `--output`, relative to the current directory. */
205
+ output?: string;
206
+ params?: string[];
207
+ paramsFile?: string;
208
+ }
209
+
210
+ /** The generators' own default environment when `--env` is not given. */
211
+ const DEFAULT_ENV = "production";
212
+
213
+ /**
214
+ * Plan a member's component pipeline: which member, which file and which
215
+ * command. Undefined when the project belongs to no member. Throws with a
216
+ * readable message when there is no default file for the forge and no
217
+ * `--output`.
218
+ */
219
+ export function planMemberComponentPipeline(input: ComponentPlanInput): MemberComponentPlan | undefined {
220
+ const ctx = resolveMemberContext(input.projectDir, input.found);
221
+ if (!ctx) return undefined;
222
+ const env = input.env ?? DEFAULT_ENV;
223
+ let target: string;
224
+ if (input.output) {
225
+ target = resolve(input.output);
226
+ } else {
227
+ const file = memberPipelineFile(input.lexicon, ctx.member.name, env);
228
+ if (file === undefined) {
229
+ throw new Error(`there is no default pipeline path for ${input.lexicon} in a workspace member; pass --output <file>`);
230
+ }
231
+ target = join(ctx.repoRoot, file);
232
+ }
233
+ const file = repoRelative(ctx, target);
234
+
235
+ const parts = ["chant", "build", "--components", "--generate", input.lexicon, "--env", env];
236
+ if (input.promoteTo) parts.push("--promote-to", input.promoteTo);
237
+ for (const p of input.params ?? []) parts.push("--param", p);
238
+ if (input.paramsFile) parts.push("--params-file", relPath(ctx.memberRoot, resolve(input.paramsFile)));
239
+ if (input.output) parts.push("--output", relPath(ctx.memberRoot, target));
240
+
241
+ return {
242
+ ctx,
243
+ member: { ...ctx.pipeline, file },
244
+ file,
245
+ target,
246
+ command: parts.map(shellArg).join(" "),
247
+ env,
248
+ };
249
+ }
250
+
251
+ /** Write a member's pipeline with its header and record it as the member's generated file. */
252
+ export function writeMemberComponentPipeline(plan: MemberComponentPlan, yaml: string): void {
253
+ mkdirSync(dirname(plan.target), { recursive: true });
254
+ writeFileSync(plan.target, generatedHeader(plan.command) + yaml);
255
+ recordGeneratedFiles(plan.ctx.memberRoot, [{ path: plan.file, command: plan.command, env: plan.env }]);
256
+ }
257
+
258
+ /** What `chant run --generate` needs to write a member's Op pipelines. */
259
+ export interface MemberOpPlan {
260
+ ctx: MemberContext;
261
+ member: PipelineMember;
262
+ /** Absolute directory the files are written to. */
263
+ outDir: string;
264
+ command: string;
265
+ }
266
+
267
+ /** Plan a member's Op pipelines. Undefined when the directory belongs to no member. */
268
+ export function planMemberOpPipelines(input: {
269
+ projectDir: string;
270
+ found: WorkspaceRootSearch;
271
+ provider: string;
272
+ output?: string;
273
+ specFile?: string;
274
+ }): MemberOpPlan | undefined {
275
+ const ctx = resolveMemberContext(input.projectDir, input.found);
276
+ if (!ctx) return undefined;
277
+ let outDir: string;
278
+ if (input.output) {
279
+ outDir = resolve(input.output);
280
+ } else {
281
+ const dir = memberPipelineDir(input.provider);
282
+ if (dir === undefined) {
283
+ throw new Error(`there is no default pipeline directory for ${input.provider} in a workspace member; pass --output <dir>`);
284
+ }
285
+ outDir = join(ctx.repoRoot, dir);
286
+ }
287
+ const parts = ["chant", "run", "--generate", input.provider];
288
+ if (input.specFile) parts.push("--spec", relPath(ctx.memberRoot, resolve(input.specFile)));
289
+ if (input.output) parts.push("--output", relPath(ctx.memberRoot, outDir));
290
+ return {
291
+ ctx,
292
+ member: { ...ctx.pipeline, fileDir: repoRelative(ctx, outDir) },
293
+ outDir,
294
+ command: parts.map(shellArg).join(" "),
295
+ };
296
+ }
297
+
298
+ /** Write a member's Op pipeline files with their header and record them. Returns the paths written, relative to the current directory. */
299
+ export function writeMemberOpPipelines(plan: MemberOpPlan, files: { name: string; yaml: string }[]): string[] {
300
+ const written: string[] = [];
301
+ const entries: GeneratedFileEntry[] = [];
302
+ for (const f of files) {
303
+ const target = join(plan.outDir, f.name);
304
+ mkdirSync(dirname(target), { recursive: true });
305
+ writeFileSync(target, generatedHeader(plan.command) + f.yaml);
306
+ entries.push({ path: repoRelative(plan.ctx, target), command: plan.command });
307
+ written.push(relative(process.cwd(), target) || target);
308
+ }
309
+ recordGeneratedFiles(plan.ctx.memberRoot, entries);
310
+ return written;
311
+ }
312
+
313
+ /** A message for an error from planning: a declaration error with its file and line, or the error's own message. */
314
+ export function describeError(err: unknown): string {
315
+ if (err instanceof WorkspaceReadError) return err.describe();
316
+ return err instanceof Error ? err.message : String(err);
317
+ }