@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
@@ -12,6 +12,7 @@
12
12
 
13
13
  import * as baseActivities from "./activities";
14
14
  import { ACTIVITY_PROFILES, type ActivityProfile } from "./activity-profiles";
15
+ import { importLexiconModule } from "../lexicon-module";
15
16
 
16
17
  export type { ActivityProfile } from "./activity-profiles";
17
18
 
@@ -52,6 +53,16 @@ export async function loadActivities(lexicons: string[] = []): Promise<Map<strin
52
53
 
53
54
  for (const name of lexicons) {
54
55
  try {
56
+ // chant #2520 — a lexicon declared by module path has no subpath to
57
+ // import; its plugin's `activities()` member stands in for it.
58
+ const local = await importLexiconModule(name);
59
+ if (local !== undefined) {
60
+ const plugin = Object.values(local).find((v) => isActivityContributor(v) && v.name === name) as
61
+ | ActivityContributor
62
+ | undefined;
63
+ if (plugin) collectActivities(await plugin.activities(), activities);
64
+ continue;
65
+ }
55
66
  const spec = `@intentius/chant-lexicon-${name}/op/activities`;
56
67
  collectActivities((await import(spec)) as Record<string, unknown>, activities);
57
68
  } catch {
@@ -62,6 +73,21 @@ export async function loadActivities(lexicons: string[] = []): Promise<Map<strin
62
73
  return activities;
63
74
  }
64
75
 
76
+ /** A plugin-shaped export with an `activities()` member (`LexiconPlugin.activities`). Structural, so this layer does not import `../lexicon.ts`. */
77
+ interface ActivityContributor {
78
+ name: string;
79
+ activities(): Record<string, unknown> | Promise<Record<string, unknown>>;
80
+ }
81
+
82
+ function isActivityContributor(value: unknown): value is ActivityContributor {
83
+ return (
84
+ typeof value === "object" &&
85
+ value !== null &&
86
+ typeof (value as { name?: unknown }).name === "string" &&
87
+ typeof (value as { activities?: unknown }).activities === "function"
88
+ );
89
+ }
90
+
65
91
  /**
66
92
  * The built-in profile table. Kept async because it is awaited on the CLI's
67
93
  * run path alongside {@link loadActivities}, and because a hosted runtime may
@@ -1,6 +1,11 @@
1
1
  import { describe, test, expect, vi, beforeEach } from "vitest";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import { discoverOps } from "./discover";
3
5
 
6
+ /** The example project that holds `ops/alb-deploy.op.ts`. */
7
+ const ALB_INFRA = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../examples/gitlab-aws-alb-infra");
8
+
4
9
  // Mock getRuntime to return git root pointing at the repo root. Partial, via
5
10
  // `importOriginal`: the modules a discovered Op file pulls in reach the rest of
6
11
  // this module (`moduleDir`, for the lint presets), and a wholesale replacement
@@ -23,14 +28,19 @@ vi.mock("../runtime-adapter", async (importOriginal) => ({
23
28
  }));
24
29
 
25
30
  describe("discoverOps", () => {
26
- test("discovers alb-deploy.op.ts from examples/", async () => {
27
- const { ops, errors } = await discoverOps();
31
+ test("discovers alb-deploy.op.ts from its own project", async () => {
32
+ const { ops, errors } = await discoverOps({ cwd: ALB_INFRA });
28
33
  expect(errors).toHaveLength(0);
29
34
  expect(ops.has("alb-deploy")).toBe(true);
30
35
  });
31
36
 
32
- test("discovered Op has correct config shape", async () => {
37
+ test("from the git root, a child project's Ops are not discovered (#2527)", async () => {
33
38
  const { ops } = await discoverOps();
39
+ expect(ops.has("alb-deploy")).toBe(false);
40
+ });
41
+
42
+ test("discovered Op has correct config shape", async () => {
43
+ const { ops } = await discoverOps({ cwd: ALB_INFRA });
34
44
  const op = ops.get("alb-deploy");
35
45
  expect(op).toBeDefined();
36
46
  expect(op!.config.name).toBe("alb-deploy");
@@ -40,7 +50,7 @@ describe("discoverOps", () => {
40
50
  });
41
51
 
42
52
  test("filePath points to the .op.ts source file", async () => {
43
- const { ops } = await discoverOps();
53
+ const { ops } = await discoverOps({ cwd: ALB_INFRA });
44
54
  const op = ops.get("alb-deploy");
45
55
  expect(op!.filePath).toMatch(/alb-deploy\.op\.ts$/);
46
56
  });
@@ -1,8 +1,10 @@
1
1
  import { getRuntime } from "../runtime-adapter";
2
- import { readdir } from "node:fs/promises";
3
2
  import { existsSync } from "node:fs";
4
3
  import { dirname, join, resolve } from "node:path";
5
4
  import type { OpConfig } from "./types";
5
+ import { walkDiscovery, workspaceMemberDirs } from "../discovery/walk";
6
+ import { hasDiscoveryMarkerSync } from "../discovery/files";
7
+ import { resolveDiscoveryGlobs } from "../config";
6
8
 
7
9
  export interface DiscoveredOp {
8
10
  config: OpConfig;
@@ -52,28 +54,22 @@ async function findDiscoveryRoot(cwd?: string): Promise<string> {
52
54
  return gitRoot;
53
55
  }
54
56
 
55
- async function collectOpFiles(dir: string): Promise<string[]> {
56
- const files: string[] = [];
57
- let entries;
58
- try {
59
- entries = await readdir(dir, { withFileTypes: true });
60
- } catch {
61
- return files;
62
- }
63
- for (const entry of entries) {
64
- const fullPath = join(dir, entry.name);
65
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules" && entry.name !== "dist") {
66
- files.push(...await collectOpFiles(fullPath));
67
- } else if (
68
- entry.isFile() &&
69
- entry.name.endsWith(".op.ts") &&
70
- !entry.name.endsWith(".test.ts") &&
71
- !entry.name.endsWith(".spec.ts")
72
- ) {
73
- files.push(fullPath);
74
- }
75
- }
76
- return files;
57
+ /**
58
+ * Every `*.op.ts` file below the Op root, through the one discovery walk
59
+ * (`../discovery/walk.ts`, #2527). The walk down stops at child projects
60
+ * whatever the root is, so a configless layout whose Op root is the git root
61
+ * no longer collects its sibling projects' Ops. The project's
62
+ * `exclude`/`include` globs and the skip marker apply.
63
+ */
64
+ async function collectOpFiles(root: string): Promise<string[]> {
65
+ return walkDiscovery({
66
+ walker: "ops",
67
+ root,
68
+ globs: await resolveDiscoveryGlobs(root),
69
+ excludeDirs: await workspaceMemberDirs(root),
70
+ accept: (name, full) =>
71
+ name.endsWith(".op.ts") && !name.endsWith(".test.ts") && !name.endsWith(".spec.ts") && !hasDiscoveryMarkerSync(full),
72
+ });
77
73
  }
78
74
 
79
75
  /** The `OpConfig` behind an exported value, or `undefined` when the value is not an Op. An Op entity carries its config on `.props` (`./resource.ts`); `name` and `phases` are what every consumer of a discovered Op reads. */
@@ -32,6 +32,7 @@
32
32
  */
33
33
 
34
34
  import { createHash } from "node:crypto";
35
+ import { lexiconModulePath } from "../lexicon-module";
35
36
  import { isStepOutputRef, type StepOutputRef } from "./step-output-ref";
36
37
 
37
38
  /** What a policy's decision does to the gate. */
@@ -221,7 +222,8 @@ function isContextValue(value: unknown): boolean {
221
222
 
222
223
  /** Import `lexicon`'s evaluator. Throws a message that names the package when it is not installed or exports none. */
223
224
  export async function loadGatePolicyEvaluator(lexicon: string): Promise<GatePolicyEvaluator> {
224
- const spec = `@intentius/chant-lexicon-${lexicon}/gate-policy`;
225
+ // chant #2520 — a lexicon declared by module path exports its evaluator from that module.
226
+ const spec = lexiconModulePath(lexicon) ?? `@intentius/chant-lexicon-${lexicon}/gate-policy`;
225
227
  let mod: Partial<GatePolicyEvaluator>;
226
228
  try {
227
229
  mod = (await import(spec)) as Partial<GatePolicyEvaluator>;
@@ -29,6 +29,14 @@ import type { OpConfig, PhaseDefinition, StepDefinition } from "./types";
29
29
  */
30
30
  export const FAN_OUT_GATE_OP = "fan-out";
31
31
 
32
+ /**
33
+ * The op name a `chant workspace upgrade` gate is recorded under (#2550). The
34
+ * gate is the scope (`.` for a plain project), and it binds the digest of the
35
+ * upgrade's patch. Like a fan-out, the upgrade is a command with no `*.op.ts`
36
+ * behind it, so `chant approve` must not warn that it found none.
37
+ */
38
+ export const WORKSPACE_UPGRADE_GATE_OP = "workspace-upgrade";
39
+
32
40
  /** Either spelling of a gate step's name. `gate` since #2202; `signalName` through 0.59.0. */
33
41
  export interface GateNamed {
34
42
  gate?: string;
@@ -58,6 +58,20 @@ describe("gatedRunSummaryMarkdown (#2243)", () => {
58
58
  expect(md).toContain("and not the next run");
59
59
  });
60
60
 
61
+ // #2574 with #2538: a component gate in a workspace member names both its
62
+ // environment in the approve line and the member's gate ledger path.
63
+ test("a member's component gate names its environment and its member ledger path", () => {
64
+ const md = gatedRunSummaryMarkdown({
65
+ op: "search-service",
66
+ gate: "release",
67
+ expiresAt: "2026-09-08T00:00:00Z",
68
+ environment: "prod",
69
+ ledgerPrefix: "_members/services/search/",
70
+ });
71
+ expect(md).toContain("chant approve search-service release --env prod --approver <you>");
72
+ expect(md).toContain("`_members/services/search/_gates/search-service.jsonl`");
73
+ });
74
+
61
75
  test("a gate that binds no plan says nothing about one", () => {
62
76
  expect(gatedRunSummaryMarkdown(summary)).not.toContain("and not the next run");
63
77
  });
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { appendFileSync } from "node:fs";
22
22
  import { gateLedgerPath } from "../lifecycle/gate-ledger";
23
+ import { resolveMemberLedger } from "../lifecycle/member-ledger";
23
24
  import { approveCommand } from "./gate";
24
25
 
25
26
  /** What a gated run knows about the gate it stopped on. */
@@ -50,6 +51,28 @@ export interface GatedRunSummary {
50
51
  * to see that the digest has moved on.
51
52
  */
52
53
  planDigest?: string;
54
+ /**
55
+ * The project's ledger prefix, `_members/<member>/` for a workspace member
56
+ * (#2538), so the ledger path shown is the one the fact was written to.
57
+ * Absent at level 0 and for the root member.
58
+ */
59
+ ledgerPrefix?: string;
60
+ /** The environment a component gate's pending fact was recorded in (#2574). The `chant approve` line names it. */
61
+ environment?: string;
62
+ }
63
+
64
+ /**
65
+ * `{ ledgerPrefix }` for the project at `cwd` when it is a workspace member,
66
+ * else `{}`. A summary is a report, so a declaration that cannot be read
67
+ * leaves the path as it is rather than failing the report.
68
+ */
69
+ export async function summaryLedgerPrefix(cwd: string = process.cwd()): Promise<{ ledgerPrefix?: string }> {
70
+ try {
71
+ const { prefix } = await resolveMemberLedger(cwd);
72
+ return prefix ? { ledgerPrefix: prefix } : {};
73
+ } catch {
74
+ return {};
75
+ }
53
76
  }
54
77
 
55
78
  /**
@@ -78,10 +101,10 @@ export function gatedRunSummaryMarkdown(summary: GatedRunSummary): string {
78
101
  "Approve it, then re-run this workflow:",
79
102
  "",
80
103
  "```",
81
- `${approveCommand(summary.op, summary.gate)} --approver <you>`,
104
+ `${approveCommand(summary.op, summary.gate, summary.environment)} --approver <you>`,
82
105
  "```",
83
106
  "",
84
- `Ledger: \`${gateLedgerPath(summary.op)}\` on the \`chant/lifecycle\` branch.`,
107
+ `Ledger: \`${gateLedgerPath(summary.op, summary.ledgerPrefix)}\` on the \`chant/lifecycle\` branch.`,
85
108
  );
86
109
  if (summary.expiresAt) lines.push(`Expires: ${summary.expiresAt}`);
87
110
  if (summary.url) lines.push(`Approve at: ${summary.url}`);
package/src/op/gate.ts CHANGED
@@ -170,6 +170,14 @@ export interface GateCheckInput {
170
170
  * the standing pending fact satisfies the gate whatever has changed since.
171
171
  */
172
172
  planDigest?: string;
173
+ /**
174
+ * The environment this run deploys to (#2574). The component driver sets it,
175
+ * because a component's gate ledger is keyed by component and holds every
176
+ * environment's facts. Set, the gate reads only this environment's pending
177
+ * facts and resolutions (plus resolutions that record no environment, which
178
+ * come back as a mismatch), and the pending fact it records carries it.
179
+ */
180
+ environment?: string;
173
181
  /**
174
182
  * The gate's quorum and policy (#2508), context already resolved. Absent,
175
183
  * one approval passes the gate, which is the rule every gate had before.
@@ -194,6 +202,8 @@ export interface GateDigestMismatch {
194
202
  resolvedBy: string;
195
203
  /** When they recorded it. */
196
204
  timestamp: string;
205
+ /** The environment this run deploys to, on a component gate (#2574). The approve command a refusal prints names it. */
206
+ environment?: string;
197
207
  }
198
208
 
199
209
  /**
@@ -210,15 +220,18 @@ export interface GateDigestMismatch {
210
220
  export function describeGateMismatch(op: string, gate: string, mismatch: GateDigestMismatch): string {
211
221
  const why =
212
222
  mismatch.approved === undefined
213
- ? "That resolution predates plan-bound gates (#2300) and records no plan at all, so it cannot " +
214
- "answer for this one. Approving again binds it:"
223
+ ? mismatch.environment !== undefined
224
+ ? "That approval predates environment- and plan-bound component gates (#2574) and records neither, " +
225
+ "so it cannot answer for this one. Approving again binds it:"
226
+ : "That resolution predates plan-bound gates (#2300) and records no plan at all, so it cannot " +
227
+ "answer for this one. Approving again binds it:"
215
228
  : "The configuration or the live system changed between that approval and this plan, so it needs " +
216
229
  "a fresh one:";
217
230
  return (
218
231
  `Gate "${gate}" is approved, but not for this plan. ` +
219
232
  `approved: ${describePlanDigest(mismatch.approved)} (by ${mismatch.resolvedBy} at ${mismatch.timestamp}); ` +
220
233
  `planned: ${mismatch.planned}. ` +
221
- `${why} ${approveCommand(op, gate)}`
234
+ `${why} ${approveCommand(op, gate, mismatch.environment)}`
222
235
  );
223
236
  }
224
237
 
@@ -374,7 +387,17 @@ const EPOCH = new Date(0).toISOString();
374
387
  */
375
388
  export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput): Promise<GateCheck> {
376
389
  const now = input.now ?? new Date().toISOString();
377
- const { resolutions, pending } = await port.read(input.op);
390
+ const ledger = await port.read(input.op);
391
+ // #2574: a component's ledger holds every environment's facts. This run
392
+ // answers only to its own environment's pending facts and approvals. An
393
+ // approval that records no environment at all is kept, so it comes back as
394
+ // a mismatch with a message instead of silently not counting.
395
+ const env = input.environment;
396
+ const pending = env === undefined ? ledger.pending : ledger.pending.filter((p) => p.environment === env);
397
+ const resolutions =
398
+ env === undefined
399
+ ? ledger.resolutions
400
+ : ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env);
378
401
 
379
402
  const standing = latestPendingGate(pending, input.gate);
380
403
  const since = standing?.timestamp ?? EPOCH;
@@ -410,6 +433,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
410
433
  planned: input.planDigest!,
411
434
  resolvedBy: mismatched.resolvedBy,
412
435
  timestamp: mismatched.timestamp,
436
+ ...(env !== undefined ? { environment: env } : {}),
413
437
  }
414
438
  : undefined;
415
439
  const asMismatch = { ...(mismatch ? { mismatch } : {}), ...(quorum ? { quorum } : {}) };
@@ -441,6 +465,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
441
465
  ...(input.runId ? { runId: input.runId } : {}),
442
466
  ...(url ? { url } : {}),
443
467
  ...(input.planDigest !== undefined ? { planDigest: input.planDigest } : {}),
468
+ ...(env !== undefined ? { environment: env } : {}),
444
469
  ...(input.approval ? { approval: input.approval } : {}),
445
470
  });
446
471
  return {
@@ -453,7 +478,11 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
453
478
  };
454
479
  }
455
480
 
456
- /** The line every renderer prints to say how a pending gate is cleared. */
457
- export function approveCommand(op: string, gate: string): string {
458
- return `chant approve ${op} ${gate}`;
481
+ /**
482
+ * The line every renderer prints to say how a pending gate is cleared. A
483
+ * component gate names its environment (#2574), so the approval picks that
484
+ * environment's pending fact when others stand beside it.
485
+ */
486
+ export function approveCommand(op: string, gate: string, environment?: string): string {
487
+ return environment === undefined ? `chant approve ${op} ${gate}` : `chant approve ${op} ${gate} --env ${environment}`;
459
488
  }
@@ -3,12 +3,16 @@
3
3
  * `../components/cli-support.ts`'s `generateComponentsPipeline`. Mirrors that
4
4
  * module's own test style (`cli-support.test.ts`): a minimal mocked lexicon
5
5
  * plugin satisfying `isLexiconPlugin`, real `discoverOps()` resolution
6
- * against this repo's actual `*.op.ts` fixtures (`examples/alb-deploy.op.ts`
7
- * — see `./discover.test.ts`) so Op-name validation exercises the real
8
- * discovery path rather than a stub.
6
+ * against this repo's actual `*.op.ts` fixtures
7
+ * (`examples/gitlab-aws-alb-infra/ops/alb-deploy.op.ts`, see
8
+ * `./discover.test.ts`) so Op-name validation exercises the real discovery
9
+ * path rather than a stub. Discovery starts in that example: since #2527 the
10
+ * walk from the git root stops at child projects, so the repo root finds none.
9
11
  */
10
12
 
11
13
  import { describe, test, expect, vi } from "vitest";
14
+ import { dirname, resolve } from "node:path";
15
+ import { fileURLToPath } from "node:url";
12
16
  import { generateOpsPipeline, withOpSchedules } from "./generate-pipeline";
13
17
  import type { DiscoveredOp } from "./discover";
14
18
  import type { OpConfig } from "./types";
@@ -34,6 +38,9 @@ vi.mock("@intentius/chant-lexicon-gitlab", () => ({
34
38
  },
35
39
  }));
36
40
 
41
+ /** The example project that holds `alb-deploy.op.ts`. */
42
+ const ALB_INFRA = resolve(dirname(fileURLToPath(import.meta.url)), "../../../../examples/gitlab-aws-alb-infra");
43
+
37
44
  describe("generateOpsPipeline", () => {
38
45
  test("errors when the target lexicon has no generateOpPipeline", async () => {
39
46
  const result = await generateOpsPipeline([{ name: "alb-deploy", schedule: "0 6 * * *" }], "aws");
@@ -45,6 +52,8 @@ describe("generateOpsPipeline", () => {
45
52
  const result = await generateOpsPipeline(
46
53
  [{ name: "definitely-not-a-real-op", schedule: "0 6 * * *" }],
47
54
  "gitlab",
55
+ undefined,
56
+ ALB_INFRA,
48
57
  );
49
58
  expect(result.success).toBe(false);
50
59
  expect(result.error).toMatch(/Unknown Op\(s\): definitely-not-a-real-op/);
@@ -52,7 +61,7 @@ describe("generateOpsPipeline", () => {
52
61
 
53
62
  test("validates every named Op and delegates to the lexicon plugin", async () => {
54
63
  const specs: ScheduledOpSpec[] = [{ name: "alb-deploy", schedule: "0 6 * * *", findingMode: "issue" }];
55
- const result = await generateOpsPipeline(specs, "gitlab");
64
+ const result = await generateOpsPipeline(specs, "gitlab", undefined, ALB_INFRA);
56
65
  expect(result.success).toBe(true);
57
66
  expect(result.files).toEqual([{ name: "alb-deploy.yml", yaml: "# alb-deploy @ 0 6 * * *" }]);
58
67
  expect(result.jobs).toEqual([
@@ -66,7 +75,7 @@ describe("generateOpsPipeline", () => {
66
75
  // `PipelineAuditOp`, `ReconcileOp`) only ever sets `schedule`.
67
76
  test("a legacy `{ schedule }` spec with no `trigger` round-trips unchanged", async () => {
68
77
  const specs: ScheduledOpSpec[] = [{ name: "alb-deploy", schedule: "0 6 * * *" }];
69
- const result = await generateOpsPipeline(specs, "gitlab");
78
+ const result = await generateOpsPipeline(specs, "gitlab", undefined, ALB_INFRA);
70
79
  expect(result.success).toBe(true);
71
80
  expect(result.files).toEqual([{ name: "alb-deploy.yml", yaml: "# alb-deploy @ 0 6 * * *" }]);
72
81
  expect(result.jobs).toEqual([
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import { discoverOps, type DiscoveredOp } from "./discover";
20
+ import { lexiconModulePath } from "../lexicon-module";
20
21
  import {
21
22
  isLexiconPlugin,
22
23
  type LexiconPlugin,
@@ -36,7 +37,7 @@ import {
36
37
  async function loadLexiconPlugin(name: string): Promise<LexiconPlugin | null> {
37
38
  let mod: Record<string, unknown>;
38
39
  try {
39
- mod = (await import(`@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
40
+ mod = (await import(lexiconModulePath(name) ?? `@intentius/chant-lexicon-${name}`)) as Record<string, unknown>;
40
41
  } catch {
41
42
  return null;
42
43
  }
@@ -18,6 +18,7 @@
18
18
  * answers.
19
19
  */
20
20
 
21
+ import { lexiconNames } from "../../lexicon-module";
21
22
  import { loadChantConfig } from "../../config";
22
23
  import { loadActivities, loadProfiles } from "../activity-registry";
23
24
  import { runOpLocally, OpRunFailure, type OpRunResult } from "../local-executor";
@@ -129,7 +130,7 @@ export function createLocalOpRuntime(opts: { projectPath?: string } = {}): OpRun
129
130
  // unreadable config just yields the base activities.
130
131
  let lexicons: string[] = [];
131
132
  try {
132
- lexicons = (await loadChantConfig(projectPath)).config.lexicons ?? [];
133
+ lexicons = lexiconNames((await loadChantConfig(projectPath)).config.lexicons ?? []);
133
134
  } catch {
134
135
  // No/invalid chant.config — base activities only.
135
136
  }
@@ -2,7 +2,7 @@ import { describe, test, expect, beforeEach, afterEach } from "vitest";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { tmpdir } from "os";
5
- import { findProjectConfig, findProjectRoot } from "./project-root";
5
+ import { findProjectConfig, findProjectRoot, findWorkspaceRoot } from "./project-root";
6
6
 
7
7
  // Every test builds its own isolated directory tree under a fresh tmpdir —
8
8
  // never reused across tests, and never anywhere near the real repo's own
@@ -103,3 +103,68 @@ describe("findProjectConfig / findProjectRoot (chant #1117)", () => {
103
103
  expect(result.configPath).toBe(join(projectRoot, "chant.config.ts"));
104
104
  });
105
105
  });
106
+
107
+ describe("findWorkspaceRoot (chant #2534)", () => {
108
+ const declare = (dir: string, name = "chant.workspace.json") => {
109
+ mkdirSync(dir, { recursive: true });
110
+ writeFileSync(join(dir, name), "{}");
111
+ };
112
+
113
+ test("walks up to the nearest declaration, and never looks at chant.config", () => {
114
+ mkdirSync(join(root, ".git"));
115
+ declare(root);
116
+ const deep = join(root, "lexicons", "aws", "examples", "one");
117
+ mkdirSync(deep, { recursive: true });
118
+ writeFileSync(join(root, "lexicons", "aws", "package.json"), "{}");
119
+ writeFileSync(join(deep, "chant.config.ts"), "export default {};");
120
+ expect(findWorkspaceRoot(deep)).toEqual({ dir: root, file: join(root, "chant.workspace.json") });
121
+ // findProjectConfig is unchanged: it still stops at the project's own config.
122
+ expect(findProjectConfig(deep).dir).toBe(deep);
123
+ });
124
+
125
+ test("the nearest declaration wins, so a nested workspace finds itself", () => {
126
+ mkdirSync(join(root, ".git"));
127
+ declare(root);
128
+ const inner = join(root, "vendor", "kit");
129
+ declare(inner, "chant.workspace.jsonc");
130
+ mkdirSync(join(inner, "src"));
131
+ expect(findWorkspaceRoot(join(inner, "src"))).toEqual({ dir: inner, file: join(inner, "chant.workspace.jsonc") });
132
+ });
133
+
134
+ test("stops at the git root after checking it", () => {
135
+ declare(root);
136
+ const repo = join(root, "repo");
137
+ mkdirSync(join(repo, ".git"), { recursive: true });
138
+ mkdirSync(join(repo, "src"));
139
+ expect(findWorkspaceRoot(join(repo, "src"))).toBeUndefined();
140
+ declare(repo);
141
+ expect(findWorkspaceRoot(join(repo, "src"))?.dir).toBe(repo);
142
+ });
143
+
144
+ test("a .git file (a worktree or submodule) is a git root too", () => {
145
+ declare(root);
146
+ const repo = join(root, "wt");
147
+ mkdirSync(repo);
148
+ writeFileSync(join(repo, ".git"), "gitdir: /elsewhere\n");
149
+ expect(findWorkspaceRoot(repo)).toBeUndefined();
150
+ });
151
+
152
+ test("outside a git repository only the start directory is checked", () => {
153
+ declare(root);
154
+ const sub = join(root, "sub");
155
+ mkdirSync(sub);
156
+ expect(findWorkspaceRoot(sub)).toBeUndefined();
157
+ expect(findWorkspaceRoot(root)?.dir).toBe(root);
158
+ });
159
+
160
+ test("both names present is reported, not thrown", () => {
161
+ mkdirSync(join(root, ".git"));
162
+ declare(root);
163
+ declare(root, "chant.workspace.jsonc");
164
+ expect(findWorkspaceRoot(root)).toEqual({
165
+ dir: root,
166
+ file: join(root, "chant.workspace.json"),
167
+ conflicting: join(root, "chant.workspace.jsonc"),
168
+ });
169
+ });
170
+ });
@@ -76,3 +76,51 @@ export function findProjectConfig(startDir: string): ProjectConfigSearch {
76
76
  export function findProjectRoot(startDir: string): string {
77
77
  return findProjectConfig(startDir).dir;
78
78
  }
79
+
80
+ /** The two names a workspace declaration may have (#2524 D1). */
81
+ export const WORKSPACE_DECLARATION_FILES = ["chant.workspace.json", "chant.workspace.jsonc"] as const;
82
+
83
+ /** Where {@link findWorkspaceRoot} found a declaration. */
84
+ export interface WorkspaceRootSearch {
85
+ /** The directory holding the declaration. */
86
+ dir: string;
87
+ /** Absolute path of the declaration file. When both names are present this is the `.json` one. */
88
+ file: string;
89
+ /**
90
+ * Absolute path of the `.jsonc` file when `.json` is also there. Having both
91
+ * is an error, which the reader reports; the walk itself never throws.
92
+ */
93
+ conflicting?: string;
94
+ }
95
+
96
+ /**
97
+ * Walk up from `startDir` (inclusive) to the nearest `chant.workspace.json` or
98
+ * `chant.workspace.jsonc` (#2524 D1). The walk stops at the git root, the first
99
+ * directory holding `.git`, after checking it. Outside a git repository only
100
+ * `startDir` itself is checked, so a stray declaration in an unrelated
101
+ * ancestor is never picked up.
102
+ *
103
+ * It only tests whether files exist and loads nothing else, so a level-0
104
+ * command can afford to call it. {@link findProjectConfig} is unchanged and
105
+ * never looks at a declaration.
106
+ */
107
+ export function findWorkspaceRoot(startDir: string): WorkspaceRootSearch | undefined {
108
+ const start = resolve(startDir);
109
+ let top: string | undefined;
110
+ for (let dir = start; ; dir = dirname(dir)) {
111
+ if (existsSync(join(dir, ".git"))) {
112
+ top = dir;
113
+ break;
114
+ }
115
+ if (dirname(dir) === dir) break;
116
+ }
117
+ for (let dir = start; ; dir = dirname(dir)) {
118
+ const [json, jsonc] = WORKSPACE_DECLARATION_FILES.map((name) => join(dir, name));
119
+ const hasJson = existsSync(json);
120
+ const hasJsonc = existsSync(jsonc);
121
+ if (hasJson && hasJsonc) return { dir, file: json, conflicting: jsonc };
122
+ if (hasJson) return { dir, file: json };
123
+ if (hasJsonc) return { dir, file: jsonc };
124
+ if (top === undefined || dir === top || dirname(dir) === dir) return undefined;
125
+ }
126
+ }